From f981e6f6251b460c44e1cbc62bc67430a959de0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roso=20Pe=C3=B1aranda?= Date: Sun, 26 Jul 2020 17:30:44 -0500 Subject: [PATCH 1/5] feat: create role module --- packages/api/src/App.module.ts | 8 +- packages/api/src/Role/Role.entity.ts | 64 ++++++++++ packages/api/src/Role/Role.module.ts | 12 ++ packages/api/src/Role/Role.resolver.ts | 44 +++++++ packages/api/src/Role/Role.service.ts | 113 ++++++++++++++++++ packages/api/src/Role/RoleType.enum.ts | 5 + .../api/src/Role/decorator/Role.decorator.ts | 3 + .../api/src/Role/guards/Role.guard.spec.ts | 7 ++ packages/api/src/Role/guards/Role.guard.ts | 26 ++++ .../api/src/Role/pipe/Role-validation.pipe.ts | 31 +++++ packages/api/src/User/User.entity.ts | 43 ++++++- packages/api/src/schema.gql | 32 +++-- 12 files changed, 375 insertions(+), 13 deletions(-) create mode 100644 packages/api/src/Role/Role.entity.ts create mode 100644 packages/api/src/Role/Role.module.ts create mode 100644 packages/api/src/Role/Role.resolver.ts create mode 100644 packages/api/src/Role/Role.service.ts create mode 100644 packages/api/src/Role/RoleType.enum.ts create mode 100644 packages/api/src/Role/decorator/Role.decorator.ts create mode 100644 packages/api/src/Role/guards/Role.guard.spec.ts create mode 100644 packages/api/src/Role/guards/Role.guard.ts create mode 100644 packages/api/src/Role/pipe/Role-validation.pipe.ts diff --git a/packages/api/src/App.module.ts b/packages/api/src/App.module.ts index 05a505fe..f52c67f5 100644 --- a/packages/api/src/App.module.ts +++ b/packages/api/src/App.module.ts @@ -19,6 +19,7 @@ import { StorySectionModule } from './StorySection'; import { UserConceptModule } from './UserConcepts'; import { UserModule } from './User'; import { UserModuleModule } from './UserModule'; +import { RoleModule } from './Role/Role.module'; /** * Export these dependencies so they can be used in testing @@ -41,20 +42,21 @@ export const appImports = [ UserConceptModule, UserModule, UserModuleModule, + RoleModule, DatabaseModule, GraphQLModule.forRoot({ installSubscriptionHandlers: true, autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'), - context: ({ req }) => ({ req }) - }) + context: ({ req }) => ({ req }), + }), ]; /** * Main App module for NestJS */ @Module({ - imports: appImports + imports: appImports, }) export class AppModule {} diff --git a/packages/api/src/Role/Role.entity.ts b/packages/api/src/Role/Role.entity.ts new file mode 100644 index 00000000..4cb930f0 --- /dev/null +++ b/packages/api/src/Role/Role.entity.ts @@ -0,0 +1,64 @@ +import { + Entity, + Column, + ManyToMany, + JoinColumn, + Unique, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { UserWithPassword } from '../User/User.entity'; +import { CMBaseEntity } from '../lib/Base.entity'; +import { RoleType } from './RoleType.enum'; +import { + ObjectType, + Field, + ID, + InputType, + registerEnumType, +} from '@nestjs/graphql'; + +registerEnumType(RoleType, { + name: 'RoleType', +}); + +@ObjectType() +export class Roles { + @Field(() => ID) + id: string; + + @Field() + name: RoleType; + + @Field() + description: string; +} + +@Entity('roles') +@Unique(['name']) +export class Role extends CMBaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: 'simple-enum', + enum: RoleType, + default: RoleType.STUDENT, + }) + name: RoleType; + + @Column({ type: 'varchar', length: 100 }) + description!: string; + + @ManyToMany(() => UserWithPassword, (user) => user.roles) + @JoinColumn() + users!: UserWithPassword[]; +} + +@InputType() +export class RoleInput { + @Field() + name: RoleType; + + @Field() + description: string; +} diff --git a/packages/api/src/Role/Role.module.ts b/packages/api/src/Role/Role.module.ts new file mode 100644 index 00000000..0ad629e2 --- /dev/null +++ b/packages/api/src/Role/Role.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RoleService } from './Role.service'; +import { RoleResolver } from './Role.resolver'; +import { Role } from './Role.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Role])], + providers: [RoleService, RoleResolver], + exports: [RoleService], +}) +export class RoleModule {} diff --git a/packages/api/src/Role/Role.resolver.ts b/packages/api/src/Role/Role.resolver.ts new file mode 100644 index 00000000..f72c2edb --- /dev/null +++ b/packages/api/src/Role/Role.resolver.ts @@ -0,0 +1,44 @@ +import { UsePipes, ValidationPipe } from '@nestjs/common'; +import { RoleService } from './Role.service'; +import { Roles, RoleInput } from './Role.entity'; + +import { RoleValidationPipe } from './pipe/Role-validation.pipe'; +import { Query, Args, Mutation, Resolver } from '@nestjs/graphql'; + +@Resolver(() => Roles) +export class RoleResolver { + constructor(private readonly _roleService: RoleService) {} + + @Query(() => String) + sayHello() { + return "let's start"; + } + + @Query(() => Roles) + searchRoles(@Args('roleId') roleId: string) { + return this._roleService.findById(roleId); + } + + @Query(() => [Roles]) + findAll() { + return this._roleService.findAll(); + } + + @Mutation(() => Roles) + @UsePipes(ValidationPipe) + @UsePipes(RoleValidationPipe) + createRole(@Args('role') role: RoleInput) { + return this._roleService.create(role); + } + + @Mutation(() => Roles) + @UsePipes(ValidationPipe) + updateRole(@Args('role') roleId: string, role: RoleInput) { + return this._roleService.update(roleId, role); + } + + @Mutation(() => Boolean) + deleteRole(@Args('roleId') roleId: string) { + return this._roleService.delete(roleId); + } +} diff --git a/packages/api/src/Role/Role.service.ts b/packages/api/src/Role/Role.service.ts new file mode 100644 index 00000000..2498ef8b --- /dev/null +++ b/packages/api/src/Role/Role.service.ts @@ -0,0 +1,113 @@ +import { + Injectable, + BadRequestException, + NotFoundException, + InternalServerErrorException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Role, RoleInput } from './Role.entity'; + +import { Repository } from 'typeorm'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Role) + private readonly _roleRepository: Repository + ) {} + + async findById(rolId: string): Promise { + const success = false; + let message = 'rolId must be sent'; + if (!rolId) { + throw new BadRequestException({ success, message }); + } + let role: Role | undefined; + try { + role = await this._roleRepository.findOne(rolId); + } catch (error) { + message = `we had a problem making the request to the database: ${error.error}`; + throw new InternalServerErrorException({ success, message }); + } + + if (!role) { + message = 'rolId not found'; + throw new NotFoundException({ success, message }); + } + + return role; + } + + async findAll(): Promise { + let roles: Role[]; + try { + roles = await this._roleRepository.find(); + } catch (error) { + const success = false; + const message = 'we had a problem making the request to the database'; + throw new InternalServerErrorException({ success, message }); + } + return roles; + } + + async create(role: RoleInput): Promise { + let savedRole; + let foundRole: Role | undefined; + + try { + foundRole = await this._roleRepository.findOne({ + where: { name: role.name }, + }); + } catch (error) { + console.log(error); + throw new InternalServerErrorException(); + } + + if (foundRole) { + const message = 'Role name already exists'; + throw new BadRequestException({ message }); + } else { + try { + savedRole = await this._roleRepository.save(role); + } catch (error) { + console.log(error); + throw new InternalServerErrorException(); + } + } + + return savedRole; + } + + async update(roleId: string, role: RoleInput): Promise { + const foundRole = await this._roleRepository.findOne(roleId); + + if (!foundRole) { + throw new NotFoundException('roles search failed'); + } + let updateRole; + try { + await this._roleRepository.update({ id: roleId }, role); + updateRole = await this._roleRepository.findOne(roleId); + } catch (error) { + throw new InternalServerErrorException(); + } + + return updateRole; + } + + async delete(roleId: string): Promise { + const roleExists = await this._roleRepository.findOne(roleId); + if (!roleExists) { + throw new NotFoundException('rolesId not found'); + } + + try { + await this._roleRepository.delete(roleId); + } catch (error) { + throw new InternalServerErrorException( + 'we had a problem making the request to the database' + ); + } + return true; + } +} diff --git a/packages/api/src/Role/RoleType.enum.ts b/packages/api/src/Role/RoleType.enum.ts new file mode 100644 index 00000000..ac217c35 --- /dev/null +++ b/packages/api/src/Role/RoleType.enum.ts @@ -0,0 +1,5 @@ +export enum RoleType { + ADMIN = 'ADMIN', + STUDENT = 'STUDENT', + MENTOR = 'MENTOR', +} diff --git a/packages/api/src/Role/decorator/Role.decorator.ts b/packages/api/src/Role/decorator/Role.decorator.ts new file mode 100644 index 00000000..b0376727 --- /dev/null +++ b/packages/api/src/Role/decorator/Role.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; + +export const Roles = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/packages/api/src/Role/guards/Role.guard.spec.ts b/packages/api/src/Role/guards/Role.guard.spec.ts new file mode 100644 index 00000000..9773f31d --- /dev/null +++ b/packages/api/src/Role/guards/Role.guard.spec.ts @@ -0,0 +1,7 @@ +import { RoleGuard } from './role.guard'; + +describe('RoleGuard', () => { + it('should be defined', () => { + expect(new RoleGuard()).toBeDefined(); + }); +}); diff --git a/packages/api/src/Role/guards/Role.guard.ts b/packages/api/src/Role/guards/Role.guard.ts new file mode 100644 index 00000000..aae59251 --- /dev/null +++ b/packages/api/src/Role/guards/Role.guard.ts @@ -0,0 +1,26 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class RoleGuard implements CanActivate { + constructor(private readonly _reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const roles: string[] = this._reflector.get( + 'roles', + context.getHandler(), + ); + + if (!roles) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const { user } = request; + + const hasRole = () => + user.roles.some((role: string) => roles.includes(role)); + + return user && user.roles && hasRole(); + } +} diff --git a/packages/api/src/Role/pipe/Role-validation.pipe.ts b/packages/api/src/Role/pipe/Role-validation.pipe.ts new file mode 100644 index 00000000..2bf02b99 --- /dev/null +++ b/packages/api/src/Role/pipe/Role-validation.pipe.ts @@ -0,0 +1,31 @@ +import { + ArgumentMetadata, + BadRequestException, + Injectable, + PipeTransform, +} from '@nestjs/common'; + +import { plainToClass } from 'class-transformer'; +import { RoleType } from '../RoleType.enum'; +import { RoleInput } from '../Role.entity'; + +@Injectable() +export class RoleValidationPipe implements PipeTransform { + async transform( + value: RoleInput, + { metatype }: ArgumentMetadata + ): Promise { + const newRol: RoleInput = plainToClass(RoleInput, value); + + const roles = Object.values(RoleType); + console.log(metatype); + + if (!roles.includes(newRol.name)) { + throw new BadRequestException( + `Role validation fail, name should be in [${roles}]` + ); + } + + return value; + } +} diff --git a/packages/api/src/User/User.entity.ts b/packages/api/src/User/User.entity.ts index e2fa45f1..53281ff6 100644 --- a/packages/api/src/User/User.entity.ts +++ b/packages/api/src/User/User.entity.ts @@ -1,10 +1,22 @@ import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; -import { Column, CreateDateColumn, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + Unique, + ManyToMany, + JoinTable, +} from 'typeorm'; import { CMBaseEntity } from '../lib/Base.entity'; import { PathUser } from '../PathUser/PathUser.entity'; import { UserPreferences } from '../UserPreferences/UserPreferences.entity'; import { UserModule } from '../UserModule/UserModule.entity'; +import { Role, Roles } from '../Role/Role.entity'; +import { RoleType } from '../Role/RoleType.enum'; @ObjectType() export class User { @@ -23,6 +35,9 @@ export class User { @Field() profileImage: string; + @Field(() => Roles) + roles: Roles[]; + @Field(() => UserPreferences, { nullable: true }) userPreferences?: UserPreferences; @@ -54,12 +69,16 @@ export class UserWithPassword extends CMBaseEntity { @CreateDateColumn() createdAt: Date; - @OneToMany(() => PathUser, pathUser => pathUser.user) + @OneToMany(() => PathUser, (pathUser) => pathUser.user) pathUser: PathUser[]; - @OneToMany(() => UserModule, userModules => userModules.user) + @OneToMany(() => UserModule, (userModules) => userModules.user) userModules: UserModule[]; + @ManyToMany(() => Role, (role) => role.users, { eager: true }) + @JoinTable({ name: 'user_roles' }) + roles: Role[]; + @OneToOne(() => UserPreferences) userPreferences: UserPreferences; } @@ -78,3 +97,21 @@ export class UserInput { @Field() password: string; } + +@InputType() +export class UserUpdate { + @Field({ nullable: true }) + firstName?: string; + + @Field({ nullable: true }) + lastName?: string; + + @Field({ nullable: true }) + email?: string; + + @Field({ nullable: true }) + password?: string; + + @Field(() => RoleType) + roles?: RoleType[]; +} diff --git a/packages/api/src/schema.gql b/packages/api/src/schema.gql index 46be23b3..4599ed30 100644 --- a/packages/api/src/schema.gql +++ b/packages/api/src/schema.gql @@ -2,13 +2,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ -type Assignment { - id: String! - description: String! - moduleId: String! - module: ModuleAssignment! -} - type StorySection { content: String! } @@ -73,6 +66,13 @@ type ModuleLesson { lesson: Lesson! } +type Assignment { + id: String! + description: String! + moduleId: String! + module: ModuleAssignment! +} + type Character { id: String! name: String! @@ -113,12 +113,19 @@ type UserModule { completedAt: DateTime } +type Roles { + id: ID! + name: String! + description: String! +} + type User { id: ID! firstName: String! lastName: String! email: String! profileImage: String! + roles: Roles! userPreferences: UserPreferences createdAt: DateTime! } @@ -206,6 +213,9 @@ type Query { path(id: String!): Path! questions(type: String, moduleIndex: Float!, pathId: String!): [Question!]! lessonStorySections(lessonId: String!): [StorySection!]! + sayHello: String! + searchRoles(roleId: String!): Roles! + findAll: [Roles!]! } type Mutation { @@ -231,6 +241,9 @@ type Mutation { joinPaths(paths: [String!]!): Boolean! updatePath(path: UpdatePathInput!): Path! completeModule(moduleName: String!): UserModule! + createRole(role: RoleInput!): Roles! + updateRole(role: String!): Roles! + deleteRole(roleId: String!): Boolean! } input CreateAssignmentFileInput { @@ -301,3 +314,8 @@ input UpdatePathInput { description: String characterId: String } + +input RoleInput { + name: String! + description: String! +} From 912bfb70cbe3aaafe8a5a5b0eb5a565ea5f3f5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roso=20Pe=C3=B1aranda?= Date: Tue, 28 Jul 2020 22:36:36 -0500 Subject: [PATCH 2/5] feat: created Role module --- .eslintrc | 32 ++-- packages/api/src/Auth/index.ts | 7 +- packages/api/src/Database/index.ts | 11 +- .../src/Database/seeders/Seeders.service.ts | 137 +++++++++++------- packages/api/src/Role/Role.entity.ts | 29 ++-- packages/api/src/Role/Role.module.ts | 6 +- packages/api/src/Role/Role.resolver.ts | 25 ++-- packages/api/src/Role/Role.service.ts | 27 ++-- .../api/src/Role/decorator/Role.decorator.ts | 2 +- packages/api/src/Role/guards/Role.guard.ts | 29 +++- packages/api/src/User/User.entity.ts | 10 +- packages/api/src/User/User.resolver.ts | 27 +++- packages/api/src/User/User.service.ts | 89 +++++++++++- packages/api/src/User/index.ts | 16 +- packages/api/src/schema.gql | 16 +- 15 files changed, 324 insertions(+), 139 deletions(-) diff --git a/.eslintrc b/.eslintrc index dc925e66..b71eecbc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,13 +3,9 @@ "plugins": ["@typescript-eslint", "import"], "extends": ["airbnb"], - "ignorePatterns": [ - "node_modules", - "dist", - "packages/api/types.ts" - ], + "ignorePatterns": ["node_modules", "dist", "packages/api/types.ts"], - "env":{ + "env": { "browser": true, "node": true, "jasmine": true @@ -34,11 +30,14 @@ "import/no-extraneous-dependencies": 0, "import/extensions": 0, "import-name": 0, - "no-unused-vars": ["error", { - "argsIgnorePattern": "^_", - "args": "after-used", - "ignoreRestSiblings": true - }], + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "args": "after-used", + "ignoreRestSiblings": true + } + ], "@typescript-eslint/no-unused-vars": ["error"], "array-bracket-spacing": ["error", "never"], @@ -54,10 +53,13 @@ "jsx-a11y/no-static-element-interactions": 0, "linebreak-style": 0, "max-classes-per-file": 0, // Leads to too many files - "no-empty-function": ["error", {"allow": ["constructors"]}], // Needed by NestJS dep injection - "no-empty": [2, { - "allowEmptyCatch": true - }], + "no-empty-function": ["error", { "allow": ["constructors"] }], // Needed by NestJS dep injection + "no-empty": [ + 2, + { + "allowEmptyCatch": true + } + ], "no-underscore-dangle": 0, "no-useless-constructor": 0, // Needed by NestJS dep injection "object-curly-newline": 0, diff --git a/packages/api/src/Auth/index.ts b/packages/api/src/Auth/index.ts index 2e265376..96fb5533 100644 --- a/packages/api/src/Auth/index.ts +++ b/packages/api/src/Auth/index.ts @@ -8,17 +8,18 @@ import { AuthService } from './Auth.service'; import { JWTStrategy } from './strategies/jwt.strategy'; import { LocalStrategy } from './strategies/local.strategy'; - @Module({ imports: [ UserModule, - PassportModule, + PassportModule.register({ + defaultStrategy: 'jwt' + }), JwtModule.register({ secret: config.get('auth.jwtSecret'), signOptions: { expiresIn: '1h' } }) ], providers: [AuthResolver, AuthService, LocalStrategy, JWTStrategy], - exports: [AuthService] + exports: [AuthService, PassportModule] }) export class AuthModule {} diff --git a/packages/api/src/Database/index.ts b/packages/api/src/Database/index.ts index 586a300a..055c65f9 100644 --- a/packages/api/src/Database/index.ts +++ b/packages/api/src/Database/index.ts @@ -28,7 +28,8 @@ import { DatabaseService } from './Database.service'; import { SeederService } from './seeders/Seeders.service'; import { TypeORMModule } from './TypeORM.module'; import { CMS } from '../CMS/CMS'; - +import { Role } from '../Role/Role.entity'; +import { RoleService } from '../Role/Role.service'; /** * Main Database Module, used in App.module and testing @@ -47,7 +48,8 @@ import { CMS } from '../CMS/CMS'; UserConcept, Friend, Character, - UserModule + UserModule, + Role ]) ], providers: [ @@ -65,8 +67,9 @@ import { CMS } from '../CMS/CMS'; FriendService, CharacterService, PathUserService, - UserModuleService + UserModuleService, + RoleService ], exports: [DatabaseService, SeederService] }) -export class DatabaseModule { } +export class DatabaseModule {} diff --git a/packages/api/src/Database/seeders/Seeders.service.ts b/packages/api/src/Database/seeders/Seeders.service.ts index 2ad4afa1..53303124 100644 --- a/packages/api/src/Database/seeders/Seeders.service.ts +++ b/packages/api/src/Database/seeders/Seeders.service.ts @@ -20,7 +20,8 @@ import { UserPreferencesService } from '../../UserPreferences/UserPreferences.se import { DatabaseService } from '../Database.service'; import * as random from './random'; import { CMS } from '../../CMS/CMS'; - +import { RoleService } from '../../Role/Role.service'; +import { RoleType } from '../../Role/RoleType.enum'; interface CTX { users: UserWithPassword[]; @@ -32,7 +33,6 @@ interface CTX { @Injectable() export class SeederService { - /** * Initializes the database service * @param connection The connection, which gets injected @@ -48,8 +48,9 @@ export class SeederService { public assignmentFileService: AssignmentFileService, public conceptService: ConceptService, public friendService: FriendService, - public characterService: CharacterService - ) { } + public characterService: CharacterService, + public roleService: RoleService + ) {} db = new DatabaseService(this.connection); @@ -66,54 +67,61 @@ export class SeederService { * @param num Number of users you want to create */ async seedUsers(num: number = 4): Promise { - return Promise.all(Array(num).fill(undefined).map(async (_, i) => { - const user = await this.userService.create( - random.userInput({ email: `user${i}@test.com` }) - ); - - if (i % 2 === 0) { - await this.userPreferencesService.update( - user.id, - random.userPreferenceInput() - ); - } - return user; - })); + return Promise.all( + Array(num) + .fill(undefined) + .map(async (_, i) => { + const user = await this.userService.create( + random.userInput({ email: `user${i}@test.com` }) + ); + + if (i % 2 === 0) { + await this.userPreferencesService.update( + user.id, + random.userPreferenceInput() + ); + } + return user; + }) + ); } async seedCharacters(num: number = 3): Promise { - return Promise.all(Array(num).fill(undefined).map(async () => this.characterService.create( - random.characterInput() - ))); + return Promise.all( + Array(num) + .fill(undefined) + .map(async () => this.characterService.create(random.characterInput())) + ); } - - async seedPaths(users: UserWithPassword[] = [], characters: Character[] = []) { + async seedPaths( + users: UserWithPassword[] = [], + characters: Character[] = [] + ) { const paths = [ { id: 'js', name: 'Javascript', icon: 'js' }, { id: 'css', name: 'CSS', icon: 'css' }, { id: 'html', name: 'HTML', icon: 'html' } ]; - return Promise.all(paths.map(async (path, i) => { - let newPath = new Path(); - - if ((i % 2 === 0) && (i < characters.length)) { - newPath = await this.pathService.create( - random.pathInput({ ...path, characterId: characters[i].id }) - ); - } else { - newPath = await this.pathService.create( - random.pathInput(path) - ); - } - if (i === 0 && users.length) { - await this.pathService.addUserToPath(users[0].id, newPath.id); - } - return newPath; - })); - } + return Promise.all( + paths.map(async (path, i) => { + let newPath = new Path(); + if (i % 2 === 0 && i < characters.length) { + newPath = await this.pathService.create( + random.pathInput({ ...path, characterId: characters[i].id }) + ); + } else { + newPath = await this.pathService.create(random.pathInput(path)); + } + if (i === 0 && users.length) { + await this.pathService.addUserToPath(users[0].id, newPath.id); + } + return newPath; + }) + ); + } async seedConcept( numConcept: number = 3, @@ -121,16 +129,20 @@ export class SeederService { users: UserWithPassword[] ) { const modules = Object.values(this.cms.modules); - const numMod = (numModule < modules.length) ? numModule : modules.length; - return Promise.all(Array(numConcept).fill(undefined).map(async (_, i) => { - const concept = await this.conceptService.create( - random.conceptInput(modules[i % numMod].id) - ); - - if (i === 0) { - await this.conceptService.addUserConcept(concept.id, users[0].id); - } - })); + const numMod = numModule < modules.length ? numModule : modules.length; + return Promise.all( + Array(numConcept) + .fill(undefined) + .map(async (_, i) => { + const concept = await this.conceptService.create( + random.conceptInput(modules[i % numMod].id) + ); + + if (i === 0) { + await this.conceptService.addUserConcept(concept.id, users[0].id); + } + }) + ); } async seedFriend(users: UserWithPassword[]) { @@ -156,6 +168,25 @@ export class SeederService { } } + /** + * Add Roles + */ + + async seedRoles() { + await this.roleService.create({ + name: RoleType.ADMIN, + description: 'Site Admin' + }); + await this.roleService.create({ + name: RoleType.MENTOR, + description: 'Site Mentor' + }); + await this.roleService.create({ + name: RoleType.STUDENT, + description: 'Site STUDENT' + }); + } + /** * Seeds all entities in the database */ @@ -214,6 +245,12 @@ export class SeederService { task: async (ctx: CTX) => { await this.seedFriend(ctx.users); } + }, + { + title: 'Create Roles', + task: async () => { + await this.seedRoles(); + } } ]).run(); } diff --git a/packages/api/src/Role/Role.entity.ts b/packages/api/src/Role/Role.entity.ts index 4cb930f0..f5a4c771 100644 --- a/packages/api/src/Role/Role.entity.ts +++ b/packages/api/src/Role/Role.entity.ts @@ -4,21 +4,23 @@ import { ManyToMany, JoinColumn, Unique, - PrimaryGeneratedColumn, + PrimaryGeneratedColumn } from 'typeorm'; -import { UserWithPassword } from '../User/User.entity'; -import { CMBaseEntity } from '../lib/Base.entity'; -import { RoleType } from './RoleType.enum'; + import { ObjectType, Field, ID, InputType, - registerEnumType, + registerEnumType } from '@nestjs/graphql'; +import { CMBaseEntity } from '../lib/Base.entity'; +import { RoleType } from './RoleType.enum'; +import { UserWithPassword } from '../User/User.entity'; + registerEnumType(RoleType, { - name: 'RoleType', + name: 'RoleType' }); @ObjectType() @@ -42,14 +44,14 @@ export class Role extends CMBaseEntity { @Column({ type: 'simple-enum', enum: RoleType, - default: RoleType.STUDENT, + default: RoleType.STUDENT }) name: RoleType; @Column({ type: 'varchar', length: 100 }) - description!: string; + description: string; - @ManyToMany(() => UserWithPassword, (user) => user.roles) + @ManyToMany(() => UserWithPassword, user => user.roles) @JoinColumn() users!: UserWithPassword[]; } @@ -62,3 +64,12 @@ export class RoleInput { @Field() description: string; } + +@InputType() +export class RoleUpdateInput { + @Field() + id: string; + + @Field() + description: string; +} diff --git a/packages/api/src/Role/Role.module.ts b/packages/api/src/Role/Role.module.ts index 0ad629e2..4e826ecd 100644 --- a/packages/api/src/Role/Role.module.ts +++ b/packages/api/src/Role/Role.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +// import { PassportModule } from '@nestjs/passport'; import { RoleService } from './Role.service'; import { RoleResolver } from './Role.resolver'; import { Role } from './Role.entity'; +import { AuthModule } from '../Auth'; @Module({ - imports: [TypeOrmModule.forFeature([Role])], + imports: [TypeOrmModule.forFeature([Role]), AuthModule], providers: [RoleService, RoleResolver], - exports: [RoleService], + exports: [RoleService] }) export class RoleModule {} diff --git a/packages/api/src/Role/Role.resolver.ts b/packages/api/src/Role/Role.resolver.ts index f72c2edb..6ec731de 100644 --- a/packages/api/src/Role/Role.resolver.ts +++ b/packages/api/src/Role/Role.resolver.ts @@ -1,26 +1,27 @@ -import { UsePipes, ValidationPipe } from '@nestjs/common'; +import { UsePipes, ValidationPipe, UseGuards } from '@nestjs/common'; +import { Query, Args, Mutation, Resolver } from '@nestjs/graphql'; import { RoleService } from './Role.service'; -import { Roles, RoleInput } from './Role.entity'; +import { Roles, RoleInput, RoleUpdateInput } from './Role.entity'; +import { RolArray } from './decorator/Role.decorator'; import { RoleValidationPipe } from './pipe/Role-validation.pipe'; -import { Query, Args, Mutation, Resolver } from '@nestjs/graphql'; +import { RoleType } from './RoleType.enum'; +import { RoleGuard } from './guards/Role.guard'; +import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; +@RolArray(RoleType.ADMIN) +@UseGuards(GQLAuthGuard, RoleGuard) @Resolver(() => Roles) export class RoleResolver { constructor(private readonly _roleService: RoleService) {} - @Query(() => String) - sayHello() { - return "let's start"; - } - @Query(() => Roles) - searchRoles(@Args('roleId') roleId: string) { + searchRoleById(@Args('roleId') roleId: string) { return this._roleService.findById(roleId); } @Query(() => [Roles]) - findAll() { + findRoles() { return this._roleService.findAll(); } @@ -33,8 +34,8 @@ export class RoleResolver { @Mutation(() => Roles) @UsePipes(ValidationPipe) - updateRole(@Args('role') roleId: string, role: RoleInput) { - return this._roleService.update(roleId, role); + updateRole(@Args('role') role: RoleUpdateInput) { + return this._roleService.update(role); } @Mutation(() => Boolean) diff --git a/packages/api/src/Role/Role.service.ts b/packages/api/src/Role/Role.service.ts index 2498ef8b..aa624f8b 100644 --- a/packages/api/src/Role/Role.service.ts +++ b/packages/api/src/Role/Role.service.ts @@ -2,12 +2,11 @@ import { Injectable, BadRequestException, NotFoundException, - InternalServerErrorException, + InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Role, RoleInput } from './Role.entity'; - import { Repository } from 'typeorm'; +import { Role, RoleInput, RoleUpdateInput } from './Role.entity'; @Injectable() export class RoleService { @@ -56,11 +55,10 @@ export class RoleService { try { foundRole = await this._roleRepository.findOne({ - where: { name: role.name }, + where: { name: role.name } }); } catch (error) { - console.log(error); - throw new InternalServerErrorException(); + throw new InternalServerErrorException(error.message); } if (foundRole) { @@ -68,31 +66,30 @@ export class RoleService { throw new BadRequestException({ message }); } else { try { - savedRole = await this._roleRepository.save(role); + savedRole = await this._roleRepository.create(role).save(); } catch (error) { - console.log(error); - throw new InternalServerErrorException(); + throw new InternalServerErrorException(error.message); } } return savedRole; } - async update(roleId: string, role: RoleInput): Promise { - const foundRole = await this._roleRepository.findOne(roleId); + async update(role: RoleUpdateInput): Promise { + let foundRole = await this._roleRepository.findOne(role.id); if (!foundRole) { throw new NotFoundException('roles search failed'); } - let updateRole; + try { - await this._roleRepository.update({ id: roleId }, role); - updateRole = await this._roleRepository.findOne(roleId); + foundRole.description = role.description; + foundRole = await this._roleRepository.save(foundRole); } catch (error) { throw new InternalServerErrorException(); } - return updateRole; + return foundRole; } async delete(roleId: string): Promise { diff --git a/packages/api/src/Role/decorator/Role.decorator.ts b/packages/api/src/Role/decorator/Role.decorator.ts index b0376727..9be8580d 100644 --- a/packages/api/src/Role/decorator/Role.decorator.ts +++ b/packages/api/src/Role/decorator/Role.decorator.ts @@ -1,3 +1,3 @@ import { SetMetadata } from '@nestjs/common'; -export const Roles = (...roles: string[]) => SetMetadata('roles', roles); +export const RolArray = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/packages/api/src/Role/guards/Role.guard.ts b/packages/api/src/Role/guards/Role.guard.ts index aae59251..50841459 100644 --- a/packages/api/src/Role/guards/Role.guard.ts +++ b/packages/api/src/Role/guards/Role.guard.ts @@ -1,5 +1,12 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException +} from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { Role } from '../Role.entity'; @Injectable() export class RoleGuard implements CanActivate { @@ -8,19 +15,29 @@ export class RoleGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const roles: string[] = this._reflector.get( 'roles', - context.getHandler(), + context.getHandler() ); if (!roles) { return true; } - const request = context.switchToHttp().getRequest(); - const { user } = request; + const ctx = GqlExecutionContext.create(context); + + const { req } = ctx.getContext(); + + const { user } = req; const hasRole = () => - user.roles.some((role: string) => roles.includes(role)); + user.roles.some((role: Role) => roles.includes(role.name)); + + const authorized = hasRole(); + if (!authorized) { + throw new UnauthorizedException( + "You haven't permissions to access this resource" + ); + } - return user && user.roles && hasRole(); + return user && user.roles && authorized; } } diff --git a/packages/api/src/User/User.entity.ts b/packages/api/src/User/User.entity.ts index 53281ff6..4e9fc012 100644 --- a/packages/api/src/User/User.entity.ts +++ b/packages/api/src/User/User.entity.ts @@ -8,7 +8,7 @@ import { PrimaryGeneratedColumn, Unique, ManyToMany, - JoinTable, + JoinTable } from 'typeorm'; import { CMBaseEntity } from '../lib/Base.entity'; @@ -35,7 +35,7 @@ export class User { @Field() profileImage: string; - @Field(() => Roles) + @Field(() => [Roles]) roles: Roles[]; @Field(() => UserPreferences, { nullable: true }) @@ -69,13 +69,13 @@ export class UserWithPassword extends CMBaseEntity { @CreateDateColumn() createdAt: Date; - @OneToMany(() => PathUser, (pathUser) => pathUser.user) + @OneToMany(() => PathUser, pathUser => pathUser.user) pathUser: PathUser[]; - @OneToMany(() => UserModule, (userModules) => userModules.user) + @OneToMany(() => UserModule, userModules => userModules.user) userModules: UserModule[]; - @ManyToMany(() => Role, (role) => role.users, { eager: true }) + @ManyToMany(() => Role, role => role.users, { eager: true }) @JoinTable({ name: 'user_roles' }) roles: Role[]; diff --git a/packages/api/src/User/User.resolver.ts b/packages/api/src/User/User.resolver.ts index 706ed5c2..00578bc0 100644 --- a/packages/api/src/User/User.resolver.ts +++ b/packages/api/src/User/User.resolver.ts @@ -17,6 +17,7 @@ import { UserPreferencesInput } from '../UserPreferences/UserPreferences.entity'; import { UserPreferencesService } from '../UserPreferences/UserPreferences.service'; +import { RoleType } from '../Role/RoleType.enum'; @Resolver(() => User) export class UserResolver { @@ -53,7 +54,7 @@ export class UserResolver { updatePreferences( @CurrentUser() user: User, @Args('preferences', { type: () => UserPreferencesInput }) - preferences: UserPreferencesInput + preferences: UserPreferencesInput ) { return this.userPreferencesService.update(user.id, preferences); } @@ -64,8 +65,26 @@ export class UserResolver { @ResolveField(() => UserPreferences) async userPreferences(@Parent() user: User) { - return this.userPreferencesService.findByUser( - user.id - ); + return this.userPreferencesService.findByUser(user.id); + } + + // ---------------------------- + // -----------Roles ----------- + // ---------------------------- + + @Mutation(() => User) + addRoleToUser( + @Args('userId') userId: string, + @Args('roleName') roleName: RoleType + ) { + return this.userService.addRoleToUser(userId, roleName); + } + + @Mutation(() => User) + removeRoleToUser( + @Args('userId') userId: string, + @Args('roleName') roleName: RoleType + ) { + return this.userService.removeRoleToUser(userId, roleName); } } diff --git a/packages/api/src/User/User.service.ts b/packages/api/src/User/User.service.ts index 298b5645..f8adc33f 100644 --- a/packages/api/src/User/User.service.ts +++ b/packages/api/src/User/User.service.ts @@ -1,16 +1,25 @@ -import { Injectable } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + InternalServerErrorException +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import bcrypt from 'bcrypt'; import md5 from 'md5'; import { Repository } from 'typeorm'; import { UserInput, UserWithPassword } from './User.entity'; +import { Role } from '../Role/Role.entity'; +import { RoleType } from '../Role/RoleType.enum'; @Injectable() export class UserService { constructor( @InjectRepository(UserWithPassword) - private readonly userRepository: Repository + private readonly userRepository: Repository, + + @InjectRepository(Role) + private readonly roleRepository: Repository ) {} async findAll(): Promise { @@ -29,6 +38,15 @@ export class UserService { async create(input: UserInput): Promise { const user = this.userRepository.create(input); + + const defaultRole = await this.roleRepository.findOne({ + where: { name: RoleType.STUDENT } + }); + + if (defaultRole !== undefined) { + user.roles = [defaultRole]; + } + user.password = await bcrypt.hash(input.password, 10); // When in production add // ?r=pg&d=https:%3A%2F%2Fapi.codementoring.co%2Fstatic%2Fdefault-profile.svg; @@ -36,4 +54,71 @@ export class UserService { user.profileImage = `https://www.gravatar.com/avatar/${md5(input.email)}`; return this.userRepository.save(user); } + + async addRoleToUser(userId: string, roleName: RoleType) { + let user: UserWithPassword | undefined; + try { + user = await this.userRepository.findOne({ + where: { id: userId } + }); + } catch (error) { + const message = `we had a problem making the request to the database: ${error.error}`; + throw new InternalServerErrorException(message); + } + + if (!user) { + throw new NotFoundException('User id not found'); + } + + let newRole: Role | undefined; + try { + newRole = await this.roleRepository.findOne({ + where: { name: roleName } + }); + } catch (error) { + const message = `we had a problem making the request to the database: ${error.error}`; + throw new InternalServerErrorException(message); + } + + if (newRole) { + if (!user.roles.find(rl => rl.name === newRole?.name)) { + user.roles.push(newRole); + user = await this.userRepository.save(user); + } + } + + return user; + } + + async removeRoleToUser(userId: string, roleName: RoleType) { + let user; + try { + user = await this.userRepository.findOne({ + where: { id: userId } + }); + } catch (error) { + const message = `we had a problem making the request to the database: ${error.error}`; + throw new InternalServerErrorException(message); + } + + if (!user) { + throw new NotFoundException('User id not found'); + } + + let role: Role | undefined; + try { + role = await this.roleRepository.findOne({ + where: { name: roleName } + }); + } catch (error) { + const message = `we had a problem making the request to the database: ${error.error}`; + throw new InternalServerErrorException(message); + } + + if (role) { + user.roles = user.roles.filter(rl => rl.name !== role?.name); + } + + return this.userRepository.save(user); + } } diff --git a/packages/api/src/User/index.ts b/packages/api/src/User/index.ts index 37184e88..1249b8ff 100644 --- a/packages/api/src/User/index.ts +++ b/packages/api/src/User/index.ts @@ -9,14 +9,18 @@ import { UserPreferencesService } from '../UserPreferences/UserPreferences.servi import { PathService } from '../Path/Path.service'; import { PathUser } from '../PathUser/PathUser.entity'; import { Path } from '../Path/Path.entity'; +import { Role } from '../Role/Role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([ - UserWithPassword, - UserPreferences, - Path, - PathUser - ])], + imports: [ + TypeOrmModule.forFeature([ + UserWithPassword, + UserPreferences, + Path, + PathUser, + Role + ]) + ], providers: [ UserResolver, UserService, diff --git a/packages/api/src/schema.gql b/packages/api/src/schema.gql index 4599ed30..30491121 100644 --- a/packages/api/src/schema.gql +++ b/packages/api/src/schema.gql @@ -125,7 +125,7 @@ type User { lastName: String! email: String! profileImage: String! - roles: Roles! + roles: [Roles!]! userPreferences: UserPreferences createdAt: DateTime! } @@ -213,9 +213,8 @@ type Query { path(id: String!): Path! questions(type: String, moduleIndex: Float!, pathId: String!): [Question!]! lessonStorySections(lessonId: String!): [StorySection!]! - sayHello: String! - searchRoles(roleId: String!): Roles! - findAll: [Roles!]! + searchRoleById(roleId: String!): Roles! + findRoles: [Roles!]! } type Mutation { @@ -224,6 +223,8 @@ type Mutation { deleteAssignmentFile(assignmentFileId: String!): Boolean! createUser(user: UserInput!): User! updatePreferences(preferences: UserPreferencesInput!): UserPreferences! + addRoleToUser(roleName: String!, userId: String!): User! + removeRoleToUser(roleName: String!, userId: String!): User! login(password: String!, email: String!): LoginOutput! createCharacter(character: CreateCharacterInput!): Character! updateCharacter(character: UpdateCharacterInput!): Character! @@ -242,7 +243,7 @@ type Mutation { updatePath(path: UpdatePathInput!): Path! completeModule(moduleName: String!): UserModule! createRole(role: RoleInput!): Roles! - updateRole(role: String!): Roles! + updateRole(role: RoleUpdateInput!): Roles! deleteRole(roleId: String!): Boolean! } @@ -319,3 +320,8 @@ input RoleInput { name: String! description: String! } + +input RoleUpdateInput { + id: String! + description: String! +} From fb481b69203bf47bc47118e06e323b9501c404ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roso=20Pe=C3=B1aranda?= Date: Wed, 29 Jul 2020 16:44:29 -0500 Subject: [PATCH 3/5] feat: created Role module --- packages/api/src/App.module.ts | 6 +- .../api/src/Role/guards/Role.guard.spec.ts | 7 -- .../api/src/Role/pipe/Role-validation.pipe.ts | 13 +-- packages/api/src/User/User.resolver.ts | 2 +- packages/api/src/User/User.resolver.ts~ | 90 +++++++++++++++++++ 5 files changed, 96 insertions(+), 22 deletions(-) delete mode 100644 packages/api/src/Role/guards/Role.guard.spec.ts create mode 100644 packages/api/src/User/User.resolver.ts~ diff --git a/packages/api/src/App.module.ts b/packages/api/src/App.module.ts index f52c67f5..2f449040 100644 --- a/packages/api/src/App.module.ts +++ b/packages/api/src/App.module.ts @@ -49,14 +49,14 @@ export const appImports = [ GraphQLModule.forRoot({ installSubscriptionHandlers: true, autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'), - context: ({ req }) => ({ req }), - }), + context: ({ req }) => ({ req }) + }) ]; /** * Main App module for NestJS */ @Module({ - imports: appImports, + imports: appImports }) export class AppModule {} diff --git a/packages/api/src/Role/guards/Role.guard.spec.ts b/packages/api/src/Role/guards/Role.guard.spec.ts deleted file mode 100644 index 9773f31d..00000000 --- a/packages/api/src/Role/guards/Role.guard.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RoleGuard } from './role.guard'; - -describe('RoleGuard', () => { - it('should be defined', () => { - expect(new RoleGuard()).toBeDefined(); - }); -}); diff --git a/packages/api/src/Role/pipe/Role-validation.pipe.ts b/packages/api/src/Role/pipe/Role-validation.pipe.ts index 2bf02b99..371334be 100644 --- a/packages/api/src/Role/pipe/Role-validation.pipe.ts +++ b/packages/api/src/Role/pipe/Role-validation.pipe.ts @@ -1,9 +1,4 @@ -import { - ArgumentMetadata, - BadRequestException, - Injectable, - PipeTransform, -} from '@nestjs/common'; +import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { RoleType } from '../RoleType.enum'; @@ -11,14 +6,10 @@ import { RoleInput } from '../Role.entity'; @Injectable() export class RoleValidationPipe implements PipeTransform { - async transform( - value: RoleInput, - { metatype }: ArgumentMetadata - ): Promise { + async transform(value: RoleInput): Promise { const newRol: RoleInput = plainToClass(RoleInput, value); const roles = Object.values(RoleType); - console.log(metatype); if (!roles.includes(newRol.name)) { throw new BadRequestException( diff --git a/packages/api/src/User/User.resolver.ts b/packages/api/src/User/User.resolver.ts index 00578bc0..e1fc06f9 100644 --- a/packages/api/src/User/User.resolver.ts +++ b/packages/api/src/User/User.resolver.ts @@ -54,7 +54,7 @@ export class UserResolver { updatePreferences( @CurrentUser() user: User, @Args('preferences', { type: () => UserPreferencesInput }) - preferences: UserPreferencesInput + preferences: UserPreferencesInput ) { return this.userPreferencesService.update(user.id, preferences); } diff --git a/packages/api/src/User/User.resolver.ts~ b/packages/api/src/User/User.resolver.ts~ new file mode 100644 index 00000000..00578bc0 --- /dev/null +++ b/packages/api/src/User/User.resolver.ts~ @@ -0,0 +1,90 @@ +import { UseGuards } from '@nestjs/common'; +import { + Args, + Mutation, + Query, + Resolver, + ResolveField, + Parent +} from '@nestjs/graphql'; + +import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; +import { User, UserInput } from './User.entity'; +import { UserService } from './User.service'; +import { CurrentUser } from './CurrentUser.decorator'; +import { + UserPreferences, + UserPreferencesInput +} from '../UserPreferences/UserPreferences.entity'; +import { UserPreferencesService } from '../UserPreferences/UserPreferences.service'; +import { RoleType } from '../Role/RoleType.enum'; + +@Resolver(() => User) +export class UserResolver { + constructor( + private readonly userService: UserService, + private readonly userPreferencesService: UserPreferencesService + ) {} + + @UseGuards(GQLAuthGuard) + @Query(() => [User]) + users() { + return this.userService.findAll(); + } + + @UseGuards(GQLAuthGuard) + @Query(() => [User]) + searchUsers(@Args('query') query: string) { + return this.userService.search(query); + } + + @UseGuards(GQLAuthGuard) + @Query(() => User) + me(@CurrentUser() user: User) { + return user; + } + + @Mutation(() => User) + createUser(@Args({ name: 'user', type: () => UserInput }) user: UserInput) { + return this.userService.create(user); + } + + @UseGuards(GQLAuthGuard) + @Mutation(() => UserPreferences) + updatePreferences( + @CurrentUser() user: User, + @Args('preferences', { type: () => UserPreferencesInput }) + preferences: UserPreferencesInput + ) { + return this.userPreferencesService.update(user.id, preferences); + } + + // --------------------------------------------------------------------------- + // -------------------------------------------------------------------- Fields + // --------------------------------------------------------------------------- + + @ResolveField(() => UserPreferences) + async userPreferences(@Parent() user: User) { + return this.userPreferencesService.findByUser(user.id); + } + + // ---------------------------- + // -----------Roles ----------- + // ---------------------------- + + @Mutation(() => User) + addRoleToUser( + @Args('userId') userId: string, + @Args('roleName') roleName: RoleType + ) { + return this.userService.addRoleToUser(userId, roleName); + } + + @Mutation(() => User) + removeRoleToUser( + @Args('userId') userId: string, + @Args('roleName') roleName: RoleType + ) { + return this.userService.removeRoleToUser(userId, roleName); + } +} From 832d0cb72f22b43ca7931aa71cba5c470b5100fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roso=20Pe=C3=B1aranda?= Date: Fri, 31 Jul 2020 21:48:44 -0500 Subject: [PATCH 4/5] fix: bug in Seeders fixed --- .../src/Database/seeders/Seeders.service.ts | 12 +++++----- packages/api/src/schema.gql | 24 ++++--------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/api/src/Database/seeders/Seeders.service.ts b/packages/api/src/Database/seeders/Seeders.service.ts index 53303124..edd70671 100644 --- a/packages/api/src/Database/seeders/Seeders.service.ts +++ b/packages/api/src/Database/seeders/Seeders.service.ts @@ -198,6 +198,12 @@ export class SeederService { this.db.DANGEROUSLY_RESET_DATABASE(); } }, + { + title: 'Create Roles', + task: async () => { + await this.seedRoles(); + } + }, { title: 'Create users', task: async (ctx: CTX) => { @@ -245,12 +251,6 @@ export class SeederService { task: async (ctx: CTX) => { await this.seedFriend(ctx.users); } - }, - { - title: 'Create Roles', - task: async () => { - await this.seedRoles(); - } } ]).run(); } diff --git a/packages/api/src/schema.gql b/packages/api/src/schema.gql index 441d55c0..a9ec4b06 100644 --- a/packages/api/src/schema.gql +++ b/packages/api/src/schema.gql @@ -11,11 +11,7 @@ type Lesson { questions: [Question!]! } -union Question = - QuestionMultiChoice - | QuestionMemory - | QuestionDragDrop - | QuestionBugHighlight +union Question = QuestionMultiChoice | QuestionMemory | QuestionDragDrop | QuestionBugHighlight type QuestionMultiChoice { id: String! @@ -220,14 +216,10 @@ type Query { modules: [Module!]! pathModules(pathId: String!): [Module!]! paths( - """ - Only get paths the current user has not joined - """ + """Only get paths the current user has not joined""" notJoined: Boolean = false - """ - Only get the current user's paths - """ + """Only get the current user's paths""" onlyJoined: Boolean = false ): [Path!]! path(id: String!): Path! @@ -238,9 +230,7 @@ type Query { } type Mutation { - createAssignmentFile( - assignmentFile: CreateAssignmentFileInput! - ): AssignmentFile! + createAssignmentFile(assignmentFile: CreateAssignmentFileInput!): AssignmentFile! updateAssignmentFile(file: UpdateAssignmentFileInput!): AssignmentFile! deleteAssignmentFile(assignmentFileId: String!): Boolean! createUser(user: UserInput!): User! @@ -259,11 +249,7 @@ type Mutation { deleteConcept(conceptId: String!): Boolean! learnConcept(conceptId: String!): Boolean! createFriendship(toId: String!): FriendOutput! - respondToFriendRequest( - response: String! - user2Id: String! - user1Id: String! - ): Friend! + respondToFriendRequest(response: String!, user2Id: String!, user1Id: String!): Friend! deleteFriendship(friendId: String!): Boolean! createPath(path: PathInput!): Path! joinPath(pathId: String!): Boolean! From 6bbaa5b8a55d31c9e94358bb34186959fab8c03d Mon Sep 17 00:00:00 2001 From: Tristan Matthias Date: Sat, 1 Aug 2020 17:47:50 -0500 Subject: [PATCH 5/5] feat(api): improvements for roles --- packages/api/src/App.module.ts | 8 +- .../api/src/Character/Character.resolver.ts | 12 +- packages/api/src/Concept/Concept.resolver.ts | 21 ++-- packages/api/src/Database/index.ts | 8 +- .../src/Database/seeders/Seeders.service.ts | 44 ++----- packages/api/src/Path/Path.resolver.ts | 5 +- packages/api/src/Role/Role.entity.ts | 75 ------------ packages/api/src/Role/Role.guard.ts | 49 ++++++++ packages/api/src/Role/Role.module.ts | 14 --- packages/api/src/Role/Role.resolver.ts | 45 ------- packages/api/src/Role/Role.service.ts | 110 ------------------ packages/api/src/Role/RoleType.enum.ts | 17 ++- .../api/src/Role/decorator/Role.decorator.ts | 3 - packages/api/src/Role/guards/Role.guard.ts | 43 ------- .../api/src/Role/pipe/Role-validation.pipe.ts | 22 ---- packages/api/src/User/User.entity.ts | 38 ++---- packages/api/src/User/User.resolver.ts | 41 ++----- packages/api/src/User/User.resolver.ts~ | 90 -------------- packages/api/src/User/User.service.ts | 99 ++-------------- packages/api/src/User/index.ts | 4 +- packages/api/src/schema.gql | 46 +++----- 21 files changed, 138 insertions(+), 656 deletions(-) delete mode 100644 packages/api/src/Role/Role.entity.ts create mode 100644 packages/api/src/Role/Role.guard.ts delete mode 100644 packages/api/src/Role/Role.module.ts delete mode 100644 packages/api/src/Role/Role.resolver.ts delete mode 100644 packages/api/src/Role/Role.service.ts delete mode 100644 packages/api/src/Role/decorator/Role.decorator.ts delete mode 100644 packages/api/src/Role/guards/Role.guard.ts delete mode 100644 packages/api/src/Role/pipe/Role-validation.pipe.ts delete mode 100644 packages/api/src/User/User.resolver.ts~ diff --git a/packages/api/src/App.module.ts b/packages/api/src/App.module.ts index 2f449040..a97482ea 100644 --- a/packages/api/src/App.module.ts +++ b/packages/api/src/App.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import path from 'path'; - -import { AssignmentFileModule } from './AssignmentFile'; import { AssignmentModule } from './Assignment'; +import { AssignmentFileModule } from './AssignmentFile'; import { AuthModule } from './Auth'; import { CharacterModule } from './Character'; import { CMSModule } from './CMS'; @@ -16,10 +15,10 @@ import { PathModule } from './Path'; import { PathUserModule } from './PathUser'; import { QuestionModule } from './Question'; import { StorySectionModule } from './StorySection'; -import { UserConceptModule } from './UserConcepts'; import { UserModule } from './User'; +import { UserConceptModule } from './UserConcepts'; import { UserModuleModule } from './UserModule'; -import { RoleModule } from './Role/Role.module'; + /** * Export these dependencies so they can be used in testing @@ -42,7 +41,6 @@ export const appImports = [ UserConceptModule, UserModule, UserModuleModule, - RoleModule, DatabaseModule, diff --git a/packages/api/src/Character/Character.resolver.ts b/packages/api/src/Character/Character.resolver.ts index 1b9869c9..ed0a2b67 100644 --- a/packages/api/src/Character/Character.resolver.ts +++ b/packages/api/src/Character/Character.resolver.ts @@ -1,10 +1,10 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; - import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; - -import { CharacterService } from './Character.service'; +import { Roles } from '../Role/Role.guard'; import { Character, CreateCharacterInput, UpdateCharacterInput } from './Character.entity'; +import { CharacterService } from './Character.service'; + @Resolver('Character') export class CharacterResolver { @@ -16,19 +16,19 @@ export class CharacterResolver { return this.characterService.findAll(); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Character) createCharacter(@Args('character') character: CreateCharacterInput) { return this.characterService.create(character); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Character) async updateCharacter(@Args('character') character: UpdateCharacterInput) { return this.characterService.update(character); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Boolean) async deleteCharacter(@Args('id') id: string) { return this.characterService.delete(id); diff --git a/packages/api/src/Concept/Concept.resolver.ts b/packages/api/src/Concept/Concept.resolver.ts index f62da35c..c514f05c 100644 --- a/packages/api/src/Concept/Concept.resolver.ts +++ b/packages/api/src/Concept/Concept.resolver.ts @@ -1,15 +1,16 @@ -import { Resolver, Query, Args, Mutation, ResolveField, Parent } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; - -import { ConceptService } from './Concept.service'; -import { Concept, CreateConceptInput, UpdateConceptInput } from './Concept.entity'; +import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; +import { CMS } from '../CMS/CMS'; +import { LessonModule } from '../Lesson'; +import { Roles } from '../Role/Role.guard'; import { CurrentUser } from '../User/CurrentUser.decorator'; import { User } from '../User/User.entity'; -import { UserConceptService } from '../UserConcepts/UserConcept.service'; import { UserConcept } from '../UserConcepts/UserConcept.entity'; -import { LessonModule } from '../Lesson'; -import { CMS } from '../CMS/CMS'; +import { UserConceptService } from '../UserConcepts/UserConcept.service'; +import { Concept, CreateConceptInput, UpdateConceptInput } from './Concept.entity'; +import { ConceptService } from './Concept.service'; + @Resolver(Concept) export class ConceptResolver { @@ -37,19 +38,19 @@ export class ConceptResolver { return this.userConceptService.findByUser(user.id); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Concept) createConcept(@Args('concept') concept: CreateConceptInput) { return this.conceptService.create(concept); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Concept) updateConcept(@Args('concept') concept: UpdateConceptInput) { return this.conceptService.update(concept); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Boolean) deleteConcept(@Args('conceptId') conceptId: string) { return this.conceptService.delete(conceptId); diff --git a/packages/api/src/Database/index.ts b/packages/api/src/Database/index.ts index 055c65f9..03e1e668 100644 --- a/packages/api/src/Database/index.ts +++ b/packages/api/src/Database/index.ts @@ -28,8 +28,6 @@ import { DatabaseService } from './Database.service'; import { SeederService } from './seeders/Seeders.service'; import { TypeORMModule } from './TypeORM.module'; import { CMS } from '../CMS/CMS'; -import { Role } from '../Role/Role.entity'; -import { RoleService } from '../Role/Role.service'; /** * Main Database Module, used in App.module and testing @@ -48,8 +46,7 @@ import { RoleService } from '../Role/Role.service'; UserConcept, Friend, Character, - UserModule, - Role + UserModule ]) ], providers: [ @@ -67,8 +64,7 @@ import { RoleService } from '../Role/Role.service'; FriendService, CharacterService, PathUserService, - UserModuleService, - RoleService + UserModuleService ], exports: [DatabaseService, SeederService] }) diff --git a/packages/api/src/Database/seeders/Seeders.service.ts b/packages/api/src/Database/seeders/Seeders.service.ts index edd70671..741589f8 100644 --- a/packages/api/src/Database/seeders/Seeders.service.ts +++ b/packages/api/src/Database/seeders/Seeders.service.ts @@ -1,12 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; import Listr from 'listr'; import { Connection, Repository } from 'typeorm'; - import { Assignment } from '../../Assignment/Assignment.entity'; import { AssignmentService } from '../../Assignment/Assignment.service'; import { AssignmentFileService } from '../../AssignmentFile/AssignmentFile.service'; import { Character } from '../../Character/Character.entity'; import { CharacterService } from '../../Character/Character.service'; +import { CMS } from '../../CMS/CMS'; import { ConceptService } from '../../Concept/Concept.service'; import { FriendStatus } from '../../Friend/Friend.entity'; import { FriendService } from '../../Friend/Friend.service'; @@ -19,9 +19,8 @@ import { UserService } from '../../User/User.service'; import { UserPreferencesService } from '../../UserPreferences/UserPreferences.service'; import { DatabaseService } from '../Database.service'; import * as random from './random'; -import { CMS } from '../../CMS/CMS'; -import { RoleService } from '../../Role/Role.service'; -import { RoleType } from '../../Role/RoleType.enum'; +// import { RoleType } from '../../Role/RoleType.enum'; + interface CTX { users: UserWithPassword[]; @@ -48,8 +47,7 @@ export class SeederService { public assignmentFileService: AssignmentFileService, public conceptService: ConceptService, public friendService: FriendService, - public characterService: CharacterService, - public roleService: RoleService + public characterService: CharacterService ) {} db = new DatabaseService(this.connection); @@ -71,9 +69,12 @@ export class SeederService { Array(num) .fill(undefined) .map(async (_, i) => { - const user = await this.userService.create( - random.userInput({ email: `user${i}@test.com` }) - ); + const input = random.userInput({ email: `user${i}@test.com` }); + if (i === 0) { + input.email = 'admin@test.com'; + // (input as UserWithPassword).role = RoleType.admin; + } + const user = await this.userService.create(input); if (i % 2 === 0) { await this.userPreferencesService.update( @@ -168,25 +169,6 @@ export class SeederService { } } - /** - * Add Roles - */ - - async seedRoles() { - await this.roleService.create({ - name: RoleType.ADMIN, - description: 'Site Admin' - }); - await this.roleService.create({ - name: RoleType.MENTOR, - description: 'Site Mentor' - }); - await this.roleService.create({ - name: RoleType.STUDENT, - description: 'Site STUDENT' - }); - } - /** * Seeds all entities in the database */ @@ -198,12 +180,6 @@ export class SeederService { this.db.DANGEROUSLY_RESET_DATABASE(); } }, - { - title: 'Create Roles', - task: async () => { - await this.seedRoles(); - } - }, { title: 'Create users', task: async (ctx: CTX) => { diff --git a/packages/api/src/Path/Path.resolver.ts b/packages/api/src/Path/Path.resolver.ts index 958263f5..ae778f11 100644 --- a/packages/api/src/Path/Path.resolver.ts +++ b/packages/api/src/Path/Path.resolver.ts @@ -9,6 +9,7 @@ import { CurrentUser } from '../User/CurrentUser.decorator'; import { User } from '../User/User.entity'; import { Path, PathInput, UpdatePathInput } from './Path.entity'; import { PathService } from './Path.service'; +import { Roles } from '../Role/Role.guard'; @Resolver(() => Path) export class PathResolver { @@ -43,7 +44,7 @@ export class PathResolver { return this.pathService.findById(id); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Path) createPath(@Args('path') path: PathInput) { return this.pathService.create(path); @@ -67,7 +68,7 @@ export class PathResolver { return Boolean(await this.pathService.addUserToPath(user.id, paths)); } - @UseGuards(GQLAuthGuard) + @Roles('admin') @Mutation(() => Path) async updatePath( @Args('path') path: UpdatePathInput diff --git a/packages/api/src/Role/Role.entity.ts b/packages/api/src/Role/Role.entity.ts deleted file mode 100644 index f5a4c771..00000000 --- a/packages/api/src/Role/Role.entity.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Entity, - Column, - ManyToMany, - JoinColumn, - Unique, - PrimaryGeneratedColumn -} from 'typeorm'; - -import { - ObjectType, - Field, - ID, - InputType, - registerEnumType -} from '@nestjs/graphql'; - -import { CMBaseEntity } from '../lib/Base.entity'; -import { RoleType } from './RoleType.enum'; -import { UserWithPassword } from '../User/User.entity'; - -registerEnumType(RoleType, { - name: 'RoleType' -}); - -@ObjectType() -export class Roles { - @Field(() => ID) - id: string; - - @Field() - name: RoleType; - - @Field() - description: string; -} - -@Entity('roles') -@Unique(['name']) -export class Role extends CMBaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ - type: 'simple-enum', - enum: RoleType, - default: RoleType.STUDENT - }) - name: RoleType; - - @Column({ type: 'varchar', length: 100 }) - description: string; - - @ManyToMany(() => UserWithPassword, user => user.roles) - @JoinColumn() - users!: UserWithPassword[]; -} - -@InputType() -export class RoleInput { - @Field() - name: RoleType; - - @Field() - description: string; -} - -@InputType() -export class RoleUpdateInput { - @Field() - id: string; - - @Field() - description: string; -} diff --git a/packages/api/src/Role/Role.guard.ts b/packages/api/src/Role/Role.guard.ts new file mode 100644 index 00000000..1e9587ac --- /dev/null +++ b/packages/api/src/Role/Role.guard.ts @@ -0,0 +1,49 @@ +import { applyDecorators, CanActivate, ExecutionContext, Injectable, SetMetadata, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; +import { UserWithPassword } from '../User/User.entity'; +import { RolePermissionLevels, RoleType } from './RoleType.enum'; + +@Injectable() +export class RoleGuard implements CanActivate { + constructor(private readonly _reflector: Reflector) { } + + /** + * Looks up user's role, and if any is supplied to `@Role(...)` then compare it + */ + canActivate(context: ExecutionContext): boolean { + // Get the roles passed in from the decorator (array of RoleType) + const roles = this._reflector.get<(keyof typeof RoleType)[]>( + 'roles', context.getHandler()); + + // If no roles supplied, allow anyone + if (!roles.length) return true; + // Map the roles to their numbers, and find the HIGHEST number + const permLevels = roles.map(r => RolePermissionLevels[r]); + const highestAllowed = Math.max(...permLevels); + + // Get the users permmission level + const ctx = GqlExecutionContext.create(context); + const { user } = ctx.getContext().req; + const userLevel = RolePermissionLevels[(user as UserWithPassword).role]; + + // If user's level is too high, block them + if (userLevel > highestAllowed) { + throw new UnauthorizedException( + "You haven't permissions to access this resource" + ); + } + + return true; + } +} + +/** + * Protect a query or mutation by specific roles. Also implemments GQLAuthGuard + * @param roles List of RoleType's to auth a query/mutation/etc + */ +export const Roles = (...roles: (keyof typeof RoleType)[]) => applyDecorators( + SetMetadata('roles', roles), + UseGuards(GQLAuthGuard, RoleGuard) +); diff --git a/packages/api/src/Role/Role.module.ts b/packages/api/src/Role/Role.module.ts deleted file mode 100644 index 4e826ecd..00000000 --- a/packages/api/src/Role/Role.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -// import { PassportModule } from '@nestjs/passport'; -import { RoleService } from './Role.service'; -import { RoleResolver } from './Role.resolver'; -import { Role } from './Role.entity'; -import { AuthModule } from '../Auth'; - -@Module({ - imports: [TypeOrmModule.forFeature([Role]), AuthModule], - providers: [RoleService, RoleResolver], - exports: [RoleService] -}) -export class RoleModule {} diff --git a/packages/api/src/Role/Role.resolver.ts b/packages/api/src/Role/Role.resolver.ts deleted file mode 100644 index 6ec731de..00000000 --- a/packages/api/src/Role/Role.resolver.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { UsePipes, ValidationPipe, UseGuards } from '@nestjs/common'; -import { Query, Args, Mutation, Resolver } from '@nestjs/graphql'; -import { RoleService } from './Role.service'; -import { Roles, RoleInput, RoleUpdateInput } from './Role.entity'; -import { RolArray } from './decorator/Role.decorator'; - -import { RoleValidationPipe } from './pipe/Role-validation.pipe'; -import { RoleType } from './RoleType.enum'; -import { RoleGuard } from './guards/Role.guard'; -import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; - -@RolArray(RoleType.ADMIN) -@UseGuards(GQLAuthGuard, RoleGuard) -@Resolver(() => Roles) -export class RoleResolver { - constructor(private readonly _roleService: RoleService) {} - - @Query(() => Roles) - searchRoleById(@Args('roleId') roleId: string) { - return this._roleService.findById(roleId); - } - - @Query(() => [Roles]) - findRoles() { - return this._roleService.findAll(); - } - - @Mutation(() => Roles) - @UsePipes(ValidationPipe) - @UsePipes(RoleValidationPipe) - createRole(@Args('role') role: RoleInput) { - return this._roleService.create(role); - } - - @Mutation(() => Roles) - @UsePipes(ValidationPipe) - updateRole(@Args('role') role: RoleUpdateInput) { - return this._roleService.update(role); - } - - @Mutation(() => Boolean) - deleteRole(@Args('roleId') roleId: string) { - return this._roleService.delete(roleId); - } -} diff --git a/packages/api/src/Role/Role.service.ts b/packages/api/src/Role/Role.service.ts deleted file mode 100644 index aa624f8b..00000000 --- a/packages/api/src/Role/Role.service.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - Injectable, - BadRequestException, - NotFoundException, - InternalServerErrorException -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Role, RoleInput, RoleUpdateInput } from './Role.entity'; - -@Injectable() -export class RoleService { - constructor( - @InjectRepository(Role) - private readonly _roleRepository: Repository - ) {} - - async findById(rolId: string): Promise { - const success = false; - let message = 'rolId must be sent'; - if (!rolId) { - throw new BadRequestException({ success, message }); - } - let role: Role | undefined; - try { - role = await this._roleRepository.findOne(rolId); - } catch (error) { - message = `we had a problem making the request to the database: ${error.error}`; - throw new InternalServerErrorException({ success, message }); - } - - if (!role) { - message = 'rolId not found'; - throw new NotFoundException({ success, message }); - } - - return role; - } - - async findAll(): Promise { - let roles: Role[]; - try { - roles = await this._roleRepository.find(); - } catch (error) { - const success = false; - const message = 'we had a problem making the request to the database'; - throw new InternalServerErrorException({ success, message }); - } - return roles; - } - - async create(role: RoleInput): Promise { - let savedRole; - let foundRole: Role | undefined; - - try { - foundRole = await this._roleRepository.findOne({ - where: { name: role.name } - }); - } catch (error) { - throw new InternalServerErrorException(error.message); - } - - if (foundRole) { - const message = 'Role name already exists'; - throw new BadRequestException({ message }); - } else { - try { - savedRole = await this._roleRepository.create(role).save(); - } catch (error) { - throw new InternalServerErrorException(error.message); - } - } - - return savedRole; - } - - async update(role: RoleUpdateInput): Promise { - let foundRole = await this._roleRepository.findOne(role.id); - - if (!foundRole) { - throw new NotFoundException('roles search failed'); - } - - try { - foundRole.description = role.description; - foundRole = await this._roleRepository.save(foundRole); - } catch (error) { - throw new InternalServerErrorException(); - } - - return foundRole; - } - - async delete(roleId: string): Promise { - const roleExists = await this._roleRepository.findOne(roleId); - if (!roleExists) { - throw new NotFoundException('rolesId not found'); - } - - try { - await this._roleRepository.delete(roleId); - } catch (error) { - throw new InternalServerErrorException( - 'we had a problem making the request to the database' - ); - } - return true; - } -} diff --git a/packages/api/src/Role/RoleType.enum.ts b/packages/api/src/Role/RoleType.enum.ts index ac217c35..ce2c5a60 100644 --- a/packages/api/src/Role/RoleType.enum.ts +++ b/packages/api/src/Role/RoleType.enum.ts @@ -1,5 +1,16 @@ +import { registerEnumType } from '@nestjs/graphql'; + export enum RoleType { - ADMIN = 'ADMIN', - STUDENT = 'STUDENT', - MENTOR = 'MENTOR', + admin = 'admin', + user = 'user', } + +export const RolePermissionLevels: { [role in RoleType]: number } = { + admin: 1, + // Gives us room to add other roles if we need + user: 10 +}; + +registerEnumType(RoleType, { + name: 'RoleType' +}); diff --git a/packages/api/src/Role/decorator/Role.decorator.ts b/packages/api/src/Role/decorator/Role.decorator.ts deleted file mode 100644 index 9be8580d..00000000 --- a/packages/api/src/Role/decorator/Role.decorator.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; - -export const RolArray = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/packages/api/src/Role/guards/Role.guard.ts b/packages/api/src/Role/guards/Role.guard.ts deleted file mode 100644 index 50841459..00000000 --- a/packages/api/src/Role/guards/Role.guard.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - UnauthorizedException -} from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { GqlExecutionContext } from '@nestjs/graphql'; -import { Role } from '../Role.entity'; - -@Injectable() -export class RoleGuard implements CanActivate { - constructor(private readonly _reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean { - const roles: string[] = this._reflector.get( - 'roles', - context.getHandler() - ); - - if (!roles) { - return true; - } - - const ctx = GqlExecutionContext.create(context); - - const { req } = ctx.getContext(); - - const { user } = req; - - const hasRole = () => - user.roles.some((role: Role) => roles.includes(role.name)); - - const authorized = hasRole(); - if (!authorized) { - throw new UnauthorizedException( - "You haven't permissions to access this resource" - ); - } - - return user && user.roles && authorized; - } -} diff --git a/packages/api/src/Role/pipe/Role-validation.pipe.ts b/packages/api/src/Role/pipe/Role-validation.pipe.ts deleted file mode 100644 index 371334be..00000000 --- a/packages/api/src/Role/pipe/Role-validation.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; - -import { plainToClass } from 'class-transformer'; -import { RoleType } from '../RoleType.enum'; -import { RoleInput } from '../Role.entity'; - -@Injectable() -export class RoleValidationPipe implements PipeTransform { - async transform(value: RoleInput): Promise { - const newRol: RoleInput = plainToClass(RoleInput, value); - - const roles = Object.values(RoleType); - - if (!roles.includes(newRol.name)) { - throw new BadRequestException( - `Role validation fail, name should be in [${roles}]` - ); - } - - return value; - } -} diff --git a/packages/api/src/User/User.entity.ts b/packages/api/src/User/User.entity.ts index 4e9fc012..bd1e820d 100644 --- a/packages/api/src/User/User.entity.ts +++ b/packages/api/src/User/User.entity.ts @@ -6,17 +6,14 @@ import { OneToMany, OneToOne, PrimaryGeneratedColumn, - Unique, - ManyToMany, - JoinTable + Unique } from 'typeorm'; - import { CMBaseEntity } from '../lib/Base.entity'; import { PathUser } from '../PathUser/PathUser.entity'; -import { UserPreferences } from '../UserPreferences/UserPreferences.entity'; -import { UserModule } from '../UserModule/UserModule.entity'; -import { Role, Roles } from '../Role/Role.entity'; import { RoleType } from '../Role/RoleType.enum'; +import { UserModule } from '../UserModule/UserModule.entity'; +import { UserPreferences } from '../UserPreferences/UserPreferences.entity'; + @ObjectType() export class User { @@ -35,8 +32,8 @@ export class User { @Field() profileImage: string; - @Field(() => [Roles]) - roles: Roles[]; + @Field(() => RoleType) + role: RoleType; @Field(() => UserPreferences, { nullable: true }) userPreferences?: UserPreferences; @@ -75,9 +72,8 @@ export class UserWithPassword extends CMBaseEntity { @OneToMany(() => UserModule, userModules => userModules.user) userModules: UserModule[]; - @ManyToMany(() => Role, role => role.users, { eager: true }) - @JoinTable({ name: 'user_roles' }) - roles: Role[]; + @Column({ type: 'simple-enum', enum: RoleType, default: RoleType.user }) + role: RoleType; @OneToOne(() => UserPreferences) userPreferences: UserPreferences; @@ -97,21 +93,3 @@ export class UserInput { @Field() password: string; } - -@InputType() -export class UserUpdate { - @Field({ nullable: true }) - firstName?: string; - - @Field({ nullable: true }) - lastName?: string; - - @Field({ nullable: true }) - email?: string; - - @Field({ nullable: true }) - password?: string; - - @Field(() => RoleType) - roles?: RoleType[]; -} diff --git a/packages/api/src/User/User.resolver.ts b/packages/api/src/User/User.resolver.ts index e1fc06f9..86ede274 100644 --- a/packages/api/src/User/User.resolver.ts +++ b/packages/api/src/User/User.resolver.ts @@ -1,23 +1,16 @@ import { UseGuards } from '@nestjs/common'; -import { - Args, - Mutation, - Query, - Resolver, - ResolveField, - Parent -} from '@nestjs/graphql'; - +import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; -import { User, UserInput } from './User.entity'; -import { UserService } from './User.service'; -import { CurrentUser } from './CurrentUser.decorator'; +import { Roles } from '../Role/Role.guard'; import { UserPreferences, UserPreferencesInput } from '../UserPreferences/UserPreferences.entity'; import { UserPreferencesService } from '../UserPreferences/UserPreferences.service'; -import { RoleType } from '../Role/RoleType.enum'; +import { CurrentUser } from './CurrentUser.decorator'; +import { User, UserInput } from './User.entity'; +import { UserService } from './User.service'; + @Resolver(() => User) export class UserResolver { @@ -26,7 +19,7 @@ export class UserResolver { private readonly userPreferencesService: UserPreferencesService ) {} - @UseGuards(GQLAuthGuard) + @Roles('admin') @Query(() => [User]) users() { return this.userService.findAll(); @@ -67,24 +60,4 @@ export class UserResolver { async userPreferences(@Parent() user: User) { return this.userPreferencesService.findByUser(user.id); } - - // ---------------------------- - // -----------Roles ----------- - // ---------------------------- - - @Mutation(() => User) - addRoleToUser( - @Args('userId') userId: string, - @Args('roleName') roleName: RoleType - ) { - return this.userService.addRoleToUser(userId, roleName); - } - - @Mutation(() => User) - removeRoleToUser( - @Args('userId') userId: string, - @Args('roleName') roleName: RoleType - ) { - return this.userService.removeRoleToUser(userId, roleName); - } } diff --git a/packages/api/src/User/User.resolver.ts~ b/packages/api/src/User/User.resolver.ts~ deleted file mode 100644 index 00578bc0..00000000 --- a/packages/api/src/User/User.resolver.ts~ +++ /dev/null @@ -1,90 +0,0 @@ -import { UseGuards } from '@nestjs/common'; -import { - Args, - Mutation, - Query, - Resolver, - ResolveField, - Parent -} from '@nestjs/graphql'; - -import { GQLAuthGuard } from '../Auth/GQLAuth.guard'; -import { User, UserInput } from './User.entity'; -import { UserService } from './User.service'; -import { CurrentUser } from './CurrentUser.decorator'; -import { - UserPreferences, - UserPreferencesInput -} from '../UserPreferences/UserPreferences.entity'; -import { UserPreferencesService } from '../UserPreferences/UserPreferences.service'; -import { RoleType } from '../Role/RoleType.enum'; - -@Resolver(() => User) -export class UserResolver { - constructor( - private readonly userService: UserService, - private readonly userPreferencesService: UserPreferencesService - ) {} - - @UseGuards(GQLAuthGuard) - @Query(() => [User]) - users() { - return this.userService.findAll(); - } - - @UseGuards(GQLAuthGuard) - @Query(() => [User]) - searchUsers(@Args('query') query: string) { - return this.userService.search(query); - } - - @UseGuards(GQLAuthGuard) - @Query(() => User) - me(@CurrentUser() user: User) { - return user; - } - - @Mutation(() => User) - createUser(@Args({ name: 'user', type: () => UserInput }) user: UserInput) { - return this.userService.create(user); - } - - @UseGuards(GQLAuthGuard) - @Mutation(() => UserPreferences) - updatePreferences( - @CurrentUser() user: User, - @Args('preferences', { type: () => UserPreferencesInput }) - preferences: UserPreferencesInput - ) { - return this.userPreferencesService.update(user.id, preferences); - } - - // --------------------------------------------------------------------------- - // -------------------------------------------------------------------- Fields - // --------------------------------------------------------------------------- - - @ResolveField(() => UserPreferences) - async userPreferences(@Parent() user: User) { - return this.userPreferencesService.findByUser(user.id); - } - - // ---------------------------- - // -----------Roles ----------- - // ---------------------------- - - @Mutation(() => User) - addRoleToUser( - @Args('userId') userId: string, - @Args('roleName') roleName: RoleType - ) { - return this.userService.addRoleToUser(userId, roleName); - } - - @Mutation(() => User) - removeRoleToUser( - @Args('userId') userId: string, - @Args('roleName') roleName: RoleType - ) { - return this.userService.removeRoleToUser(userId, roleName); - } -} diff --git a/packages/api/src/User/User.service.ts b/packages/api/src/User/User.service.ts index f8adc33f..02164ce4 100644 --- a/packages/api/src/User/User.service.ts +++ b/packages/api/src/User/User.service.ts @@ -1,26 +1,17 @@ -import { - Injectable, - NotFoundException, - InternalServerErrorException -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import bcrypt from 'bcrypt'; import md5 from 'md5'; import { Repository } from 'typeorm'; +import { UserWithPassword } from './User.entity'; -import { UserInput, UserWithPassword } from './User.entity'; -import { Role } from '../Role/Role.entity'; -import { RoleType } from '../Role/RoleType.enum'; @Injectable() export class UserService { constructor( @InjectRepository(UserWithPassword) - private readonly userRepository: Repository, - - @InjectRepository(Role) - private readonly roleRepository: Repository - ) {} + private readonly userRepository: Repository + ) { } async findAll(): Promise { return this.userRepository.find(); @@ -36,89 +27,19 @@ export class UserService { }); } - async create(input: UserInput): Promise { + async create(input: Partial): Promise { const user = this.userRepository.create(input); - - const defaultRole = await this.roleRepository.findOne({ - where: { name: RoleType.STUDENT } - }); - - if (defaultRole !== undefined) { - user.roles = [defaultRole]; - } - user.password = await bcrypt.hash(input.password, 10); // When in production add // ?r=pg&d=https:%3A%2F%2Fapi.codementoring.co%2Fstatic%2Fdefault-profile.svg; // to end of profileImage URL. - user.profileImage = `https://www.gravatar.com/avatar/${md5(input.email)}`; + user.profileImage = `https://www.gravatar.com/avatar/${md5(input.email!)}`; return this.userRepository.save(user); } - async addRoleToUser(userId: string, roleName: RoleType) { - let user: UserWithPassword | undefined; - try { - user = await this.userRepository.findOne({ - where: { id: userId } - }); - } catch (error) { - const message = `we had a problem making the request to the database: ${error.error}`; - throw new InternalServerErrorException(message); - } - - if (!user) { - throw new NotFoundException('User id not found'); - } - - let newRole: Role | undefined; - try { - newRole = await this.roleRepository.findOne({ - where: { name: roleName } - }); - } catch (error) { - const message = `we had a problem making the request to the database: ${error.error}`; - throw new InternalServerErrorException(message); - } - - if (newRole) { - if (!user.roles.find(rl => rl.name === newRole?.name)) { - user.roles.push(newRole); - user = await this.userRepository.save(user); - } - } - - return user; - } - - async removeRoleToUser(userId: string, roleName: RoleType) { - let user; - try { - user = await this.userRepository.findOne({ - where: { id: userId } - }); - } catch (error) { - const message = `we had a problem making the request to the database: ${error.error}`; - throw new InternalServerErrorException(message); - } - - if (!user) { - throw new NotFoundException('User id not found'); - } - - let role: Role | undefined; - try { - role = await this.roleRepository.findOne({ - where: { name: roleName } - }); - } catch (error) { - const message = `we had a problem making the request to the database: ${error.error}`; - throw new InternalServerErrorException(message); - } - - if (role) { - user.roles = user.roles.filter(rl => rl.name !== role?.name); - } - - return this.userRepository.save(user); + async update(id: string, input: Partial): Promise { + const _input = { ...input }; + if (_input.password) _input.password = await bcrypt.hash(input.password, 10); + return this.userRepository.save({ id, ..._input }); } } diff --git a/packages/api/src/User/index.ts b/packages/api/src/User/index.ts index 1249b8ff..20fadef8 100644 --- a/packages/api/src/User/index.ts +++ b/packages/api/src/User/index.ts @@ -9,7 +9,6 @@ import { UserPreferencesService } from '../UserPreferences/UserPreferences.servi import { PathService } from '../Path/Path.service'; import { PathUser } from '../PathUser/PathUser.entity'; import { Path } from '../Path/Path.entity'; -import { Role } from '../Role/Role.entity'; @Module({ imports: [ @@ -17,8 +16,7 @@ import { Role } from '../Role/Role.entity'; UserWithPassword, UserPreferences, Path, - PathUser, - Role + PathUser ]) ], providers: [ diff --git a/packages/api/src/schema.gql b/packages/api/src/schema.gql index a9ec4b06..049886f5 100644 --- a/packages/api/src/schema.gql +++ b/packages/api/src/schema.gql @@ -105,14 +105,6 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime -type UserPreferences { - id: String! - userId: String! - practiceGoal: Float! - why: String! - codingAbility: Float! -} - type UserModule { id: String! userId: String! @@ -121,10 +113,12 @@ type UserModule { completedAt: DateTime } -type Roles { - id: ID! - name: String! - description: String! +type UserPreferences { + id: String! + userId: String! + practiceGoal: Float! + why: String! + codingAbility: Float! } type User { @@ -133,11 +127,16 @@ type User { lastName: String! email: String! profileImage: String! - roles: [Roles!]! + role: RoleType! userPreferences: UserPreferences createdAt: DateTime! } +enum RoleType { + admin + user +} + type AssignmentFile { id: String! name: String! @@ -200,10 +199,10 @@ type FriendOutput { } type Query { - assignmentFiles(assignmentId: String!): [AssignmentFile!]! - userAssignmentFiles(authorId: String!): [AssignmentFile!]! assignments: [Assignment!]! moduleAssignments(moduleId: String!): [Assignment!]! + assignmentFiles(assignmentId: String!): [AssignmentFile!]! + userAssignmentFiles(authorId: String!): [AssignmentFile!]! users: [User!]! searchUsers(query: String!): [User!]! me: User! @@ -225,8 +224,6 @@ type Query { path(id: String!): Path! checkAnswer(answer: [String!]!, questionId: String!): [Boolean!]! lessonStorySections(lessonId: String!): [StorySection!]! - searchRoleById(roleId: String!): Roles! - findRoles: [Roles!]! } type Mutation { @@ -235,8 +232,6 @@ type Mutation { deleteAssignmentFile(assignmentFileId: String!): Boolean! createUser(user: UserInput!): User! updatePreferences(preferences: UserPreferencesInput!): UserPreferences! - addRoleToUser(roleName: String!, userId: String!): User! - removeRoleToUser(roleName: String!, userId: String!): User! login(password: String!, email: String!): LoginOutput! createCharacter(character: CreateCharacterInput!): Character! updateCharacter(character: UpdateCharacterInput!): Character! @@ -255,9 +250,6 @@ type Mutation { joinPath(pathId: String!): Boolean! joinPaths(paths: [String!]!): Boolean! updatePath(path: UpdatePathInput!): Path! - createRole(role: RoleInput!): Roles! - updateRole(role: RoleUpdateInput!): Roles! - deleteRole(roleId: String!): Boolean! } input CreateAssignmentFileInput { @@ -328,13 +320,3 @@ input UpdatePathInput { description: String characterId: String } - -input RoleInput { - name: String! - description: String! -} - -input RoleUpdateInput { - id: String! - description: String! -}