This commit is contained in:
2026-07-02 20:11:44 +03:30
parent d20e5be9c8
commit 6b3327ee16
21 changed files with 262 additions and 7 deletions
+10
View File
@@ -1,5 +1,10 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subscription.decorator';
import {
ApiProtectedEndpoint,
ApiPublicEndpoint,
} from '../swagger/decorators/swagger-auth.decorator';
import { AuthService } from './auth.service';
import { CurrentUser } from './decorators/current-user.decorator';
import { Public } from './decorators/public.decorator';
@@ -8,29 +13,34 @@ import { RequestOtpDto } from './dto/request-otp.dto';
import { VerifyOtpDto } from './dto/verify-otp.dto';
import { AuthUser } from './interfaces/auth-user.interface';
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@ApiPublicEndpoint('Request OTP code via SMS')
@Post('otp/request')
requestOtp(@Body() dto: RequestOtpDto) {
return this.authService.requestOtp(dto.mobile);
}
@Public()
@ApiPublicEndpoint('Verify OTP and receive access/refresh tokens')
@Post('otp/verify')
verifyOtp(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtp(dto);
}
@Public()
@ApiPublicEndpoint('Refresh access token')
@Post('refresh')
refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refresh(dto.refreshToken);
}
@AllowWithoutSubscription()
@ApiProtectedEndpoint('Logout and invalidate refresh token')
@Post('logout')
logout(@CurrentUser() user: AuthUser, @Body() dto: RefreshTokenDto) {
return this.authService.logout(user.id, dto.refreshToken);
+4
View File
@@ -8,6 +8,7 @@ import {
Patch,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { PermissionCode } from '../authorization/enums/permission-code.enum';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -18,7 +19,10 @@ import { AuthUser } from '../auth/interfaces/auth-user.interface';
import { UserRole } from '../users/enums/user-role.enum';
import { CustomersService } from './customers.service';
import { UpdateCustomerDto } from './dto/update-customer.dto';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Customers')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('customers')
@UseGuards(RolesGuard, PermissionsGuard)
export class CustomersController {
@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class FixReservesStatusEnum1720500000000 implements MigrationInterface {
name = 'FixReservesStatusEnum1720500000000';
public async up(queryRunner: QueryRunner): Promise<void> {
const oldType = await queryRunner.query(`
SELECT 1 FROM pg_type WHERE typname = 'reserves_status_enum_old'
`);
if (oldType.length === 0) {
return;
}
await queryRunner.query(`
ALTER TABLE "reserve_status_history"
ALTER COLUMN "fromStatus" TYPE "reserves_status_enum"
USING "fromStatus"::text::"reserves_status_enum",
ALTER COLUMN "toStatus" TYPE "reserves_status_enum"
USING "toStatus"::text::"reserves_status_enum"
`);
await queryRunner.query(`
ALTER TABLE "reserves"
ALTER COLUMN "status" TYPE "reserves_status_enum"
USING "status"::text::"reserves_status_enum"
`);
await queryRunner.query(`DROP TYPE "reserves_status_enum_old"`);
}
public async down(): Promise<void> {
// Irreversible cleanup migration.
}
}
+3 -1
View File
@@ -13,7 +13,9 @@ export function getTypeOrmOptions(): TypeOrmModuleOptions {
autoLoadEntities: true,
entities: [__dirname + '/../**/*.entity.{js,ts}'],
migrations: [__dirname + '/migrations/*.{js,ts}'],
synchronize: !isProduction,
// synchronize breaks shared PostgreSQL enums (reserves_status_enum is used
// by both reserves and reserve_status_history). Use migrations instead.
synchronize: false,
migrationsRun: isProduction,
};
}
+3
View File
@@ -1,6 +1,7 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { setupSwagger } from './swagger/swagger.setup';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@@ -17,6 +18,8 @@ async function bootstrap() {
// Global prefix
app.setGlobalPrefix('api');
setupSwagger(app);
// Cors
const corsOrigin = process.env.CORS_ORIGIN;
if (corsOrigin) {
+4
View File
@@ -9,6 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
@@ -21,7 +22,10 @@ import { CreateReserveDto } from './dto/create-reserve.dto';
import { FindReservesQueryDto } from './dto/find-reserves-query.dto';
import { UpdateReserveDto } from './dto/update-reserve.dto';
import { ReservesService } from './reserves.service';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Reserves')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('reserves')
@UseGuards(RolesGuard, PermissionsGuard)
export class ReservesController {
+7
View File
@@ -9,6 +9,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { OptionalCurrentUser } from '../auth/decorators/optional-current-user.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
@@ -23,7 +24,11 @@ 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';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Salons')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('salons')
@UseGuards(RolesGuard, PermissionsGuard)
export class SalonsController {
@@ -41,12 +46,14 @@ export class SalonsController {
}
@Public()
@ApiPublicEndpoint('List all salons')
@Get()
findAll(@OptionalCurrentUser() user?: AuthUser) {
return this.salonsService.findAll(user);
}
@Public()
@ApiPublicEndpoint('Get salon by ID')
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.salonsService.findOne(id);
@@ -9,6 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
@@ -18,7 +19,10 @@ import { CreateSentMessageDto } from './dto/create-sent-message.dto';
import { FindSentMessagesQueryDto } from './dto/find-sent-messages-query.dto';
import { UpdateSentMessageDto } from './dto/update-sent-message.dto';
import { SentMessagesService } from './sent-messages.service';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Sent Messages')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('sent-messages')
@UseGuards(RolesGuard)
export class SentMessagesController {
+9
View File
@@ -9,6 +9,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -23,7 +24,11 @@ import { CreateServiceDto } from './dto/create-service.dto';
import { UpdateServiceSkillDto } from './dto/update-service-skill.dto';
import { UpdateServiceDto } from './dto/update-service.dto';
import { ServicesService } from './services.service';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Services')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('services')
@UseGuards(RolesGuard, PermissionsGuard)
export class ServicesController {
@@ -40,12 +45,14 @@ export class ServicesController {
}
@Public()
@ApiPublicEndpoint('List all services')
@Get()
findAll() {
return this.servicesService.findAll();
}
@Public()
@ApiPublicEndpoint('Get service by ID')
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.servicesService.findOne(id);
@@ -88,12 +95,14 @@ export class ServicesController {
}
@Public()
@ApiPublicEndpoint('List skills for a service')
@Get(':serviceId/skills')
findSkills(@Param('serviceId', ParseUUIDPipe) serviceId: string) {
return this.servicesService.findSkills(serviceId);
}
@Public()
@ApiPublicEndpoint('Get a service skill by ID')
@Get(':serviceId/skills/:skillId')
findSkill(
@Param('serviceId', ParseUUIDPipe) serviceId: string,
@@ -10,6 +10,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
@@ -22,7 +23,10 @@ import { CreateSmsTemplateDto } from './dto/create-sms-template.dto';
import { FindSmsTemplatesQueryDto } from './dto/find-sms-templates-query.dto';
import { UpdateSmsTemplateDto } from './dto/update-sms-template.dto';
import { SmsTemplatesService } from './sms-templates.service';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('SMS Templates')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('sms-templates')
@UseGuards(RolesGuard, PermissionsGuard)
export class SmsTemplatesController {
+8
View File
@@ -10,6 +10,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -22,7 +23,11 @@ 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';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Stylists')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('stylists')
@UseGuards(RolesGuard, PermissionsGuard)
export class StylistsController {
@@ -39,12 +44,15 @@ export class StylistsController {
}
@Public()
@ApiPublicEndpoint('List stylists, optionally filtered by salon')
@ApiQuery({ name: 'salonId', required: false, type: String, format: 'uuid' })
@Get()
findAll(@Query('salonId', new ParseUUIDPipe({ optional: true })) salonId?: string) {
return this.stylistsService.findAll(salonId);
}
@Public()
@ApiPublicEndpoint('Get stylist by ID')
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.stylistsService.findOne(id);
@@ -7,6 +7,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
@@ -19,7 +20,10 @@ import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subsc
import { PurchaseSmsPackageDto } from './dto/purchase-sms-package.dto';
import { PurchaseSubscriptionDto } from './dto/purchase-subscription.dto';
import { SalonSubscriptionsService } from './salon-subscriptions.service';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Salon Subscriptions')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('salon-subscriptions')
@UseGuards(RolesGuard, PermissionsGuard)
export class SalonSubscriptionsController {
@@ -9,6 +9,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Public } from '../auth/decorators/public.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
@@ -20,7 +21,11 @@ import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subsc
import { CreateSmsPackageDto } from './dto/create-sms-package.dto';
import { UpdateSmsPackageDto } from './dto/update-sms-package.dto';
import { SmsPackagesService } from './sms-packages.service';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('SMS Packages')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('sms-packages')
@UseGuards(RolesGuard, PermissionsGuard)
export class SmsPackagesController {
@@ -35,6 +40,7 @@ export class SmsPackagesController {
@Public()
@AllowWithoutSubscription()
@ApiPublicEndpoint('List active SMS packages')
@Get()
findAll() {
return this.smsPackagesService.findAll(true);
@@ -42,6 +48,7 @@ export class SmsPackagesController {
@Public()
@AllowWithoutSubscription()
@ApiPublicEndpoint('Get SMS package by ID')
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.smsPackagesService.findOne(id);
@@ -9,6 +9,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -22,7 +23,11 @@ import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subsc
import { CreateSubscriptionPlanDto } from './dto/create-subscription-plan.dto';
import { UpdateSubscriptionPlanDto } from './dto/update-subscription-plan.dto';
import { SubscriptionPlansService } from './subscription-plans.service';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Subscription Plans')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('subscription-plans')
@UseGuards(RolesGuard, PermissionsGuard)
export class SubscriptionPlansController {
@@ -39,6 +44,7 @@ export class SubscriptionPlansController {
@Public()
@AllowWithoutSubscription()
@ApiPublicEndpoint('List active subscription plans')
@Get()
findAll() {
return this.subscriptionPlansService.findAll(true);
@@ -46,6 +52,7 @@ export class SubscriptionPlansController {
@Public()
@AllowWithoutSubscription()
@ApiPublicEndpoint('Get subscription plan by ID')
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.subscriptionPlansService.findOne(id);
@@ -10,6 +10,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -23,7 +24,11 @@ import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto'
import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto';
import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto';
import { SuggestionsService } from './suggestions.service';
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Suggestions')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('suggestions')
@UseGuards(RolesGuard, PermissionsGuard)
export class SuggestionsController {
@@ -47,6 +52,7 @@ export class SuggestionsController {
}
@Public()
@ApiPublicEndpoint('Get suggestions for a service')
@Get('for-service/:serviceId')
findForService(@Param('serviceId', ParseUUIDPipe) serviceId: string) {
return this.suggestionsService.findForService(serviceId);
@@ -0,0 +1,19 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { SWAGGER_JWT_AUTH } from '../swagger.setup';
export function ApiPublicEndpoint(summary?: string) {
return applyDecorators(
ApiOperation({
summary,
security: [],
}),
);
}
export function ApiProtectedEndpoint(summary?: string) {
return applyDecorators(
ApiBearerAuth(SWAGGER_JWT_AUTH),
...(summary ? [ApiOperation({ summary })] : []),
);
}
+33
View File
@@ -0,0 +1,33 @@
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export const SWAGGER_JWT_AUTH = 'access-token';
export function setupSwagger(app: INestApplication): void {
const config = new DocumentBuilder()
.setTitle('Grow API')
.setDescription('BeautyBook Grow API documentation')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT access token obtained from POST /api/auth/otp/verify',
},
SWAGGER_JWT_AUTH,
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
jsonDocumentUrl: 'docs-json',
yamlDocumentUrl: 'docs-yaml',
swaggerOptions: {
persistAuthorization: true,
tagsSorter: 'alpha',
operationsSorter: 'alpha',
},
});
}
+4
View File
@@ -10,6 +10,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { PermissionCode } from '../authorization/enums/permission-code.enum';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Permissions } from '../auth/decorators/permissions.decorator';
@@ -21,7 +22,10 @@ import { UserRole } from './enums/user-role.enum';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UsersService } from './users.service';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
@ApiTags('Users')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@Controller('users')
@UseGuards(RolesGuard, PermissionsGuard)
export class UsersController {