diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 160ec47..3e7e8a8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,6 @@ datasource db { model User { id String @id username String - role Role @default(STUDENT) createdAt DateTime @default(now()) Class Class[] @@ -104,9 +103,4 @@ model RoomSurveyAnswerUser { userId String Answer RoomSurveyAnswer @relation(fields: [answerId], references: [id]) answerId Int -} - -enum Role { - STUDENT - ADMIN -} +} \ No newline at end of file diff --git a/src/modules/auth/decorators/roles.decorator.ts b/src/modules/auth/decorators/roles.decorator.ts index 0aeb9b1..3ca4b06 100644 --- a/src/modules/auth/decorators/roles.decorator.ts +++ b/src/modules/auth/decorators/roles.decorator.ts @@ -1,4 +1,3 @@ import { SetMetadata } from "@nestjs/common"; -import { $Enums } from "@prisma/client"; -export const Roles = (roles: $Enums.Role[]) => SetMetadata("roles", roles); +export const Roles = (roles: string[]) => SetMetadata("roles", roles); diff --git a/src/modules/auth/guards/role.guard.ts b/src/modules/auth/guards/role.guard.ts index 51d902b..3dad826 100644 --- a/src/modules/auth/guards/role.guard.ts +++ b/src/modules/auth/guards/role.guard.ts @@ -7,12 +7,13 @@ import { } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Request } from "express"; +import { decode, JwtPayload, UserJwtPayload } from "jsonwebtoken"; @Injectable() export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} - canActivate(context: ExecutionContext): boolean { + async canActivate(context: ExecutionContext): Promise { const RolesHandler = this.reflector.get( "roles", context.getHandler(), @@ -26,14 +27,24 @@ export class RolesGuard implements CanActivate { const request = context.switchToHttp().getRequest() as Request; const user = request.user; - + if (!user) throw new ForbiddenException("User not authenticated"); + const decodedToken = await this.decodeToken( + this.extractTokenFromHeader(request.headers.authorization), + ); + + // Check if the user has the right role + // On the handler level const hasRoleHandler = - RolesHandler?.some((role) => user.role?.includes(role)) ?? - false, + RolesHandler?.some((role) => + decodedToken.realm_access.roles?.includes(role), + ) ?? false, + // On the class level hasRoleClass = - RolesClass?.some((role) => user.role?.includes(role)) ?? false; + RolesClass?.some((role) => + decodedToken.realm_access.roles?.includes(role), + ) ?? false; if (hasRoleHandler) return true; else if (hasRoleClass) return true; @@ -42,4 +53,20 @@ export class RolesGuard implements CanActivate { `User doesn't have the right role, expected: ${RolesHandler ?? RolesClass}`, ); } + + async decodeToken(token: string) { + try { + return decode(token) as UserJwtPayload; + } catch (error) { + throw new UnauthorizedException("Invalid token"); + } + } + + extractTokenFromHeader(header: string) { + const token = header.split(" ")[1]; + + if (!token) throw new UnauthorizedException("Token not found"); + + return token; + } } diff --git a/src/modules/class/class.controller.ts b/src/modules/class/class.controller.ts index bb28f05..4482aa0 100644 --- a/src/modules/class/class.controller.ts +++ b/src/modules/class/class.controller.ts @@ -39,7 +39,7 @@ import { ClassRoomEntity } from "./entities/room.entity"; @UseGuards(RolesGuard) @UseGuards(JwtAuthGuard) @ApiBearerAuth() -@Roles(["ADMIN"]) +@Roles(["admin"]) @ApiUnauthorizedResponse(UnauthorizedResponse) export class ClassController { constructor(private readonly classService: ClassService) { } diff --git a/src/modules/user/ApiResponses/UserReponse.ts b/src/modules/user/ApiResponses/UserReponse.ts index 3d75947..66ca59d 100644 --- a/src/modules/user/ApiResponses/UserReponse.ts +++ b/src/modules/user/ApiResponses/UserReponse.ts @@ -2,18 +2,18 @@ import { ApiResponseNoStatusOptions } from "@nestjs/swagger"; import { UserEntity } from "../entities/user.entity"; export const UserResponse = { - type: UserEntity, - description: "The user has been successfully found.", - examples: { - example: { - summary: "A user example", - value: { - id: "1", - role: "ADMIN", - username: "admin", - } as UserEntity, - }, - }, + type: UserEntity, + description: "The user has been successfully found.", + examples: { + example: { + summary: "A user example", + value: { + id: "1", + role: "ADMIN", + username: "admin", + } as UserEntity, + }, + }, } as ApiResponseNoStatusOptions; export const UsersResponse = { @@ -23,19 +23,19 @@ export const UsersResponse = { example: { summary: "A list of users", value: [ - { id: "1", role: "ADMIN", username: "admin" }, - { id: "2", role: "STUDENT", username: "student" }, + { id: "1", username: "admin" }, + { id: "2", username: "student" }, ] as UserEntity[], }, }, } as ApiResponseNoStatusOptions; export const UserCountResponse = { - description: "The users count", - examples: { - example: { - summary: "A count of users", - value: { count: 2 }, - }, - }, -} as ApiResponseNoStatusOptions; \ No newline at end of file + description: "The users count", + examples: { + example: { + summary: "A count of users", + value: { count: 2 }, + }, + }, +} as ApiResponseNoStatusOptions; diff --git a/src/modules/user/dto/create-user.dto.ts b/src/modules/user/dto/create-user.dto.ts index 7ada6c2..e6780e0 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/modules/user/dto/create-user.dto.ts @@ -1,5 +1,4 @@ import { ApiProperty } from "@nestjs/swagger"; -import { Role } from "@prisma/client"; import { IsString } from "class-validator"; export class CreateUserDTO { @@ -10,8 +9,4 @@ export class CreateUserDTO { @IsString() @ApiProperty() username: string; - - @IsString() - @ApiProperty() - role: Role; } diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index b3d2e76..0891de3 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -1,5 +1,4 @@ import { ApiProperty, ApiSchema } from "@nestjs/swagger"; -import { $Enums } from "@prisma/client"; import { Expose } from "class-transformer"; @ApiSchema({ name: "User" }) @@ -12,10 +11,6 @@ export class UserEntity { @ApiProperty() username: string; - @Expose() - @ApiProperty() - role: $Enums.Role; - constructor(partial: Partial) { Object.assign(this, partial); } diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 8e2fd50..74e8327 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -35,7 +35,7 @@ import { Request } from "express"; @UseGuards(RolesGuard) @UseGuards(JwtAuthGuard) @ApiBearerAuth() -@Roles(["ADMIN"]) +@Roles(["admin"]) @ApiUnauthorizedResponse(UnauthorizedResponse) export class UserController { constructor(private readonly userService: UserService) {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 370e832..a048a36 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -45,13 +45,10 @@ export class UserService { }); if (!user) { - const isFirstUser = (await this.prisma.user.count()) === 0; - user = await this.prisma.user.create({ data: { id, - username, - role: isFirstUser ? "ADMIN" : "STUDENT", + username }, }); } else if (user.username !== username) { @@ -80,7 +77,6 @@ export class UserService { where: { id }, data: { username: updateUserInput.username, - role: updateUserInput.role, }, }); } @@ -107,7 +103,6 @@ export class UserService { select: { id: true, username: true, - role: true } }); } diff --git a/src/types/jwt.d.ts b/src/types/jwt.d.ts new file mode 100644 index 0000000..75b0888 --- /dev/null +++ b/src/types/jwt.d.ts @@ -0,0 +1,14 @@ +import * as jwt from "jsonwebtoken"; + +declare module "jsonwebtoken" { + export interface UserJwtPayload extends jwt.JwtPayload { + name: string; + preferred_username: string; + email: string; + given_name: string; + family_name: string; + realm_access: { + roles: string[]; + }; + } +}