Compare commits
2
Commits
9ecc9647b5
...
8fced148b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fced148b7 | ||
|
|
d1324eb960 |
@@ -28,6 +28,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:8-alpine
|
image: redis:8-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@ import { VerifyOtpDTO } from './dtos/verify-otp.dto';
|
|||||||
import { Public } from './decorators/public.decorator';
|
import { Public } from './decorators/public.decorator';
|
||||||
import { RefreshTokenDTO } from './dtos/refresh-token.dto';
|
import { RefreshTokenDTO } from './dtos/refresh-token.dto';
|
||||||
import { Throttle } from '@nestjs/throttler';
|
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')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -19,6 +23,63 @@ export class AuthController {
|
|||||||
@Post('login')
|
@Post('login')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Throttle({ login: {} })
|
@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) {
|
public async logIn(@Body() loginDto: LoginDTO) {
|
||||||
return await this.authService.logIn(loginDto);
|
return await this.authService.logIn(loginDto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class RefreshTokenDTO {
|
export class RefreshTokenDTO {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'string',
|
||||||
|
description: "User's Refresh Token",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
refreshToken!: string;
|
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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { PhoneDTO } from './login.dto';
|
import { PhoneDTO } from './login.dto';
|
||||||
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class VerifyOtpDTO extends PhoneDTO {
|
export class VerifyOtpDTO extends PhoneDTO {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -9,5 +10,11 @@ export class VerifyOtpDTO extends PhoneDTO {
|
|||||||
field: '$t(auth.fields.otp)',
|
field: '$t(auth.fields.otp)',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'string',
|
||||||
|
description: '5-digit OTP code',
|
||||||
|
example: '12345',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
otp!: string;
|
otp!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ Content-Type: application/json
|
|||||||
lang: fa
|
lang: fa
|
||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09333026363",
|
"phone": "09121212123"
|
||||||
"password": "Password1@"
|
// "password": "Password1@"
|
||||||
}
|
}
|
||||||
|
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ POST {{baseUrl}}/auth/verify-otp
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09333026363",
|
"phone": "09121212123",
|
||||||
"otp": "55486"
|
"otp": "26989"
|
||||||
}
|
}
|
||||||
|
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ export class LoginProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newUser: true,
|
|
||||||
message,
|
message,
|
||||||
otpExpiresAt: otpCode.expiresAt,
|
data: {
|
||||||
// Just for development
|
newUser: true,
|
||||||
code: otpCode.code,
|
otpExpiresAt: otpCode.expiresAt,
|
||||||
|
// Just for development
|
||||||
|
code: otpCode.code,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user