diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 55b9cb0..e461f1c 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -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); } diff --git a/src/modules/auth/dtos/responses/tokens.dto.ts b/src/modules/auth/dtos/responses/tokens.dto.ts index b119a8e..539126c 100644 --- a/src/modules/auth/dtos/responses/tokens.dto.ts +++ b/src/modules/auth/dtos/responses/tokens.dto.ts @@ -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; } diff --git a/src/modules/submissions/dtos/answer-submission.dto.ts b/src/modules/submissions/dtos/answer-submission.dto.ts index 7eb8661..5c18aff 100644 --- a/src/modules/submissions/dtos/answer-submission.dto.ts +++ b/src/modules/submissions/dtos/answer-submission.dto.ts @@ -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() diff --git a/src/modules/submissions/dtos/responses/get-answer-submission.dto.ts b/src/modules/submissions/dtos/responses/get-answer-submission.dto.ts new file mode 100644 index 0000000..4837bc0 --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-answer-submission.dto.ts @@ -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; +} diff --git a/src/modules/submissions/dtos/responses/get-choice.dto.ts b/src/modules/submissions/dtos/responses/get-choice.dto.ts new file mode 100644 index 0000000..4e66654 --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-choice.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetChoiceDTO { + @ApiProperty() + id!: number; + + @ApiProperty() + order!: number; + + @ApiProperty() + text!: string; +} diff --git a/src/modules/submissions/dtos/responses/get-history-message.dto.ts b/src/modules/submissions/dtos/responses/get-history-message.dto.ts new file mode 100644 index 0000000..93c0a6d --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-history-message.dto.ts @@ -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; +} diff --git a/src/modules/submissions/dtos/responses/get-message-answer.dto.ts b/src/modules/submissions/dtos/responses/get-message-answer.dto.ts new file mode 100644 index 0000000..3249847 --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-message-answer.dto.ts @@ -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; + + @ApiPropertyOptional({ isArray: true, type: GetChoiceDTO }) + choices?: Array; + + @ApiPropertyOptional({ type: 'number' }) + numericValue?: number; +} diff --git a/src/modules/submissions/dtos/responses/get-submission-history.dto.ts b/src/modules/submissions/dtos/responses/get-submission-history.dto.ts new file mode 100644 index 0000000..60b956b --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-submission-history.dto.ts @@ -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; + + @ApiProperty({ + type: GetSubmissionQuestionDTO, + nullable: true, + }) + currentQuestion!: GetSubmissionQuestionDTO; +} diff --git a/src/modules/submissions/dtos/responses/get-submission-question.dto.ts b/src/modules/submissions/dtos/responses/get-submission-question.dto.ts new file mode 100644 index 0000000..8fac866 --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-submission-question.dto.ts @@ -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; + + @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; +} diff --git a/src/modules/submissions/dtos/responses/get-submission-result.dto.ts b/src/modules/submissions/dtos/responses/get-submission-result.dto.ts new file mode 100644 index 0000000..0f79e5f --- /dev/null +++ b/src/modules/submissions/dtos/responses/get-submission-result.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetSubmissionResultDTO { + @ApiProperty() + score!: number; + + @ApiProperty() + probability!: number; + + @ApiProperty() + title!: string; + + @ApiProperty() + description!: string; +} diff --git a/src/modules/submissions/dtos/responses/start-authenticated.dto.ts b/src/modules/submissions/dtos/responses/start-authenticated.dto.ts new file mode 100644 index 0000000..46020f7 --- /dev/null +++ b/src/modules/submissions/dtos/responses/start-authenticated.dto.ts @@ -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; +} diff --git a/src/modules/submissions/dtos/responses/start-guest.dto.ts b/src/modules/submissions/dtos/responses/start-guest.dto.ts new file mode 100644 index 0000000..9439d82 --- /dev/null +++ b/src/modules/submissions/dtos/responses/start-guest.dto.ts @@ -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; +} diff --git a/src/modules/submissions/dtos/start-submission.dto.ts b/src/modules/submissions/dtos/start-submission.dto.ts index 175e45c..d01b6c1 100644 --- a/src/modules/submissions/dtos/start-submission.dto.ts +++ b/src/modules/submissions/dtos/start-submission.dto.ts @@ -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; } diff --git a/src/modules/submissions/providers/history/history-authenticated.provider.ts b/src/modules/submissions/providers/history/history-authenticated.provider.ts index bbfcec6..c0044bc 100644 --- a/src/modules/submissions/providers/history/history-authenticated.provider.ts +++ b/src/modules/submissions/providers/history/history-authenticated.provider.ts @@ -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: { diff --git a/src/modules/submissions/submissions.controller.ts b/src/modules/submissions/submissions.controller.ts index 29d6a3f..d1b1e46 100644 --- a/src/modules/submissions/submissions.controller.ts +++ b/src/modules/submissions/submissions.controller.ts @@ -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); } diff --git a/src/modules/tests/dtos/responses/get-test-by-id.dto.ts b/src/modules/tests/dtos/responses/get-test-by-id.dto.ts new file mode 100644 index 0000000..5d80248 --- /dev/null +++ b/src/modules/tests/dtos/responses/get-test-by-id.dto.ts @@ -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; +} diff --git a/src/modules/tests/tests.controller.ts b/src/modules/tests/tests.controller.ts index c7c9e65..b28d265 100644 --- a/src/modules/tests/tests.controller.ts +++ b/src/modules/tests/tests.controller.ts @@ -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); } diff --git a/src/modules/users/dtos/responses/get-profile.dto.ts b/src/modules/users/dtos/responses/get-profile.dto.ts new file mode 100644 index 0000000..e0eacb3 --- /dev/null +++ b/src/modules/users/dtos/responses/get-profile.dto.ts @@ -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; +} diff --git a/src/modules/users/dto/update-profile.dto.ts b/src/modules/users/dtos/update-profile.dto.ts similarity index 100% rename from src/modules/users/dto/update-profile.dto.ts rename to src/modules/users/dtos/update-profile.dto.ts diff --git a/src/modules/users/providers/user-profile.provider.ts b/src/modules/users/providers/user-profile.provider.ts index f9074de..56001a8 100644 --- a/src/modules/users/providers/user-profile.provider.ts +++ b/src/modules/users/providers/user-profile.provider.ts @@ -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'; diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index 5094cf7..855477c 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -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'; diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 6061f32..eaa708d 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -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,