stylist module
This commit is contained in:
@@ -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: [],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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