diff --git a/apps/user-service/src/users/users.controller.ts b/apps/user-service/src/users/users.controller.ts index 8d5ffb5..2cc64ce 100644 --- a/apps/user-service/src/users/users.controller.ts +++ b/apps/user-service/src/users/users.controller.ts @@ -53,13 +53,16 @@ export class UsersController { @Request() { authenticatedUserId } ): Promise { const userId = pathId === 'me' ? authenticatedUserId : parseInt(pathId); - const user = await User.findOne({ include: ['roles', 'organisation'], where: { id: userId }, }); + const user = await User.findOne({ + include: ['roles', 'organisation', 'frameworks'], + where: { id: userId }, + }); if (user == null) throw new NotFoundException(); await this.policyService.authorize('read', user); const document = buildJsonApi(); - const userResource = document.addData(user.uuid, new UserDto(user, await user.frameworks())); + const userResource = document.addData(user.uuid, new UserDto(user, await user.myFrameworks())); const org = await user.primaryOrganisation(); if (org != null) { diff --git a/libs/database/src/lib/entities/framework-user.entity.ts b/libs/database/src/lib/entities/framework-user.entity.ts new file mode 100644 index 0000000..ab6d5de --- /dev/null +++ b/libs/database/src/lib/entities/framework-user.entity.ts @@ -0,0 +1,20 @@ +import { AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'; +import { BIGINT } from 'sequelize'; +import { Framework } from './framework.entity'; +import { User } from './user.entity'; + +@Table({ tableName: 'framework_user', underscored: true }) +export class FrameworkUser extends Model { + @PrimaryKey + @AutoIncrement + @Column(BIGINT.UNSIGNED) + override id: number; + + @ForeignKey(() => Framework) + @Column(BIGINT.UNSIGNED) + frameworkId: number; + + @ForeignKey(() => User) + @Column(BIGINT.UNSIGNED) + userId: number; +} diff --git a/libs/database/src/lib/entities/index.ts b/libs/database/src/lib/entities/index.ts index bb15fd4..0967a28 100644 --- a/libs/database/src/lib/entities/index.ts +++ b/libs/database/src/lib/entities/index.ts @@ -1,5 +1,6 @@ export * from './delayed-job.entity'; export * from './framework.entity'; +export * from './framework-user.entity'; export * from './model-has-role.entity' export * from './organisation.entity'; export * from './organisation-user.entity'; diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts index 4e9bbd5..d795151 100644 --- a/libs/database/src/lib/entities/user.entity.ts +++ b/libs/database/src/lib/entities/user.entity.ts @@ -1,15 +1,17 @@ +import { uniq } from 'lodash'; import { AllowNull, AutoIncrement, BelongsTo, BelongsToMany, - Column, Default, + Column, + Default, ForeignKey, Index, Model, PrimaryKey, Table, - Unique + Unique, } from 'sequelize-typescript'; import { BIGINT, BOOLEAN, col, DATE, fn, Op, STRING, UUID } from 'sequelize'; import { Role } from './role.entity'; @@ -20,6 +22,7 @@ import { Project } from './project.entity'; import { ProjectUser } from './project-user.entity'; import { Organisation } from './organisation.entity'; import { OrganisationUser } from './organisation-user.entity'; +import { FrameworkUser } from './framework-user.entity'; @Table({ tableName: 'users', underscored: true, paranoid: true }) export class User extends Model { @@ -257,39 +260,59 @@ export class User extends Model { : this._primaryOrganisation; } - private _frameworks?: Framework[]; - async frameworks(): Promise { - if (this._frameworks == null) { + @BelongsToMany(() => Framework, () => FrameworkUser) + frameworks: Framework[]; + + async loadFrameworks() { + if (this.frameworks == null) { + this.frameworks = await (this as User).$get('frameworks'); + } + return this.frameworks; + } + + private _myFrameworks?: Framework[]; + async myFrameworks(): Promise { + if (this._myFrameworks == null) { await this.loadRoles(); const isAdmin = this.roles.find(({ name }) => name.startsWith('admin-')) != null; - let frameworkSlugs: string[]; + await this.loadFrameworks(); + + let frameworkSlugs: string[] = this.frameworks.map(({ slug }) => slug); if (isAdmin) { // Admins have access to all frameworks their permissions say they do const permissions = await Permission.getUserPermissionNames(this.id); const prefix = 'framework-'; - frameworkSlugs = permissions - .filter((permission) => permission.startsWith(prefix)) - .map((permission) => permission.substring(prefix.length)); + frameworkSlugs = [ + ...frameworkSlugs, + ...permissions + .filter((permission) => permission.startsWith(prefix)) + .map((permission) => permission.substring(prefix.length)), + ]; } else { // Other users have access to the frameworks embodied by their set of projects - frameworkSlugs = ( - await (this as User).$get('projects', { - attributes: [ - [fn('DISTINCT', col('Project.framework_key')), 'frameworkKey'], - ], - raw: true, - }) - ).map(({ frameworkKey }) => frameworkKey); + frameworkSlugs = [ + ...frameworkSlugs, + ...( + await (this as User).$get('projects', { + attributes: [ + [fn('DISTINCT', col('Project.framework_key')), 'frameworkKey'], + ], + raw: true, + }) + ).map(({ frameworkKey }) => frameworkKey), + ]; } - if (frameworkSlugs.length == 0) return (this._frameworks = []); - return (this._frameworks = await Framework.findAll({ + if (frameworkSlugs.length == 0) return (this._myFrameworks = []); + + frameworkSlugs = uniq(frameworkSlugs); + return (this._myFrameworks = await Framework.findAll({ where: { slug: { [Op.in]: frameworkSlugs } }, })); } - return this._frameworks; + return this._myFrameworks; } } diff --git a/package-lock.json b/package-lock.json index 197a44c..f4b0b90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "lodash": "^4.17.21", "mariadb": "^3.3.2", "mysql2": "^3.11.2", "nestjs-request-context": "^3.0.0", @@ -51,6 +52,7 @@ "@swc/helpers": "~0.5.11", "@types/bcryptjs": "^2.4.6", "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.13", "@types/node": "~18.16.9", "@types/supertest": "^6.0.2", "@types/validator": "^13.12.2", @@ -4685,6 +4687,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", diff --git a/package.json b/package.json index 1252ebf..48c2e49 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "lodash": "^4.17.21", "mariadb": "^3.3.2", "mysql2": "^3.11.2", "nestjs-request-context": "^3.0.0", @@ -47,6 +48,7 @@ "@swc/helpers": "~0.5.11", "@types/bcryptjs": "^2.4.6", "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.13", "@types/node": "~18.16.9", "@types/supertest": "^6.0.2", "@types/validator": "^13.12.2",