stylist module
This commit is contained in:
@@ -6,6 +6,7 @@ import { CustomersModule } from './customers/customers.module';
|
||||
import configuration from './config/configuration';
|
||||
import { getTypeOrmOptions } from './database/typeorm-options';
|
||||
import { SalonsModule } from './salons/salons.module';
|
||||
import { StylistsModule } from './stylists/stylists.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
@@ -18,6 +19,7 @@ import { UsersModule } from './users/users.module';
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
SalonsModule,
|
||||
StylistsModule,
|
||||
CustomersModule,
|
||||
],
|
||||
controllers: [],
|
||||
|
||||
@@ -5,10 +5,21 @@ import { Permission } from './entities/permission.entity';
|
||||
import { UserPermission } from './entities/user-permission.entity';
|
||||
import { CustomerPolicy } from './policies/customer.policy';
|
||||
import { SalonPolicy } from './policies/salon.policy';
|
||||
import { StylistPolicy } from './policies/stylist.policy';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Permission, UserPermission])],
|
||||
providers: [AuthorizationService, SalonPolicy, CustomerPolicy],
|
||||
exports: [AuthorizationService, SalonPolicy, CustomerPolicy],
|
||||
providers: [
|
||||
AuthorizationService,
|
||||
SalonPolicy,
|
||||
CustomerPolicy,
|
||||
StylistPolicy,
|
||||
],
|
||||
exports: [
|
||||
AuthorizationService,
|
||||
SalonPolicy,
|
||||
CustomerPolicy,
|
||||
StylistPolicy,
|
||||
],
|
||||
})
|
||||
export class AuthorizationModule {}
|
||||
|
||||
@@ -7,4 +7,7 @@ export enum PermissionCode {
|
||||
SALONS_DELETE = 'salons:delete',
|
||||
CUSTOMERS_READ = 'customers:read',
|
||||
CUSTOMERS_WRITE = 'customers:write',
|
||||
STYLISTS_READ = 'stylists:read',
|
||||
STYLISTS_WRITE = 'stylists:write',
|
||||
STYLISTS_DELETE = 'stylists:delete',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthUser } from '../../auth/interfaces/auth-user.interface';
|
||||
import { Salon } from '../../salons/entities/salon.entity';
|
||||
import { Stylist } from '../../stylists/entities/stylist.entity';
|
||||
import { UserRole } from '../../users/enums/user-role.enum';
|
||||
import { AuthorizationService } from '../authorization.service';
|
||||
|
||||
@Injectable()
|
||||
export class StylistPolicy {
|
||||
constructor(private readonly authorizationService: AuthorizationService) {}
|
||||
|
||||
canManageSalon(user: AuthUser, salon: Salon): boolean {
|
||||
if (this.authorizationService.isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
return user.role === UserRole.SALON && salon.ownerId === user.id;
|
||||
}
|
||||
|
||||
canUpdate(user: AuthUser, stylist: Stylist): boolean {
|
||||
if (this.authorizationService.isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
user.role === UserRole.SALON && stylist.salon.ownerId === user.id
|
||||
);
|
||||
}
|
||||
|
||||
canDelete(user: AuthUser, stylist: Stylist): boolean {
|
||||
return this.canUpdate(user, stylist);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class AddStylists1719700000000 implements MigrationInterface {
|
||||
name = 'AddStylists1719700000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'stylists',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
generationStrategy: 'uuid',
|
||||
default: 'gen_random_uuid()',
|
||||
},
|
||||
{ name: 'salonId', type: 'uuid' },
|
||||
{ name: 'userId', type: 'uuid', isUnique: true },
|
||||
{ name: 'avatarUrl', type: 'varchar', length: '500', isNullable: true },
|
||||
{ name: 'experienceYears', type: 'int', default: 0 },
|
||||
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'stylists',
|
||||
new TableForeignKey({
|
||||
columnNames: ['salonId'],
|
||||
referencedTableName: 'salons',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'CASCADE',
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'stylists',
|
||||
new TableForeignKey({
|
||||
columnNames: ['userId'],
|
||||
referencedTableName: 'users',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'CASCADE',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('stylists', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
IsUUID,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateStylistDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
salonId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
experienceYears: number;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { IsInt, IsOptional, IsString, IsUrl, Min } from 'class-validator';
|
||||
|
||||
export class UpdateStylistDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
experienceYears?: number;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Salon } from '../../salons/entities/salon.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('stylists')
|
||||
export class Stylist {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
salonId: string;
|
||||
|
||||
@ManyToOne(() => Salon, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'salonId' })
|
||||
salon: Salon;
|
||||
|
||||
@Column({ unique: true })
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@Column({ type: 'varchar', length: 500, nullable: true })
|
||||
avatarUrl: string | null;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
experienceYears: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
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)
|
||||
export class StylistsController {
|
||||
constructor(private readonly stylistsService: StylistsService) {}
|
||||
|
||||
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||
@Post()
|
||||
create(
|
||||
@Body() createStylistDto: CreateStylistDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.stylistsService.create(createStylistDto, user);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
findAll(@Query('salonId', new ParseUUIDPipe({ optional: true })) salonId?: string) {
|
||||
return this.stylistsService.findAll(salonId);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.stylistsService.findOne(id);
|
||||
}
|
||||
|
||||
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateStylistDto: UpdateStylistDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.stylistsService.update(id, updateStylistDto, user);
|
||||
}
|
||||
|
||||
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||
@Delete(':id')
|
||||
remove(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.stylistsService.remove(id, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||
import { SalonsModule } from '../salons/salons.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { Stylist } from './entities/stylist.entity';
|
||||
import { StylistsController } from './stylists.controller';
|
||||
import { StylistsService } from './stylists.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Stylist]),
|
||||
SalonsModule,
|
||||
UsersModule,
|
||||
AuthorizationModule,
|
||||
AuthGuardsModule,
|
||||
],
|
||||
controllers: [StylistsController],
|
||||
providers: [StylistsService],
|
||||
exports: [StylistsService],
|
||||
})
|
||||
export class StylistsModule {}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { StylistPolicy } from '../authorization/policies/stylist.policy';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
import { SalonsService } from '../salons/salons.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { CreateStylistDto } from './dto/create-stylist.dto';
|
||||
import { UpdateStylistDto } from './dto/update-stylist.dto';
|
||||
import { Stylist } from './entities/stylist.entity';
|
||||
|
||||
@Injectable()
|
||||
export class StylistsService {
|
||||
constructor(
|
||||
@InjectRepository(Stylist)
|
||||
private readonly stylistsRepository: Repository<Stylist>,
|
||||
private readonly salonsService: SalonsService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly stylistPolicy: StylistPolicy,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
createStylistDto: CreateStylistDto,
|
||||
user: AuthUser,
|
||||
): Promise<Stylist> {
|
||||
const salon = await this.salonsService.findOne(createStylistDto.salonId);
|
||||
if (!this.stylistPolicy.canManageSalon(user, salon)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
await this.usersService.findOne(createStylistDto.userId);
|
||||
|
||||
const stylist = this.stylistsRepository.create(createStylistDto);
|
||||
const saved = await this.stylistsRepository.save(stylist);
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
findAll(salonId?: string): Promise<Stylist[]> {
|
||||
const where: FindOptionsWhere<Stylist> = {};
|
||||
if (salonId) {
|
||||
where.salonId = salonId;
|
||||
}
|
||||
|
||||
return this.stylistsRepository.find({
|
||||
where,
|
||||
relations: { salon: true, user: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Stylist> {
|
||||
const stylist = await this.stylistsRepository.findOne({
|
||||
where: { id },
|
||||
relations: { salon: true, user: true },
|
||||
});
|
||||
if (!stylist) {
|
||||
throw new NotFoundException(`Stylist #${id} not found`);
|
||||
}
|
||||
return stylist;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
updateStylistDto: UpdateStylistDto,
|
||||
user: AuthUser,
|
||||
): Promise<Stylist> {
|
||||
const stylist = await this.findOne(id);
|
||||
if (!this.stylistPolicy.canUpdate(user, stylist)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
Object.assign(stylist, updateStylistDto);
|
||||
await this.stylistsRepository.save(stylist);
|
||||
return this.findOne(stylist.id);
|
||||
}
|
||||
|
||||
async remove(id: string, user: AuthUser): Promise<void> {
|
||||
const stylist = await this.findOne(id);
|
||||
if (!this.stylistPolicy.canDelete(user, stylist)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
await this.stylistsRepository.remove(stylist);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user