diff --git a/src/auth/guards/permissions.guard.ts b/src/auth/guards/permissions.guard.ts index 97d46cc..2b573d8 100644 --- a/src/auth/guards/permissions.guard.ts +++ b/src/auth/guards/permissions.guard.ts @@ -26,7 +26,7 @@ export class PermissionsGuard implements CanActivate { const { user } = context.switchToHttp().getRequest<{ user: AuthUser }>(); if (user.role !== UserRole.ADMIN) { - throw new ForbiddenException(); + return true; } if (user.permissions.length === 0) { diff --git a/src/authorization/authorization.module.ts b/src/authorization/authorization.module.ts index 0021562..55952d2 100644 --- a/src/authorization/authorization.module.ts +++ b/src/authorization/authorization.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { Reserve } from '../reserves/entities/reserve.entity'; import { AuthorizationService } from './authorization.service'; import { Permission } from './entities/permission.entity'; import { UserPermission } from './entities/user-permission.entity'; @@ -10,7 +11,7 @@ import { ServicePolicy } from './policies/service.policy'; import { StylistPolicy } from './policies/stylist.policy'; @Module({ - imports: [TypeOrmModule.forFeature([Permission, UserPermission])], + imports: [TypeOrmModule.forFeature([Permission, UserPermission, Reserve])], providers: [ AuthorizationService, SalonPolicy, diff --git a/src/authorization/enums/permission-code.enum.ts b/src/authorization/enums/permission-code.enum.ts index eb6b40c..c27aa85 100644 --- a/src/authorization/enums/permission-code.enum.ts +++ b/src/authorization/enums/permission-code.enum.ts @@ -7,6 +7,9 @@ export enum PermissionCode { SALONS_DELETE = 'salons:delete', CUSTOMERS_READ = 'customers:read', CUSTOMERS_WRITE = 'customers:write', + CUSTOMERS_DELETE = 'customers:delete', + SMS_TEMPLATES_READ = 'sms-templates:read', + SMS_TEMPLATES_WRITE = 'sms-templates:write', STYLISTS_READ = 'stylists:read', STYLISTS_WRITE = 'stylists:write', STYLISTS_DELETE = 'stylists:delete', diff --git a/src/authorization/policies/customer.policy.ts b/src/authorization/policies/customer.policy.ts index 71d514b..a316bbe 100644 --- a/src/authorization/policies/customer.policy.ts +++ b/src/authorization/policies/customer.policy.ts @@ -1,20 +1,30 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { AuthUser } from '../../auth/interfaces/auth-user.interface'; import { Customer } from '../../customers/entities/customer.entity'; +import { Reserve } from '../../reserves/entities/reserve.entity'; import { UserRole } from '../../users/enums/user-role.enum'; import { AuthorizationService } from '../authorization.service'; @Injectable() export class CustomerPolicy { - constructor(private readonly authorizationService: AuthorizationService) {} + constructor( + private readonly authorizationService: AuthorizationService, + @InjectRepository(Reserve) + private readonly reservesRepository: Repository, + ) {} - canRead(user: AuthUser, customer: Customer): boolean { + async canRead(user: AuthUser, customer: Customer): Promise { if (this.authorizationService.isAdmin(user)) { return true; } if (user.role === UserRole.CUSTOMER) { return customer.userId === user.id; } + if (user.role === UserRole.SALON) { + return this.hasReserveAtSalon(customer.id, user.id); + } return false; } @@ -27,4 +37,16 @@ export class CustomerPolicy { } return false; } + + private async hasReserveAtSalon( + customerId: string, + salonOwnerId: string, + ): Promise { + return this.reservesRepository.exists({ + where: { + customerId, + salon: { ownerId: salonOwnerId }, + }, + }); + } } diff --git a/src/authorization/policies/reserve.policy.ts b/src/authorization/policies/reserve.policy.ts index c0687f7..9b8ec12 100644 --- a/src/authorization/policies/reserve.policy.ts +++ b/src/authorization/policies/reserve.policy.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { AuthUser } from '../../auth/interfaces/auth-user.interface'; import { UserRole } from '../../users/enums/user-role.enum'; +import { UpdateReserveDto } from '../../reserves/dto/update-reserve.dto'; import { Reserve } from '../../reserves/entities/reserve.entity'; +import { ReserveStatus } from '../../reserves/enums/reserve-status.enum'; import { AuthorizationService } from '../authorization.service'; @Injectable() @@ -34,7 +36,55 @@ export class ReservePolicy { return false; } - canUpdate(user: AuthUser, reserve: Reserve): boolean { - return this.canRead(user, reserve); + canUpdate( + user: AuthUser, + reserve: Reserve, + updateDto: UpdateReserveDto, + ): boolean { + if (!this.canRead(user, reserve)) { + return false; + } + + if (this.authorizationService.isAdmin(user)) { + return true; + } + + if (user.role === UserRole.SALON) { + return true; + } + + if (user.role === UserRole.CUSTOMER) { + return this.isCustomerCancellationUpdate(updateDto); + } + + return false; + } + + private isCustomerCancellationUpdate(updateDto: UpdateReserveDto): boolean { + const providedFields = ( + Object.keys(updateDto) as (keyof UpdateReserveDto)[] + ).filter((key) => updateDto[key] !== undefined); + + if (providedFields.length === 0) { + return false; + } + + const allowedFields: (keyof UpdateReserveDto)[] = [ + 'status', + 'cancellationReason', + ]; + + if (!providedFields.every((field) => allowedFields.includes(field))) { + return false; + } + + if ( + updateDto.status !== undefined && + updateDto.status !== ReserveStatus.CANCELLED + ) { + return false; + } + + return updateDto.status === ReserveStatus.CANCELLED; } } diff --git a/src/customers/customers.controller.ts b/src/customers/customers.controller.ts index a90168c..df1fcf0 100644 --- a/src/customers/customers.controller.ts +++ b/src/customers/customers.controller.ts @@ -49,6 +49,7 @@ export class CustomersController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.CUSTOMERS_DELETE) @Delete(':id') remove(@Param('id', ParseUUIDPipe) id: string) { return this.customersService.remove(id); diff --git a/src/customers/customers.service.ts b/src/customers/customers.service.ts index 8d0e32d..0ac145b 100644 --- a/src/customers/customers.service.ts +++ b/src/customers/customers.service.ts @@ -53,7 +53,7 @@ export class CustomersService { throw new NotFoundException(`Customer #${id} not found`); } - if (user && !this.customerPolicy.canRead(user, customer)) { + if (user && !(await this.customerPolicy.canRead(user, customer))) { throw new ForbiddenException(); } diff --git a/src/reserves/reserves.controller.ts b/src/reserves/reserves.controller.ts index 467d200..3eb5b80 100644 --- a/src/reserves/reserves.controller.ts +++ b/src/reserves/reserves.controller.ts @@ -10,9 +10,12 @@ import { UseGuards, } from '@nestjs/common'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateReserveDto } from './dto/create-reserve.dto'; import { FindReservesQueryDto } from './dto/find-reserves-query.dto'; @@ -20,11 +23,12 @@ import { UpdateReserveDto } from './dto/update-reserve.dto'; import { ReservesService } from './reserves.service'; @Controller('reserves') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class ReservesController { constructor(private readonly reservesService: ReservesService) {} @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Permissions(PermissionCode.RESERVES_WRITE) @Post() create( @Body() createReserveDto: CreateReserveDto, @@ -34,6 +38,7 @@ export class ReservesController { } @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Permissions(PermissionCode.RESERVES_READ) @Get() findAll( @Query() query: FindReservesQueryDto, @@ -43,6 +48,7 @@ export class ReservesController { } @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Permissions(PermissionCode.RESERVES_READ) @Get(':id/status-history') findStatusHistory( @Param('id', ParseUUIDPipe) id: string, @@ -52,6 +58,7 @@ export class ReservesController { } @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Permissions(PermissionCode.RESERVES_READ) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, @@ -61,6 +68,7 @@ export class ReservesController { } @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Permissions(PermissionCode.RESERVES_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/reserves/reserves.service.ts b/src/reserves/reserves.service.ts index 08914c4..28e7a0e 100644 --- a/src/reserves/reserves.service.ts +++ b/src/reserves/reserves.service.ts @@ -165,7 +165,7 @@ export class ReservesService { user: AuthUser, ): Promise { const reserve = await this.findOne(id, user); - if (!this.reservePolicy.canUpdate(user, reserve)) { + if (!this.reservePolicy.canUpdate(user, reserve, updateReserveDto)) { throw new ForbiddenException(); } diff --git a/src/salons/salons.controller.ts b/src/salons/salons.controller.ts index 5236fcf..d559eff 100644 --- a/src/salons/salons.controller.ts +++ b/src/salons/salons.controller.ts @@ -12,20 +12,24 @@ import { import { OptionalCurrentUser } from '../auth/decorators/optional-current-user.decorator'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { Public } from '../auth/decorators/public.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateSalonDto } from './dto/create-salon.dto'; import { UpdateSalonDto } from './dto/update-salon.dto'; import { SalonsService } from './salons.service'; @Controller('salons') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class SalonsController { constructor(private readonly salonsService: SalonsService) {} @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.SALONS_WRITE) @Post() create( @Body() createSalonDto: CreateSalonDto, @@ -47,6 +51,7 @@ export class SalonsController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.SALONS_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -57,6 +62,7 @@ export class SalonsController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.SALONS_DELETE) @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/services/services.controller.ts b/src/services/services.controller.ts index cb3cedc..11ca348 100644 --- a/src/services/services.controller.ts +++ b/src/services/services.controller.ts @@ -11,9 +11,12 @@ import { } from '@nestjs/common'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { Public } from '../auth/decorators/public.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateServiceSkillDto } from './dto/create-service-skill.dto'; import { CreateServiceDto } from './dto/create-service.dto'; @@ -22,11 +25,12 @@ import { UpdateServiceDto } from './dto/update-service.dto'; import { ServicesService } from './services.service'; @Controller('services') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class ServicesController { constructor(private readonly servicesService: ServicesService) {} @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Post() create( @Body() createServiceDto: CreateServiceDto, @@ -48,6 +52,7 @@ export class ServicesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -58,6 +63,7 @@ export class ServicesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_DELETE) @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, @@ -67,6 +73,7 @@ export class ServicesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Post(':serviceId/skills') createSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, @@ -96,6 +103,7 @@ export class ServicesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Patch(':serviceId/skills/:skillId') updateSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, @@ -112,6 +120,7 @@ export class ServicesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_DELETE) @Delete(':serviceId/skills/:skillId') removeSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, diff --git a/src/sms-templates/sms-templates.controller.ts b/src/sms-templates/sms-templates.controller.ts index 072d216..f84963e 100644 --- a/src/sms-templates/sms-templates.controller.ts +++ b/src/sms-templates/sms-templates.controller.ts @@ -11,9 +11,12 @@ import { UseGuards, } from '@nestjs/common'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateSmsTemplateDto } from './dto/create-sms-template.dto'; import { FindSmsTemplatesQueryDto } from './dto/find-sms-templates-query.dto'; @@ -21,11 +24,12 @@ import { UpdateSmsTemplateDto } from './dto/update-sms-template.dto'; import { SmsTemplatesService } from './sms-templates.service'; @Controller('sms-templates') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class SmsTemplatesController { constructor(private readonly smsTemplatesService: SmsTemplatesService) {} @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) @Post() create( @Body() createDto: CreateSmsTemplateDto, @@ -35,18 +39,21 @@ export class SmsTemplatesController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.SMS_TEMPLATES_READ) @Get() findAll(@Query() query: FindSmsTemplatesQueryDto) { return this.smsTemplatesService.findAll(query); } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.SMS_TEMPLATES_READ) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.smsTemplatesService.findOne(id); } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -57,6 +64,7 @@ export class SmsTemplatesController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/stylists/stylists.controller.ts b/src/stylists/stylists.controller.ts index f77e201..9097162 100644 --- a/src/stylists/stylists.controller.ts +++ b/src/stylists/stylists.controller.ts @@ -12,20 +12,24 @@ import { } from '@nestjs/common'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { Public } from '../auth/decorators/public.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateStylistDto } from './dto/create-stylist.dto'; import { UpdateStylistDto } from './dto/update-stylist.dto'; import { StylistsService } from './stylists.service'; @Controller('stylists') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class StylistsController { constructor(private readonly stylistsService: StylistsService) {} @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.STYLISTS_WRITE) @Post() create( @Body() createStylistDto: CreateStylistDto, @@ -47,6 +51,7 @@ export class StylistsController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.STYLISTS_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -57,6 +62,7 @@ export class StylistsController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @Permissions(PermissionCode.STYLISTS_DELETE) @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/suggestions/suggestions.controller.ts b/src/suggestions/suggestions.controller.ts index 5fce57d..86115ef 100644 --- a/src/suggestions/suggestions.controller.ts +++ b/src/suggestions/suggestions.controller.ts @@ -12,9 +12,12 @@ import { } from '@nestjs/common'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { Public } from '../auth/decorators/public.decorator'; +import { Permissions } from '../auth/decorators/permissions.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; +import { PermissionsGuard } from '../auth/guards/permissions.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; +import { PermissionCode } from '../authorization/enums/permission-code.enum'; import { UserRole } from '../users/enums/user-role.enum'; import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto'; import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto'; @@ -22,11 +25,12 @@ import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto' import { SuggestionsService } from './suggestions.service'; @Controller('suggestions') -@UseGuards(RolesGuard) +@UseGuards(RolesGuard, PermissionsGuard) export class SuggestionsController { constructor(private readonly suggestionsService: SuggestionsService) {} @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Post() create( @Body() createDto: CreateServiceSuggestionDto, @@ -36,6 +40,7 @@ export class SuggestionsController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_READ) @Get() findAll(@Query() query: FindSuggestionsQueryDto) { return this.suggestionsService.findAll(query); @@ -48,12 +53,14 @@ export class SuggestionsController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_READ) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.suggestionsService.findOne(id); } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_WRITE) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -64,6 +71,7 @@ export class SuggestionsController { } @Roles(UserRole.ADMIN) + @Permissions(PermissionCode.SERVICES_DELETE) @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string,