feat: complete auth and submission's swagger docs

This commit is contained in:
2026-09-02 16:37:47 +03:30
parent 8fced148b7
commit 0961b8f11d
22 changed files with 453 additions and 13 deletions
+35 -8
View File
@@ -26,16 +26,13 @@ export class AuthController {
@ApiAppResponse([
{
status: HttpStatus.OK,
description: 'Successfull responses',
variants: [
{
model: TokensResponseDTO,
example: {
data: {
access:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciIsImlhdCI6MTcyNDAwMDAwMCwiZXhwIjoxNzI0MDAzNjAwfQ.dGVzdC1zaWduYXR1cmU',
refresh:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciIsImlhdCI6MTcyNDAwMDAwMCwiZXhwIjoxNzI0MDAzNjAwfQ.dGVzdC1zaWduYXR1cmU',
access: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
},
@@ -61,8 +58,7 @@ export class AuthController {
],
},
{
status: HttpStatus.BAD_REQUEST,
description: 'BadRequest responses',
status: HttpStatus.UNAUTHORIZED,
variants: [
{
messageExample: 'شماره موبایل یا رمز عبور اشتباه است.',
@@ -71,7 +67,6 @@ export class AuthController {
},
{
status: HttpStatus.TOO_MANY_REQUESTS,
description: 'TooManyRequests responses',
variants: [
{
messageExample:
@@ -88,6 +83,24 @@ export class AuthController {
@Post('verify-otp')
@HttpCode(HttpStatus.OK)
@Throttle({ otp: {} })
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: TokensResponseDTO,
},
],
},
{
status: HttpStatus.UNAUTHORIZED,
variants: [
{
messageExample: 'کد تایید اشتباه است یا منقضی شده.',
},
],
},
])
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
return await this.authService.verifyOTP(verifyOtpDto);
}
@@ -96,6 +109,20 @@ export class AuthController {
@Post('refresh')
@HttpCode(HttpStatus.OK)
@Throttle({ login: {} })
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: TokensResponseDTO,
},
],
},
{
status: HttpStatus.UNAUTHORIZED,
variants: [{ messageExample: 'توکن قبلا استفاده شده.' }],
},
])
public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) {
return await this.authService.refreshToken(refreshTokenDto);
}
@@ -3,11 +3,13 @@ import { ApiProperty } from '@nestjs/swagger';
export class TokensResponseDTO {
@ApiProperty({
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
access!: string;
@ApiProperty({
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
refresh!: string;
}
@@ -1,18 +1,22 @@
import { IsArray, IsInt, IsNumber, IsOptional } from 'class-validator';
import { SubmissionAnswer } from '../decorators/submission-answer.decorator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class AnswerSubmissionDTO {
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
choiceId?: number;
@IsArray()
@IsOptional()
@IsInt({ each: true })
@ApiPropertyOptional({ isArray: true, type: 'number' })
choiceIds?: number[];
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
numericValue?: number;
@SubmissionAnswer()
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetSubmissionQuestionDTO } from './get-submission-question.dto';
import { GetSubmissionResultDTO } from './get-submission-result.dto';
export class GetAnswerSubmissionDTO {
@ApiProperty({
type: 'boolean',
example: false,
})
isCompleted!: boolean;
@ApiProperty({
type: GetSubmissionQuestionDTO,
nullable: true,
})
currentQuestion!: GetSubmissionQuestionDTO;
@ApiProperty({
type: GetSubmissionResultDTO,
nullable: true,
})
result!: GetSubmissionResultDTO;
}
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetChoiceDTO {
@ApiProperty()
id!: number;
@ApiProperty()
order!: number;
@ApiProperty()
text!: string;
}
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetSubmissionQuestionDTO } from './get-submission-question.dto';
import { GetMessageAnswerDTO } from './get-message-answer.dto';
export class GetHistoryMessageDTO {
@ApiProperty({
type: GetSubmissionQuestionDTO,
})
question!: GetSubmissionQuestionDTO;
@ApiProperty({
type: [GetMessageAnswerDTO],
})
answers!: Array<GetMessageAnswerDTO>;
}
@@ -0,0 +1,19 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { GetChoiceDTO } from './get-choice.dto';
export class GetMessageAnswerDTO {
@ApiPropertyOptional()
choiceId!: boolean;
@ApiPropertyOptional({ type: GetChoiceDTO })
choice?: GetChoiceDTO;
@ApiPropertyOptional({ isArray: true, type: 'number' })
choiceIds!: Array<number>;
@ApiPropertyOptional({ isArray: true, type: GetChoiceDTO })
choices?: Array<GetChoiceDTO>;
@ApiPropertyOptional({ type: 'number' })
numericValue?: number;
}
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetSubmissionQuestionDTO } from './get-submission-question.dto';
import { GetHistoryMessageDTO } from './get-history-message.dto';
export class GetSubmissionHistoryDTO {
@ApiProperty({
type: 'boolean',
example: false,
})
isCompleted!: boolean;
@ApiProperty({
isArray: true,
type: GetHistoryMessageDTO,
})
messages!: Array<GetHistoryMessageDTO>;
@ApiProperty({
type: GetSubmissionQuestionDTO,
nullable: true,
})
currentQuestion!: GetSubmissionQuestionDTO;
}
@@ -0,0 +1,63 @@
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
import { type QuestionMetadata } from '@/modules/questions/types/question-metadata.type';
import { ApiProperty } from '@nestjs/swagger';
import { GetChoiceDTO } from './get-choice.dto';
export class GetSubmissionQuestionDTO {
@ApiProperty({
type: 'number',
example: 1,
})
id!: number;
@ApiProperty({
enum: QuestionType,
enumName: 'questionType',
})
type!: QuestionType;
@ApiProperty({
type: 'string',
})
content!: string;
@ApiProperty({
type: 'boolean',
})
required!: boolean;
@ApiProperty({
type: [GetChoiceDTO],
})
choices!: Array<GetChoiceDTO>;
@ApiProperty({
oneOf: [
{
type: 'object',
properties: {
min: {
type: 'number',
},
max: {
type: 'number',
},
step: {
type: 'number',
},
},
required: ['min', 'max'],
},
{
type: 'object',
properties: {
allowOther: {
type: 'boolean',
},
},
required: ['allowOther'],
},
],
})
metadata!: QuestionMetadata;
}
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetSubmissionResultDTO {
@ApiProperty()
score!: number;
@ApiProperty()
probability!: number;
@ApiProperty()
title!: string;
@ApiProperty()
description!: string;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetSubmissionQuestionDTO } from './get-submission-question.dto';
export class StartAuthenticatedDTO {
@ApiProperty({ type: 'number', required: true })
submissionId!: number;
@ApiProperty({ type: GetSubmissionQuestionDTO })
question!: GetSubmissionQuestionDTO;
}
@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetSubmissionQuestionDTO } from './get-submission-question.dto';
export class StartGuestDTO {
@ApiProperty({ type: 'string', required: true })
guestSubmissionId!: string;
@ApiProperty({ type: GetSubmissionQuestionDTO })
question!: GetSubmissionQuestionDTO;
}
@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber } from 'class-validator';
import { i18nValidationMessage as t } from 'nestjs-i18n';
@@ -8,5 +9,9 @@ export class StartSubmissionDTO {
field: '$t(submissions.fields.testId)',
}),
})
@ApiProperty({
type: 'number',
example: 1,
})
testId!: number;
}
@@ -96,6 +96,7 @@ export class HistoryAuthenticatedProvider {
type: answer.question.type,
content: answer.question.content,
required: answer.question.required,
choices: answer.choices,
metadata: answer.question.metadata,
},
answer: {
@@ -15,6 +15,11 @@ import { ActiveUser } from '@/common/decorators/active-user.decorator';
import { SubmissionsService } from './providers/submissions.service';
import { StartSubmissionDTO } from './dtos/start-submission.dto';
import { AnswerSubmissionDTO } from './dtos/answer-submission.dto';
import { ApiAppResponse } from '@/common/decorators/api-app-response.decorator';
import { StartAuthenticatedDTO } from './dtos/responses/start-authenticated.dto';
import { StartGuestDTO } from './dtos/responses/start-guest.dto';
import { GetAnswerSubmissionDTO } from './dtos/responses/get-answer-submission.dto';
import { GetSubmissionHistoryDTO } from './dtos/responses/get-submission-history.dto';
@Controller('submissions')
export class SubmissionsController {
@@ -29,6 +34,63 @@ export class SubmissionsController {
@Post('start')
@HttpCode(HttpStatus.OK)
@UseGuards(OptionalJwtGuard)
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: StartAuthenticatedDTO,
example: {
data: {
submissionId: 1,
question: {
id: 1,
type: 'numeric',
content: 'string',
required: true,
choices: [],
metadata: {
min: 0,
max: 10,
step: 1,
},
},
},
},
},
{
model: StartGuestDTO,
example: {
data: {
guestSubmissionId: 'sahfdih123ieg1231fds23ljhakshf',
question: {
id: 2,
type: 'single-choice',
content: 'string',
required: true,
choices: [
{
id: 1,
order: 1,
text: 'string',
},
],
metadata: null,
},
},
},
},
],
},
{
status: HttpStatus.NOT_FOUND,
variants: [{ messageExample: 'تست مورد نظر یافت نشد.' }],
},
{
status: HttpStatus.FORBIDDEN,
variants: [{ messageExample: 'تست مورد نظر فعال نیست.' }],
},
])
public async startTest(
@Body() startSubmissionDto: StartSubmissionDTO,
@ActiveUser() user?: User,
@@ -43,6 +105,16 @@ export class SubmissionsController {
@Post(':id/answer')
@HttpCode(HttpStatus.OK)
@UseGuards(OptionalJwtGuard)
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: GetAnswerSubmissionDTO,
},
],
},
])
public async answerSubmission(
@Param('id') id: string,
@Body() answerSubmissionDto: AnswerSubmissionDTO,
@@ -58,6 +130,16 @@ export class SubmissionsController {
@Public()
@Get(':id/history')
@UseGuards(OptionalJwtGuard)
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: GetSubmissionHistoryDTO,
},
],
},
])
public async getHistory(@Param('id') id: string, @ActiveUser() user?: User) {
return await this.submissionsService.getSubmissionHistory(id, user);
}
@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import { AccessType } from '../../enums/access-types.enum';
export class GetTestByIdDTO {
@ApiProperty({
type: 'number',
example: 1,
})
id!: number;
@ApiProperty({
type: 'string',
example: 'Test Title',
})
title!: string;
@ApiProperty({
type: 'string',
example: 'Test description',
})
description!: string;
@ApiProperty({
enum: AccessType,
enumName: 'accessType',
example: 'login',
})
accessType!: AccessType;
@ApiProperty({
type: 'string',
example: '1000000',
})
price!: string;
}
+38 -1
View File
@@ -1,6 +1,14 @@
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import {
Controller,
Get,
HttpStatus,
Param,
ParseIntPipe,
} from '@nestjs/common';
import { TestsService } from './providers/tests.service';
import { Public } from '../auth/decorators/public.decorator';
import { ApiAppResponse } from '@/common/decorators/api-app-response.decorator';
import { GetTestByIdDTO } from './dtos/responses/get-test-by-id.dto';
@Controller('tests')
export class TestsController {
@@ -13,12 +21,41 @@ export class TestsController {
@Public()
@Get()
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
isArray: true,
model: GetTestByIdDTO,
},
],
},
])
public async getAllTests() {
return await this.testsService.userGetAll();
}
@Public()
@Get(':id')
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: GetTestByIdDTO,
},
],
},
{
status: HttpStatus.NOT_FOUND,
variants: [
{
messageExample: 'تست مورد نظر یافت نشد.',
},
],
},
])
public async getTestById(@Param('id', ParseIntPipe) id: number) {
return await this.testsService.userGetOne(id);
}
@@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetProfileDTO {
@ApiProperty({
type: 'string',
example: '09121111111',
})
phone!: string;
@ApiProperty({
type: 'string',
example: 'John',
})
firstName!: string;
@ApiProperty({
type: 'string',
example: 'Doe',
})
lastName!: string;
@ApiProperty({
type: 'string',
example: '2026-09-01T09:24:34.894Z',
})
dateJoined!: Date;
}
@@ -2,7 +2,7 @@ import { HashingProvider } from '@/common/modules/hashing/providers/hashing.prov
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { User } from '../entities/user.entity';
import { UpdateProfileDTO } from '../dto/update-profile.dto';
import { UpdateProfileDTO } from '../dtos/update-profile.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AppResponse } from '@/common/responses';
+1 -1
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { UpdateProfileDTO } from '../dto/update-profile.dto';
import { UpdateProfileDTO } from '../dtos/update-profile.dto';
import { UserProfileProvider } from './user-profile.provider';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
+32 -2
View File
@@ -1,8 +1,10 @@
import { Body, Controller, Get, Patch } from '@nestjs/common';
import { Body, Controller, Get, HttpStatus, Patch } from '@nestjs/common';
import { UsersService } from './providers/users.service';
import { ActiveUser } from '@/common/decorators/active-user.decorator';
import { User } from './entities/user.entity';
import { UpdateProfileDTO } from './dto/update-profile.dto';
import { UpdateProfileDTO } from './dtos/update-profile.dto';
import { ApiAppResponse } from '@/common/decorators/api-app-response.decorator';
import { GetProfileDTO } from './dtos/responses/get-profile.dto';
@Controller('users')
export class UsersController {
@@ -14,11 +16,39 @@ export class UsersController {
) {}
@Get('profile')
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
model: GetProfileDTO,
},
],
},
{
status: HttpStatus.UNAUTHORIZED,
variants: [
{
messageExample: 'دسترسی غیر مجاز',
},
],
},
])
public getUserProfile(@ActiveUser() user: User) {
return this.usersService.getProfile(user);
}
@Patch('profile')
@ApiAppResponse([
{
status: HttpStatus.OK,
variants: [
{
messageExample: 'تغییرات با موفقیت اعمال شد.',
},
],
},
])
public async updateUserProfile(
@ActiveUser() user: User,
@Body() updateProfileDto: UpdateProfileDTO,