diff --git a/nest-cli.json b/nest-cli.json index f9aa683..d5fbed8 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,16 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "classValidatorShim": true, + "introspectComments": true, + "dtoFileNameSuffix": [".dto.ts", ".entity.ts"] + } + } + ] } } diff --git a/package-lock.json b/package-lock.json index 772da75..24dcaaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.4.5", "@nestjs/typeorm": "^11.0.2", "axios": "^1.18.1", "bcrypt": "^6.0.0", @@ -2071,6 +2072,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -2391,6 +2398,26 @@ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/passport": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", @@ -2588,6 +2615,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.5.tgz", + "integrity": "sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "4.3.0", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.27", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.27.tgz", @@ -2676,6 +2736,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -4168,7 +4235,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -7447,10 +7513,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -9444,6 +9509,15 @@ "node": ">=8" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 771ccfd..d216547 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.4.5", "@nestjs/typeorm": "^11.0.2", "axios": "^1.18.1", "bcrypt": "^6.0.0", diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 71d742d..c306ffb 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -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); diff --git a/src/customers/customers.controller.ts b/src/customers/customers.controller.ts index df1fcf0..7b7dee7 100644 --- a/src/customers/customers.controller.ts +++ b/src/customers/customers.controller.ts @@ -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 { diff --git a/src/database/migrations/1720500000000-FixReservesStatusEnum.ts b/src/database/migrations/1720500000000-FixReservesStatusEnum.ts new file mode 100644 index 0000000..36b281a --- /dev/null +++ b/src/database/migrations/1720500000000-FixReservesStatusEnum.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixReservesStatusEnum1720500000000 implements MigrationInterface { + name = 'FixReservesStatusEnum1720500000000'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + // Irreversible cleanup migration. + } +} diff --git a/src/database/typeorm-options.ts b/src/database/typeorm-options.ts index 96b3048..bd568e0 100644 --- a/src/database/typeorm-options.ts +++ b/src/database/typeorm-options.ts @@ -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, }; } diff --git a/src/main.ts b/src/main.ts index bc7b628..eec0364 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) { diff --git a/src/reserves/reserves.controller.ts b/src/reserves/reserves.controller.ts index 3eb5b80..b93465e 100644 --- a/src/reserves/reserves.controller.ts +++ b/src/reserves/reserves.controller.ts @@ -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 { diff --git a/src/salons/salons.controller.ts b/src/salons/salons.controller.ts index e79e489..2f5c713 100644 --- a/src/salons/salons.controller.ts +++ b/src/salons/salons.controller.ts @@ -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); diff --git a/src/sent-messages/sent-messages.controller.ts b/src/sent-messages/sent-messages.controller.ts index 406e10b..26900e3 100644 --- a/src/sent-messages/sent-messages.controller.ts +++ b/src/sent-messages/sent-messages.controller.ts @@ -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 { diff --git a/src/services/services.controller.ts b/src/services/services.controller.ts index 11ca348..1e05e5a 100644 --- a/src/services/services.controller.ts +++ b/src/services/services.controller.ts @@ -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, diff --git a/src/sms-templates/sms-templates.controller.ts b/src/sms-templates/sms-templates.controller.ts index f84963e..99f363c 100644 --- a/src/sms-templates/sms-templates.controller.ts +++ b/src/sms-templates/sms-templates.controller.ts @@ -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 { diff --git a/src/stylists/stylists.controller.ts b/src/stylists/stylists.controller.ts index 9097162..6bc2a78 100644 --- a/src/stylists/stylists.controller.ts +++ b/src/stylists/stylists.controller.ts @@ -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); diff --git a/src/subscriptions/salon-subscriptions.controller.ts b/src/subscriptions/salon-subscriptions.controller.ts index f7edceb..f336465 100644 --- a/src/subscriptions/salon-subscriptions.controller.ts +++ b/src/subscriptions/salon-subscriptions.controller.ts @@ -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 { diff --git a/src/subscriptions/sms-packages.controller.ts b/src/subscriptions/sms-packages.controller.ts index 9787700..ba42973 100644 --- a/src/subscriptions/sms-packages.controller.ts +++ b/src/subscriptions/sms-packages.controller.ts @@ -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); diff --git a/src/subscriptions/subscription-plans.controller.ts b/src/subscriptions/subscription-plans.controller.ts index dcb1907..907dc0d 100644 --- a/src/subscriptions/subscription-plans.controller.ts +++ b/src/subscriptions/subscription-plans.controller.ts @@ -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); diff --git a/src/suggestions/suggestions.controller.ts b/src/suggestions/suggestions.controller.ts index 86115ef..678a64a 100644 --- a/src/suggestions/suggestions.controller.ts +++ b/src/suggestions/suggestions.controller.ts @@ -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); diff --git a/src/swagger/decorators/swagger-auth.decorator.ts b/src/swagger/decorators/swagger-auth.decorator.ts new file mode 100644 index 0000000..5f6d656 --- /dev/null +++ b/src/swagger/decorators/swagger-auth.decorator.ts @@ -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 })] : []), + ); +} diff --git a/src/swagger/swagger.setup.ts b/src/swagger/swagger.setup.ts new file mode 100644 index 0000000..3a4d865 --- /dev/null +++ b/src/swagger/swagger.setup.ts @@ -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', + }, + }); +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index ef313a1..10f166a 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -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 {