1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +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:
sjaanus 2022-09-29 15:27:54 +02:00 committed by GitHub
parent 0302b3d2e3
commit ec7e256140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 192 additions and 1 deletions

View File

@ -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')

View File

@ -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,

View 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();
});

View 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>;

View File

@ -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,

View File

@ -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,

View File

@ -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>;

View File

@ -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",

View File

@ -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.');
}
}