Compare commits

...
2 Commits
11 changed files with 222 additions and 8 deletions
+1
View File
@@ -28,6 +28,7 @@ services:
redis:
image: redis:8-alpine
restart: unless-stopped
app:
build: .
@@ -0,0 +1,100 @@
import { applyDecorators, Type } from '@nestjs/common';
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
import { AppResponse } from '../responses';
interface ApiAppResponseVariant<T = any> {
model?: Type<T>;
isArray?: boolean;
summary?: string;
example?: AppResponse<T>;
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,
},
]),
),
}
: {}),
},
},
});
}),
);
}
+61
View File
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class RequirePasswordDTO {
@ApiProperty({
type: 'boolean',
example: false,
})
newUser!: boolean;
}
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
export class TokensResponseDTO {
@ApiProperty({
type: 'string',
})
access!: string;
@ApiProperty({
type: 'string',
})
refresh!: string;
}
+7
View File
@@ -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;
}
@@ -3,8 +3,8 @@ Content-Type: application/json
lang: fa
{
"phone": "09333026363",
"password": "Password1@"
"phone": "09121212123"
// "password": "Password1@"
}
{{
@@ -2,8 +2,8 @@ POST {{baseUrl}}/auth/verify-otp
Content-Type: application/json
{
"phone": "09333026363",
"otp": "55486"
"phone": "09121212123",
"otp": "26989"
}
{{
+3 -1
View File
@@ -50,11 +50,13 @@ export class LoginProvider {
});
return {
newUser: true,
message,
data: {
newUser: true,
otpExpiresAt: otpCode.expiresAt,
// Just for development
code: otpCode.code,
},
};
}