mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +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