refactor: remove role property from User model and update related logic for role handling

This commit is contained in:
Rémi 2025-01-06 11:31:57 +01:00
parent e3365cdff7
commit 7aab149bb2
10 changed files with 73 additions and 54 deletions

View File

@ -10,7 +10,6 @@ datasource db {
model User { model User {
id String @id id String @id
username String username String
role Role @default(STUDENT)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
Class Class[] Class Class[]
@ -105,8 +104,3 @@ model RoomSurveyAnswerUser {
Answer RoomSurveyAnswer @relation(fields: [answerId], references: [id]) Answer RoomSurveyAnswer @relation(fields: [answerId], references: [id])
answerId Int answerId Int
} }
enum Role {
STUDENT
ADMIN
}

View File

@ -1,4 +1,3 @@
import { SetMetadata } from "@nestjs/common"; 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);

View File

@ -7,12 +7,13 @@ import {
} from "@nestjs/common"; } from "@nestjs/common";
import { Reflector } from "@nestjs/core"; import { Reflector } from "@nestjs/core";
import { Request } from "express"; import { Request } from "express";
import { decode, JwtPayload, UserJwtPayload } from "jsonwebtoken";
@Injectable() @Injectable()
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {} constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { async canActivate(context: ExecutionContext): Promise<boolean> {
const RolesHandler = this.reflector.get<string[]>( const RolesHandler = this.reflector.get<string[]>(
"roles", "roles",
context.getHandler(), context.getHandler(),
@ -29,11 +30,21 @@ export class RolesGuard implements CanActivate {
if (!user) throw new ForbiddenException("User not authenticated"); 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 = const hasRoleHandler =
RolesHandler?.some((role) => user.role?.includes(role)) ?? RolesHandler?.some((role) =>
false, decodedToken.realm_access.roles?.includes(role),
) ?? false,
// On the class level
hasRoleClass = hasRoleClass =
RolesClass?.some((role) => user.role?.includes(role)) ?? false; RolesClass?.some((role) =>
decodedToken.realm_access.roles?.includes(role),
) ?? false;
if (hasRoleHandler) return true; if (hasRoleHandler) return true;
else if (hasRoleClass) 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}`, `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;
}
} }

View File

@ -39,7 +39,7 @@ import { ClassRoomEntity } from "./entities/room.entity";
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@Roles(["ADMIN"]) @Roles(["admin"])
@ApiUnauthorizedResponse(UnauthorizedResponse) @ApiUnauthorizedResponse(UnauthorizedResponse)
export class ClassController { export class ClassController {
constructor(private readonly classService: ClassService) { } constructor(private readonly classService: ClassService) { }

View File

@ -2,18 +2,18 @@ import { ApiResponseNoStatusOptions } from "@nestjs/swagger";
import { UserEntity } from "../entities/user.entity"; import { UserEntity } from "../entities/user.entity";
export const UserResponse = { export const UserResponse = {
type: UserEntity, type: UserEntity,
description: "The user has been successfully found.", description: "The user has been successfully found.",
examples: { examples: {
example: { example: {
summary: "A user example", summary: "A user example",
value: { value: {
id: "1", id: "1",
role: "ADMIN", role: "ADMIN",
username: "admin", username: "admin",
} as UserEntity, } as UserEntity,
}, },
}, },
} as ApiResponseNoStatusOptions; } as ApiResponseNoStatusOptions;
export const UsersResponse = { export const UsersResponse = {
@ -23,19 +23,19 @@ export const UsersResponse = {
example: { example: {
summary: "A list of users", summary: "A list of users",
value: [ value: [
{ id: "1", role: "ADMIN", username: "admin" }, { id: "1", username: "admin" },
{ id: "2", role: "STUDENT", username: "student" }, { id: "2", username: "student" },
] as UserEntity[], ] as UserEntity[],
}, },
}, },
} as ApiResponseNoStatusOptions; } as ApiResponseNoStatusOptions;
export const UserCountResponse = { export const UserCountResponse = {
description: "The users count", description: "The users count",
examples: { examples: {
example: { example: {
summary: "A count of users", summary: "A count of users",
value: { count: 2 }, value: { count: 2 },
}, },
}, },
} as ApiResponseNoStatusOptions; } as ApiResponseNoStatusOptions;

View File

@ -1,5 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { Role } from "@prisma/client";
import { IsString } from "class-validator"; import { IsString } from "class-validator";
export class CreateUserDTO { export class CreateUserDTO {
@ -10,8 +9,4 @@ export class CreateUserDTO {
@IsString() @IsString()
@ApiProperty() @ApiProperty()
username: string; username: string;
@IsString()
@ApiProperty()
role: Role;
} }

View File

@ -1,5 +1,4 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger"; import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { $Enums } from "@prisma/client";
import { Expose } from "class-transformer"; import { Expose } from "class-transformer";
@ApiSchema({ name: "User" }) @ApiSchema({ name: "User" })
@ -12,10 +11,6 @@ export class UserEntity {
@ApiProperty() @ApiProperty()
username: string; username: string;
@Expose()
@ApiProperty()
role: $Enums.Role;
constructor(partial: Partial<UserEntity>) { constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial); Object.assign(this, partial);
} }

View File

@ -35,7 +35,7 @@ import { Request } from "express";
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@Roles(["ADMIN"]) @Roles(["admin"])
@ApiUnauthorizedResponse(UnauthorizedResponse) @ApiUnauthorizedResponse(UnauthorizedResponse)
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}

View File

@ -45,13 +45,10 @@ export class UserService {
}); });
if (!user) { if (!user) {
const isFirstUser = (await this.prisma.user.count()) === 0;
user = await this.prisma.user.create({ user = await this.prisma.user.create({
data: { data: {
id, id,
username, username
role: isFirstUser ? "ADMIN" : "STUDENT",
}, },
}); });
} else if (user.username !== username) { } else if (user.username !== username) {
@ -80,7 +77,6 @@ export class UserService {
where: { id }, where: { id },
data: { data: {
username: updateUserInput.username, username: updateUserInput.username,
role: updateUserInput.role,
}, },
}); });
} }
@ -107,7 +103,6 @@ export class UserService {
select: { select: {
id: true, id: true,
username: true, username: true,
role: true
} }
}); });
} }

14
src/types/jwt.d.ts vendored Normal file
View File

@ -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[];
};
}
}