stylist module

This commit is contained in:
2026-06-28 22:18:25 +03:30
parent 6dd5526646
commit 322c9018da
11 changed files with 363 additions and 2 deletions
+2
View File
@@ -6,6 +6,7 @@ import { CustomersModule } from './customers/customers.module';
import configuration from './config/configuration'; import configuration from './config/configuration';
import { getTypeOrmOptions } from './database/typeorm-options'; import { getTypeOrmOptions } from './database/typeorm-options';
import { SalonsModule } from './salons/salons.module'; import { SalonsModule } from './salons/salons.module';
import { StylistsModule } from './stylists/stylists.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
@Module({ @Module({
@@ -18,6 +19,7 @@ import { UsersModule } from './users/users.module';
AuthModule, AuthModule,
UsersModule, UsersModule,
SalonsModule, SalonsModule,
StylistsModule,
CustomersModule, CustomersModule,
], ],
controllers: [], controllers: [],
+13 -2
View File
@@ -5,10 +5,21 @@ import { Permission } from './entities/permission.entity';
import { UserPermission } from './entities/user-permission.entity'; import { UserPermission } from './entities/user-permission.entity';
import { CustomerPolicy } from './policies/customer.policy'; import { CustomerPolicy } from './policies/customer.policy';
import { SalonPolicy } from './policies/salon.policy'; import { SalonPolicy } from './policies/salon.policy';
import { StylistPolicy } from './policies/stylist.policy';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Permission, UserPermission])], imports: [TypeOrmModule.forFeature([Permission, UserPermission])],
providers: [AuthorizationService, SalonPolicy, CustomerPolicy], providers: [
exports: [AuthorizationService, SalonPolicy, CustomerPolicy], AuthorizationService,
SalonPolicy,
CustomerPolicy,
StylistPolicy,
],
exports: [
AuthorizationService,
SalonPolicy,
CustomerPolicy,
StylistPolicy,
],
}) })
export class AuthorizationModule {} export class AuthorizationModule {}
@@ -7,4 +7,7 @@ export enum PermissionCode {
SALONS_DELETE = 'salons:delete', SALONS_DELETE = 'salons:delete',
CUSTOMERS_READ = 'customers:read', CUSTOMERS_READ = 'customers:read',
CUSTOMERS_WRITE = 'customers:write', 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);
}
}
+28
View File
@@ -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;
}
+13
View File
@@ -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;
}
+43
View File
@@ -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;
}
+67
View File
@@ -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);
}
}
+23
View File
@@ -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 {}
+87
View File
@@ -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);
}
}