diff --git a/docker-compose.yml b/docker-compose.yml index e39ab40..5da4e56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: redis: image: redis:8-alpine + restart: unless-stopped app: build: . diff --git a/src/common/decorators/api-app-response.decorator.ts b/src/common/decorators/api-app-response.decorator.ts new file mode 100644 index 0000000..603ea99 --- /dev/null +++ b/src/common/decorators/api-app-response.decorator.ts @@ -0,0 +1,100 @@ +import { applyDecorators, Type } from '@nestjs/common'; +import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'; +import { AppResponse } from '../responses'; + +interface ApiAppResponseVariant { + model?: Type; + isArray?: boolean; + summary?: string; + example?: AppResponse; + messageExample?: string; +} + +interface ApiAppResponseStatus { + status: number; + description?: string; + variants: ApiAppResponseVariant[]; +} + +export function ApiAppResponse( + options: ApiAppResponseStatus[], +): MethodDecorator { + const models = options.flatMap((status) => + status.variants + .filter((variant) => variant.model) + .map((variant) => variant.model!), + ); + + return applyDecorators( + ApiExtraModels(AppResponse, ...models), + + ...options.map((status) => { + const variants = status.variants; + + const schemas = variants.map((variant) => ({ + allOf: [ + { + $ref: getSchemaPath(AppResponse), + }, + { + type: 'object', + properties: { + data: variant.model + ? variant.isArray + ? { + type: 'array', + items: { + $ref: getSchemaPath(variant.model), + }, + } + : { + $ref: getSchemaPath(variant.model), + } + : { + nullable: true, + }, + + message: { + type: 'string', + example: + variant.messageExample ?? 'Operation completed successfully', + }, + }, + }, + ], + })); + + return ApiResponse({ + status: status.status, + description: status.description, + + content: { + 'application/json': { + schema: + variants.length > 1 + ? { + oneOf: schemas, + } + : schemas[0], + + ...(variants.some((variant) => variant.example) + ? { + examples: Object.fromEntries( + variants + .filter((variant) => variant.example) + .map((variant, index) => [ + `response${index + 1}`, + { + summary: variant.summary ?? `Response ${index + 1}`, + value: variant.example, + }, + ]), + ), + } + : {}), + }, + }, + }); + }), + ); +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index c33d611..55b9cb0 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -5,6 +5,10 @@ import { VerifyOtpDTO } from './dtos/verify-otp.dto'; import { Public } from './decorators/public.decorator'; import { RefreshTokenDTO } from './dtos/refresh-token.dto'; import { Throttle } from '@nestjs/throttler'; +import { ApiAppResponse } from '@/common/decorators/api-app-response.decorator'; +import { NewUserLoginResponseDTO } from './dtos/responses/new-user-login.dto'; +import { RequirePasswordDTO } from './dtos/responses/require-password.dto'; +import { TokensResponseDTO } from './dtos/responses/tokens.dto'; @Controller('auth') export class AuthController { @@ -19,6 +23,63 @@ export class AuthController { @Post('login') @HttpCode(HttpStatus.OK) @Throttle({ login: {} }) + @ApiAppResponse([ + { + status: HttpStatus.OK, + description: 'Successfull responses', + variants: [ + { + model: TokensResponseDTO, + example: { + data: { + access: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciIsImlhdCI6MTcyNDAwMDAwMCwiZXhwIjoxNzI0MDAzNjAwfQ.dGVzdC1zaWduYXR1cmU', + refresh: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciIsImlhdCI6MTcyNDAwMDAwMCwiZXhwIjoxNzI0MDAzNjAwfQ.dGVzdC1zaWduYXR1cmU', + }, + }, + }, + { + model: NewUserLoginResponseDTO, + example: { + data: { + newUser: true, + otpExpiresAt: '2026-09-01T09:24:34.894Z', + }, + message: 'کد تایید به شماره 09121111111 فرستاده شد.', + }, + }, + { + model: RequirePasswordDTO, + example: { + data: { + newUser: false, + }, + message: 'لطفا رمز عبور را ارسال کنید.', + }, + }, + ], + }, + { + status: HttpStatus.BAD_REQUEST, + description: 'BadRequest responses', + variants: [ + { + messageExample: 'شماره موبایل یا رمز عبور اشتباه است.', + }, + ], + }, + { + status: HttpStatus.TOO_MANY_REQUESTS, + description: 'TooManyRequests responses', + variants: [ + { + messageExample: + 'تعداد درخواست ها بیشتر از حد مجاز است! لطفا بعد از 1 دقیقه دوباره امتحان کنید.', + }, + ], + }, + ]) public async logIn(@Body() loginDto: LoginDTO) { return await this.authService.logIn(loginDto); } diff --git a/src/modules/auth/dtos/refresh-token.dto.ts b/src/modules/auth/dtos/refresh-token.dto.ts index 51416e7..d89cd40 100644 --- a/src/modules/auth/dtos/refresh-token.dto.ts +++ b/src/modules/auth/dtos/refresh-token.dto.ts @@ -1,7 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class RefreshTokenDTO { @IsNotEmpty() @IsString() + @ApiProperty({ + type: 'string', + description: "User's Refresh Token", + required: true, + }) refreshToken!: string; } diff --git a/src/modules/auth/dtos/responses/new-user-login.dto.ts b/src/modules/auth/dtos/responses/new-user-login.dto.ts new file mode 100644 index 0000000..56667f8 --- /dev/null +++ b/src/modules/auth/dtos/responses/new-user-login.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class NewUserLoginResponseDTO { + @ApiProperty({ + type: 'boolean', + example: true, + }) + newUser!: boolean; + + @ApiProperty({ + type: 'string', + example: '2026-09-01T09:24:34.894Z', + }) + otpExpiresAt!: Date; +} diff --git a/src/modules/auth/dtos/responses/require-password.dto.ts b/src/modules/auth/dtos/responses/require-password.dto.ts new file mode 100644 index 0000000..1997516 --- /dev/null +++ b/src/modules/auth/dtos/responses/require-password.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RequirePasswordDTO { + @ApiProperty({ + type: 'boolean', + example: false, + }) + newUser!: boolean; +} diff --git a/src/modules/auth/dtos/responses/tokens.dto.ts b/src/modules/auth/dtos/responses/tokens.dto.ts new file mode 100644 index 0000000..b119a8e --- /dev/null +++ b/src/modules/auth/dtos/responses/tokens.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TokensResponseDTO { + @ApiProperty({ + type: 'string', + }) + access!: string; + + @ApiProperty({ + type: 'string', + }) + refresh!: string; +} diff --git a/src/modules/auth/dtos/verify-otp.dto.ts b/src/modules/auth/dtos/verify-otp.dto.ts index 4b7ed06..c40d31d 100644 --- a/src/modules/auth/dtos/verify-otp.dto.ts +++ b/src/modules/auth/dtos/verify-otp.dto.ts @@ -1,6 +1,7 @@ import { IsNotEmpty, IsString } from 'class-validator'; import { PhoneDTO } from './login.dto'; import { i18nValidationMessage as t } from 'nestjs-i18n'; +import { ApiProperty } from '@nestjs/swagger'; export class VerifyOtpDTO extends PhoneDTO { @IsString() @@ -9,5 +10,11 @@ export class VerifyOtpDTO extends PhoneDTO { field: '$t(auth.fields.otp)', }), }) + @ApiProperty({ + type: 'string', + description: '5-digit OTP code', + example: '12345', + required: true, + }) otp!: string; } diff --git a/src/modules/auth/http/login.post.endpoints.http b/src/modules/auth/http/login.post.endpoints.http index e91fb4a..889367b 100644 --- a/src/modules/auth/http/login.post.endpoints.http +++ b/src/modules/auth/http/login.post.endpoints.http @@ -3,8 +3,8 @@ Content-Type: application/json lang: fa { - "phone": "09333026363", - "password": "Password1@" + "phone": "09121212123" + // "password": "Password1@" } {{ diff --git a/src/modules/auth/http/verify-otp.post.endpoints.http b/src/modules/auth/http/verify-otp.post.endpoints.http index f8674ef..456f9e6 100644 --- a/src/modules/auth/http/verify-otp.post.endpoints.http +++ b/src/modules/auth/http/verify-otp.post.endpoints.http @@ -2,8 +2,8 @@ POST {{baseUrl}}/auth/verify-otp Content-Type: application/json { - "phone": "09333026363", - "otp": "55486" + "phone": "09121212123", + "otp": "26989" } {{ diff --git a/src/modules/auth/providers/login.provider.ts b/src/modules/auth/providers/login.provider.ts index d2146cd..5235ec0 100644 --- a/src/modules/auth/providers/login.provider.ts +++ b/src/modules/auth/providers/login.provider.ts @@ -50,11 +50,13 @@ export class LoginProvider { }); return { - newUser: true, message, - otpExpiresAt: otpCode.expiresAt, - // Just for development - code: otpCode.code, + data: { + newUser: true, + otpExpiresAt: otpCode.expiresAt, + // Just for development + code: otpCode.code, + }, }; }