mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Backend for profile page (#2114)
* First version of profile * Fix tests * Fix typings * Replace where to andwhere to be more clear
This commit is contained in:
		
							parent
							
								
									0302b3d2e3
								
							
						
					
					
						commit
						ec7e256140
					
				@ -288,7 +288,31 @@ class ProjectStore implements IProjectStore {
 | 
				
			|||||||
        return members;
 | 
					        return members;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getMembersCountByProject(projectId?: string): Promise<number> {
 | 
					    async getProjectsByUser(userId: number): Promise<string[]> {
 | 
				
			||||||
 | 
					        const members = await this.db.from((db) => {
 | 
				
			||||||
 | 
					            db.select('project')
 | 
				
			||||||
 | 
					                .from('role_user')
 | 
				
			||||||
 | 
					                .leftJoin('roles', 'role_user.role_id', 'roles.id')
 | 
				
			||||||
 | 
					                .where('type', 'root')
 | 
				
			||||||
 | 
					                .andWhere('name', 'Editor')
 | 
				
			||||||
 | 
					                .andWhere('user_id', userId)
 | 
				
			||||||
 | 
					                .union((queryBuilder) => {
 | 
				
			||||||
 | 
					                    queryBuilder
 | 
				
			||||||
 | 
					                        .select('project')
 | 
				
			||||||
 | 
					                        .from('group_role')
 | 
				
			||||||
 | 
					                        .leftJoin(
 | 
				
			||||||
 | 
					                            'group_user',
 | 
				
			||||||
 | 
					                            'group_user.group_id',
 | 
				
			||||||
 | 
					                            'group_role.group_id',
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .where('user_id', userId);
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .as('query');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return members;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getMembersCountByProject(projectId: string): Promise<number> {
 | 
				
			||||||
        const members = await this.db
 | 
					        const members = await this.db
 | 
				
			||||||
            .from((db) => {
 | 
					            .from((db) => {
 | 
				
			||||||
                db.select('user_id')
 | 
					                db.select('user_id')
 | 
				
			||||||
 | 
				
			|||||||
@ -120,6 +120,7 @@ import { publicSignupTokenSchema } from './spec/public-signup-token-schema';
 | 
				
			|||||||
import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema';
 | 
					import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema';
 | 
				
			||||||
import { publicSignupTokenUpdateSchema } from './spec/public-signup-token-update-schema';
 | 
					import { publicSignupTokenUpdateSchema } from './spec/public-signup-token-update-schema';
 | 
				
			||||||
import apiVersion from '../util/version';
 | 
					import apiVersion from '../util/version';
 | 
				
			||||||
 | 
					import { profileSchema } from './spec/profile-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// All schemas in `openapi/spec` should be listed here.
 | 
					// All schemas in `openapi/spec` should be listed here.
 | 
				
			||||||
export const schemas = {
 | 
					export const schemas = {
 | 
				
			||||||
@ -196,6 +197,7 @@ export const schemas = {
 | 
				
			|||||||
    publicSignupTokenUpdateSchema,
 | 
					    publicSignupTokenUpdateSchema,
 | 
				
			||||||
    publicSignupTokensSchema,
 | 
					    publicSignupTokensSchema,
 | 
				
			||||||
    publicSignupTokenSchema,
 | 
					    publicSignupTokenSchema,
 | 
				
			||||||
 | 
					    profileSchema,
 | 
				
			||||||
    proxyClientSchema,
 | 
					    proxyClientSchema,
 | 
				
			||||||
    proxyFeaturesSchema,
 | 
					    proxyFeaturesSchema,
 | 
				
			||||||
    proxyFeatureSchema,
 | 
					    proxyFeatureSchema,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								src/lib/openapi/spec/profile-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/lib/openapi/spec/profile-schema.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { validateSchema } from '../validate';
 | 
				
			||||||
 | 
					import { ProfileSchema } from './profile-schema';
 | 
				
			||||||
 | 
					import { RoleName } from '../../types/model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('profileSchema', () => {
 | 
				
			||||||
 | 
					    const data: ProfileSchema = {
 | 
				
			||||||
 | 
					        rootRole: 'Editor' as RoleName,
 | 
				
			||||||
 | 
					        projects: ['default', 'secretproject'],
 | 
				
			||||||
 | 
					        features: [
 | 
				
			||||||
 | 
					            { name: 'firstFeature', project: 'default' },
 | 
				
			||||||
 | 
					            { name: 'secondFeature', project: 'secretproject' },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        validateSchema('#/components/schemas/profileSchema', data),
 | 
				
			||||||
 | 
					    ).toBeUndefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/lib/openapi/spec/profile-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/lib/openapi/spec/profile-schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import { FromSchema } from 'json-schema-to-ts';
 | 
				
			||||||
 | 
					import { featureSchema } from './feature-schema';
 | 
				
			||||||
 | 
					import { RoleName } from '../../types/model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const profileSchema = {
 | 
				
			||||||
 | 
					    $id: '#/components/schemas/profileSchema',
 | 
				
			||||||
 | 
					    type: 'object',
 | 
				
			||||||
 | 
					    additionalProperties: false,
 | 
				
			||||||
 | 
					    required: ['rootRole', 'projects', 'features'],
 | 
				
			||||||
 | 
					    properties: {
 | 
				
			||||||
 | 
					        rootRole: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            enum: Object.values(RoleName),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        projects: {
 | 
				
			||||||
 | 
					            type: 'array',
 | 
				
			||||||
 | 
					            items: {
 | 
				
			||||||
 | 
					                type: 'string',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        features: {
 | 
				
			||||||
 | 
					            type: 'array',
 | 
				
			||||||
 | 
					            items: {
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/featureSchema',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    components: {
 | 
				
			||||||
 | 
					        schemas: {
 | 
				
			||||||
 | 
					            featureSchema,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ProfileSchema = FromSchema<typeof profileSchema>;
 | 
				
			||||||
@ -16,6 +16,12 @@ import { serializeDates } from '../../../types/serialize-dates';
 | 
				
			|||||||
import { IUserPermission } from '../../../types/stores/access-store';
 | 
					import { IUserPermission } from '../../../types/stores/access-store';
 | 
				
			||||||
import { PasswordSchema } from '../../../openapi/spec/password-schema';
 | 
					import { PasswordSchema } from '../../../openapi/spec/password-schema';
 | 
				
			||||||
import { emptyResponse } from '../../../openapi/util/standard-responses';
 | 
					import { emptyResponse } from '../../../openapi/util/standard-responses';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    profileSchema,
 | 
				
			||||||
 | 
					    ProfileSchema,
 | 
				
			||||||
 | 
					} from '../../../openapi/spec/profile-schema';
 | 
				
			||||||
 | 
					import ProjectService from '../../../services/project-service';
 | 
				
			||||||
 | 
					import { RoleName } from '../../../types/model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserController extends Controller {
 | 
					class UserController extends Controller {
 | 
				
			||||||
    private accessService: AccessService;
 | 
					    private accessService: AccessService;
 | 
				
			||||||
@ -28,6 +34,8 @@ class UserController extends Controller {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private openApiService: OpenApiService;
 | 
					    private openApiService: OpenApiService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private projectService: ProjectService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        config: IUnleashConfig,
 | 
					        config: IUnleashConfig,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -36,6 +44,7 @@ class UserController extends Controller {
 | 
				
			|||||||
            userFeedbackService,
 | 
					            userFeedbackService,
 | 
				
			||||||
            userSplashService,
 | 
					            userSplashService,
 | 
				
			||||||
            openApiService,
 | 
					            openApiService,
 | 
				
			||||||
 | 
					            projectService,
 | 
				
			||||||
        }: Pick<
 | 
					        }: Pick<
 | 
				
			||||||
            IUnleashServices,
 | 
					            IUnleashServices,
 | 
				
			||||||
            | 'accessService'
 | 
					            | 'accessService'
 | 
				
			||||||
@ -43,6 +52,7 @@ class UserController extends Controller {
 | 
				
			|||||||
            | 'userFeedbackService'
 | 
					            | 'userFeedbackService'
 | 
				
			||||||
            | 'userSplashService'
 | 
					            | 'userSplashService'
 | 
				
			||||||
            | 'openApiService'
 | 
					            | 'openApiService'
 | 
				
			||||||
 | 
					            | 'projectService'
 | 
				
			||||||
        >,
 | 
					        >,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        super(config);
 | 
					        super(config);
 | 
				
			||||||
@ -51,6 +61,7 @@ class UserController extends Controller {
 | 
				
			|||||||
        this.userFeedbackService = userFeedbackService;
 | 
					        this.userFeedbackService = userFeedbackService;
 | 
				
			||||||
        this.userSplashService = userSplashService;
 | 
					        this.userSplashService = userSplashService;
 | 
				
			||||||
        this.openApiService = openApiService;
 | 
					        this.openApiService = openApiService;
 | 
				
			||||||
 | 
					        this.projectService = projectService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.route({
 | 
					        this.route({
 | 
				
			||||||
            method: 'get',
 | 
					            method: 'get',
 | 
				
			||||||
@ -66,6 +77,20 @@ class UserController extends Controller {
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.route({
 | 
				
			||||||
 | 
					            method: 'get',
 | 
				
			||||||
 | 
					            path: '/profile',
 | 
				
			||||||
 | 
					            handler: this.getProfile,
 | 
				
			||||||
 | 
					            permission: NONE,
 | 
				
			||||||
 | 
					            middleware: [
 | 
				
			||||||
 | 
					                openApiService.validPath({
 | 
				
			||||||
 | 
					                    tags: ['Users'],
 | 
				
			||||||
 | 
					                    operationId: 'getProfile',
 | 
				
			||||||
 | 
					                    responses: { 200: createResponseSchema('profileSchema') },
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.route({
 | 
					        this.route({
 | 
				
			||||||
            method: 'post',
 | 
					            method: 'post',
 | 
				
			||||||
            path: '/change-password',
 | 
					            path: '/change-password',
 | 
				
			||||||
@ -114,6 +139,30 @@ class UserController extends Controller {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getProfile(
 | 
				
			||||||
 | 
					        req: IAuthRequest,
 | 
				
			||||||
 | 
					        res: Response<ProfileSchema>,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        const { user } = req;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const projects = await this.projectService.getProjectsByUser(user.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const roles = await this.accessService.getUserRootRoles(user.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const responseData: ProfileSchema = {
 | 
				
			||||||
 | 
					            projects,
 | 
				
			||||||
 | 
					            rootRole: roles[0].name as RoleName,
 | 
				
			||||||
 | 
					            features: [],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.openApiService.respondWithValidation(
 | 
				
			||||||
 | 
					            200,
 | 
				
			||||||
 | 
					            res,
 | 
				
			||||||
 | 
					            profileSchema.$id,
 | 
				
			||||||
 | 
					            responseData,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async changeMyPassword(
 | 
					    async changeMyPassword(
 | 
				
			||||||
        req: IAuthRequest<unknown, unknown, PasswordSchema>,
 | 
					        req: IAuthRequest<unknown, unknown, PasswordSchema>,
 | 
				
			||||||
        res: Response,
 | 
					        res: Response,
 | 
				
			||||||
 | 
				
			|||||||
@ -574,6 +574,10 @@ export default class ProjectService {
 | 
				
			|||||||
        return this.store.getMembersCountByProject(projectId);
 | 
					        return this.store.getMembersCountByProject(projectId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getProjectsByUser(userId: number): Promise<string[]> {
 | 
				
			||||||
 | 
					        return this.store.getProjectsByUser(userId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getProjectOverview(
 | 
					    async getProjectOverview(
 | 
				
			||||||
        projectId: string,
 | 
					        projectId: string,
 | 
				
			||||||
        archived: boolean = false,
 | 
					        archived: boolean = false,
 | 
				
			||||||
 | 
				
			|||||||
@ -36,6 +36,7 @@ export interface IProjectStore extends Store<IProject, string> {
 | 
				
			|||||||
    deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
 | 
					    deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
 | 
				
			||||||
    getEnvironmentsForProject(id: string): Promise<string[]>;
 | 
					    getEnvironmentsForProject(id: string): Promise<string[]>;
 | 
				
			||||||
    getMembersCountByProject(projectId: string): Promise<number>;
 | 
					    getMembersCountByProject(projectId: string): Promise<number>;
 | 
				
			||||||
 | 
					    getProjectsByUser(userId: number): Promise<string[]>;
 | 
				
			||||||
    getMembersCount(): Promise<IProjectMembersCount[]>;
 | 
					    getMembersCount(): Promise<IProjectMembersCount[]>;
 | 
				
			||||||
    getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
 | 
					    getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
 | 
				
			||||||
    count(): Promise<number>;
 | 
					    count(): Promise<number>;
 | 
				
			||||||
 | 
				
			|||||||
@ -2128,6 +2128,39 @@ exports[`should serve the OpenAPI spec 1`] = `
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "profileSchema": {
 | 
				
			||||||
 | 
					        "additionalProperties": false,
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "features": {
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/featureSchema",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "projects": {
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "type": "string",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "rootRole": {
 | 
				
			||||||
 | 
					            "enum": [
 | 
				
			||||||
 | 
					              "Admin",
 | 
				
			||||||
 | 
					              "Editor",
 | 
				
			||||||
 | 
					              "Viewer",
 | 
				
			||||||
 | 
					              "Owner",
 | 
				
			||||||
 | 
					              "Member",
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "rootRole",
 | 
				
			||||||
 | 
					          "projects",
 | 
				
			||||||
 | 
					          "features",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "projectEnvironmentSchema": {
 | 
					      "projectEnvironmentSchema": {
 | 
				
			||||||
        "additionalProperties": false,
 | 
					        "additionalProperties": false,
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
@ -7013,6 +7046,26 @@ If the provided project does not exist, the list of events will be empty.",
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/api/admin/user/profile": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "operationId": "getProfile",
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "content": {
 | 
				
			||||||
 | 
					              "application/json": {
 | 
				
			||||||
 | 
					                "schema": {
 | 
				
			||||||
 | 
					                  "$ref": "#/components/schemas/profileSchema",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "description": "profileSchema",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "Users",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/api/admin/user/tokens": {
 | 
					    "/api/admin/user/tokens": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "operationId": "getPats",
 | 
					        "operationId": "getPats",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							@ -127,4 +127,9 @@ export default class FakeProjectStore implements IProjectStore {
 | 
				
			|||||||
    getMembersCount(): Promise<IProjectMembersCount[]> {
 | 
					    getMembersCount(): Promise<IProjectMembersCount[]> {
 | 
				
			||||||
        throw new Error('Method not implemented.');
 | 
					        throw new Error('Method not implemented.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					    getProjectsByUser(userId: number): Promise<string[]> {
 | 
				
			||||||
 | 
					        throw new Error('Method not implemented.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user