feat(Roles): role guards et setup ownship(not implemented, yet)

This commit is contained in:
Matthieu Haineault 2025-07-18 10:20:52 -04:00
parent e53f646659
commit cc567b2b26
13 changed files with 150 additions and 6 deletions

View File

@ -0,0 +1,11 @@
import { SetMetadata } from "@nestjs/common";
export const OWNER_KEY = 'ownership';
export interface OwnershipMeta {
serviceToken: string;
idParam?: string;
ownerField?: string;
}
export const CheckOwnership = (meta: OwnershipMeta) =>
SetMetadata(OWNER_KEY, meta);

View File

@ -2,5 +2,7 @@ import { SetMetadata } from '@nestjs/common';
import { Roles } from '@prisma/client';
export const ROLES_KEY = 'roles';
export const ROlesAllowed = (...roles: Roles[]) =>
export const RolesAllowed = (...roles: Roles[]) =>
SetMetadata(ROLES_KEY, roles);

View File

@ -0,0 +1,51 @@
import {
CanActivate,
Injectable,
ExecutionContext,
ForbiddenException,
} from "@nestjs/common";
import { Reflector, ModuleRef } from "@nestjs/core";
import { OWNER_KEY, OwnershipMeta } from "../decorators/ownership.decorator";
import { Request } from 'express';
interface RequestWithUser extends Request {
user: { id: string, role: string };
}
@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(
private reflector: Reflector,
private moduleRef: ModuleRef,
) {}
async canActivate(context: ExecutionContext): Promise<boolean>{
const meta = this.reflector.get<OwnershipMeta>(
OWNER_KEY, context.getHandler(),
);
if (!meta)
return true;
const request = context.switchToHttp().getRequest<RequestWithUser>();
const user = request.user;
const resourceId = request.params[meta.idParam || 'id'];
const service = this.moduleRef.get<any>(
meta.serviceToken,
{ strict: false },
);
const resource = await service.findOne(resourceId);
const ownerField = meta.ownerField || 'ownerId';
if (user.role === 'ADMIN') {
return true;
}
if (!resource || resource[ownerField] !== user.id) {
throw new ForbiddenException(
`You do not own the rights to this resource.`
);
}
return true;
}
}

View File

@ -22,8 +22,9 @@ export class RolesGuard implements CanActivate {
ROLES_KEY,
ctx.getHandler(),
);
if (!requiredRoles) {
return true;
//for "deny-by-default" when role is wrong or unavailable
if (!requiredRoles || requiredRoles.length === 0) {
return false;
}
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
const user = request.user;

View File

@ -1,9 +1,24 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { ModuleRef, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { JwtAuthGuard } from './modules/authentication/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard';
import { OwnershipGuard } from './common/guards/ownership.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//setup Reflector for Roles()
const reflector = app.get(Reflector);
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true}));
app.useGlobalGuards(
new JwtAuthGuard(reflector), //Authentification JWT
new RolesGuard(reflector), //deny-by-default and Role-based Access Control
new OwnershipGuard(reflector, app.get(ModuleRef)), //Global use of OwnershipGuard, not implemented yet
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@ -1,5 +1,18 @@
import { Injectable } from '@nestjs/common';
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info) {
if(err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

View File

@ -3,27 +3,33 @@ import { CustomersService } from '../services/customers.service';
import { Customers, Employees } from '@prisma/client';
import { CreateCustomerDto } from '../dtos/create-customer';
import { UpdateCustomerDto } from '../dtos/update-customer';
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller('customers')
export class CustomersController {
constructor(private readonly customersService: CustomersService) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR)
create(@Body() dto: CreateCustomerDto): Promise<Customers> {
return this.customersService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findAll(): Promise<Customers[]> {
return this.customersService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findOne(@Param('id', ParseIntPipe) id: number): Promise<Customers> {
return this.customersService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR)
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateCustomerDto,
@ -32,6 +38,7 @@ export class CustomersController {
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.SUPERVISOR)
remove(@Param('id', ParseIntPipe) id: number): Promise<Customers>{
return this.customersService.remove(id);
}

View File

@ -13,26 +13,34 @@ import { EmployeesService } from '../services/employees.service';
import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
//decorators and roles imports
import { RolesAllowed } from '../../../common/decorators/roles.decorators';
import { Roles as RoleEnum } from 'prisma/prisma-client';
@Controller('employees')
export class EmployeesController {
constructor(private readonly employeesService: EmployeesService) {}
@Post()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
create(@Body() dto: CreateEmployeeDto): Promise<Employees> {
return this.employeesService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
findAll(): Promise<Employees[]> {
return this.employeesService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateEmployeeDto,
@ -41,6 +49,7 @@ export class EmployeesController {
}
@Delete(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.remove(id);
}

View File

@ -3,27 +3,33 @@ import { LeaveRequestsService } from "../services/leave-request.service";
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
import { LeaveRequests } from "@prisma/client";
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller('leave-requests')
export class LeaveRequestController {
constructor(private readonly leaveRequetsService: LeaveRequestsService){}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
return this. leaveRequetsService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
findAll(): Promise<LeaveRequests[]> {
return this.leaveRequetsService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
return this.leaveRequetsService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateLeaveRequestsDto,
@ -32,6 +38,7 @@ export class LeaveRequestController {
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
return this.leaveRequetsService.remove(id);
}

View File

@ -3,32 +3,39 @@ import { OauthAccessTokensService } from '../services/oauth-access-tokens.servic
import { CreateOauthAccessTokenDto } from '../dtos/create-oauth-access-token.dto';
import { OAuthAccessTokens } from '@prisma/client';
import { UpdateOauthAccessTokenDto } from '../dtos/update-oauth-access-token.dto';
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller('oauth-access-tokens')
export class OauthAccessTokensController {
constructor(private readonly oauthAccessTokensService: OauthAccessTokensService){}
@Post()
@RolesAllowed(RoleEnum.ADMIN)
create(@Body()dto: CreateOauthAccessTokenDto): Promise<OAuthAccessTokens> {
return this.oauthAccessTokensService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ADMIN)
findAll(): Promise<OAuthAccessTokens[]> {
return this.oauthAccessTokensService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ADMIN)
findOne(@Param('id') id: string): Promise<OAuthAccessTokens> {
return this.oauthAccessTokensService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN)
update(@Param('id') id: string, @Body() dto: UpdateOauthAccessTokenDto): Promise<OAuthAccessTokens> {
return this.oauthAccessTokensService.update(id,dto);
}
@Delete(':id')
@RolesAllowed(RoleEnum.ADMIN)
remove(@Param('id') id: string): Promise<OAuthAccessTokens> {
return this.oauthAccessTokensService.remove(id);
}

View File

@ -3,33 +3,40 @@ import { ShiftCodesService } from "../services/shift-codes.service";
import { CreateShiftCodesDto } from "../dtos/create-shift-codes.dto";
import { ShiftCodes } from "@prisma/client";
import { UpdateShiftCodesDto } from "../dtos/update-shift-codes.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller()
export class ShiftCodesController {
constructor(private readonly shiftCodesService: ShiftCodesService) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
create(@Body()dto: CreateShiftCodesDto): Promise<ShiftCodes> {
return this.shiftCodesService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
findAll(): Promise<ShiftCodes[]> {
return this.shiftCodesService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
findOne(@Param('id', ParseIntPipe) id: number): Promise<ShiftCodes> {
return this.shiftCodesService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
update(@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateShiftCodesDto): Promise<ShiftCodes> {
return this.shiftCodesService.update(id,dto);
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
remove(@Param('id', ParseIntPipe)id: number): Promise<ShiftCodes> {
return this.shiftCodesService.remove(id);
}

View File

@ -3,6 +3,8 @@ import { ShiftsService } from "../services/shifts.service";
import { Shifts } from "@prisma/client";
import { CreateShiftDto } from "../dtos/create-shifts.dto";
import { UpdateShiftsDto } from "../dtos/update-shifts.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller('shifts')
export class ShiftsController {
@ -10,21 +12,25 @@ export class ShiftsController {
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
create(@Body() dto: CreateShiftDto): Promise<Shifts> {
return this.shiftsService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findAll(): Promise<Shifts[]> {
return this.shiftsService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
return this.shiftsService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateShiftsDto,
@ -33,6 +39,7 @@ export class ShiftsController {
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
remove(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
return this.shiftsService.remove(id);
}

View File

@ -3,27 +3,33 @@ import { TimesheetsService } from '../services/timesheets.service';
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets } from '@prisma/client';
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Controller('timesheets')
export class TimesheetsController {
constructor(private readonly timesheetsService: TimesheetsService) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
return this.timesheetsService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findAll(): Promise<Timesheets[]> {
return this.timesheetsService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
return this.timesheetsService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
update(
@Param('id', ParseIntPipe) id:number,
@Body() dto: UpdateTimesheetDto,
@ -32,6 +38,7 @@ export class TimesheetsController {
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
return this.timesheetsService.remove(id);
}