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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
            .from((db) => {
 | 
			
		||||
                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 { publicSignupTokenUpdateSchema } from './spec/public-signup-token-update-schema';
 | 
			
		||||
import apiVersion from '../util/version';
 | 
			
		||||
import { profileSchema } from './spec/profile-schema';
 | 
			
		||||
 | 
			
		||||
// All schemas in `openapi/spec` should be listed here.
 | 
			
		||||
export const schemas = {
 | 
			
		||||
@ -196,6 +197,7 @@ export const schemas = {
 | 
			
		||||
    publicSignupTokenUpdateSchema,
 | 
			
		||||
    publicSignupTokensSchema,
 | 
			
		||||
    publicSignupTokenSchema,
 | 
			
		||||
    profileSchema,
 | 
			
		||||
    proxyClientSchema,
 | 
			
		||||
    proxyFeaturesSchema,
 | 
			
		||||
    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 { PasswordSchema } from '../../../openapi/spec/password-schema';
 | 
			
		||||
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 {
 | 
			
		||||
    private accessService: AccessService;
 | 
			
		||||
@ -28,6 +34,8 @@ class UserController extends Controller {
 | 
			
		||||
 | 
			
		||||
    private openApiService: OpenApiService;
 | 
			
		||||
 | 
			
		||||
    private projectService: ProjectService;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        config: IUnleashConfig,
 | 
			
		||||
        {
 | 
			
		||||
@ -36,6 +44,7 @@ class UserController extends Controller {
 | 
			
		||||
            userFeedbackService,
 | 
			
		||||
            userSplashService,
 | 
			
		||||
            openApiService,
 | 
			
		||||
            projectService,
 | 
			
		||||
        }: Pick<
 | 
			
		||||
            IUnleashServices,
 | 
			
		||||
            | 'accessService'
 | 
			
		||||
@ -43,6 +52,7 @@ class UserController extends Controller {
 | 
			
		||||
            | 'userFeedbackService'
 | 
			
		||||
            | 'userSplashService'
 | 
			
		||||
            | 'openApiService'
 | 
			
		||||
            | 'projectService'
 | 
			
		||||
        >,
 | 
			
		||||
    ) {
 | 
			
		||||
        super(config);
 | 
			
		||||
@ -51,6 +61,7 @@ class UserController extends Controller {
 | 
			
		||||
        this.userFeedbackService = userFeedbackService;
 | 
			
		||||
        this.userSplashService = userSplashService;
 | 
			
		||||
        this.openApiService = openApiService;
 | 
			
		||||
        this.projectService = projectService;
 | 
			
		||||
 | 
			
		||||
        this.route({
 | 
			
		||||
            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({
 | 
			
		||||
            method: 'post',
 | 
			
		||||
            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(
 | 
			
		||||
        req: IAuthRequest<unknown, unknown, PasswordSchema>,
 | 
			
		||||
        res: Response,
 | 
			
		||||
 | 
			
		||||
@ -574,6 +574,10 @@ export default class ProjectService {
 | 
			
		||||
        return this.store.getMembersCountByProject(projectId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProjectsByUser(userId: number): Promise<string[]> {
 | 
			
		||||
        return this.store.getProjectsByUser(userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProjectOverview(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        archived: boolean = false,
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ export interface IProjectStore extends Store<IProject, string> {
 | 
			
		||||
    deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
 | 
			
		||||
    getEnvironmentsForProject(id: string): Promise<string[]>;
 | 
			
		||||
    getMembersCountByProject(projectId: string): Promise<number>;
 | 
			
		||||
    getProjectsByUser(userId: number): Promise<string[]>;
 | 
			
		||||
    getMembersCount(): Promise<IProjectMembersCount[]>;
 | 
			
		||||
    getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
 | 
			
		||||
    count(): Promise<number>;
 | 
			
		||||
 | 
			
		||||
@ -2128,6 +2128,39 @@ exports[`should serve the OpenAPI spec 1`] = `
 | 
			
		||||
        ],
 | 
			
		||||
        "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": {
 | 
			
		||||
        "additionalProperties": false,
 | 
			
		||||
        "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": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "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[]> {
 | 
			
		||||
        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