feat: add admin module
This commit is contained in:
@@ -20,6 +20,7 @@ import { TestsModule } from './modules/tests/tests.module';
|
|||||||
import { QuestionsModule } from './modules/questions/questions.module';
|
import { QuestionsModule } from './modules/questions/questions.module';
|
||||||
import { SubmissionsModule } from './modules/submissions/submissions.module';
|
import { SubmissionsModule } from './modules/submissions/submissions.module';
|
||||||
import { ScoringModule } from './modules/scoring/scoring.module';
|
import { ScoringModule } from './modules/scoring/scoring.module';
|
||||||
|
import { AdminModule } from './modules/admin/admin.module';
|
||||||
|
|
||||||
const ENV = process.env.NODE_ENV;
|
const ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ const ENV = process.env.NODE_ENV;
|
|||||||
QuestionsModule,
|
QuestionsModule,
|
||||||
SubmissionsModule,
|
SubmissionsModule,
|
||||||
ScoringModule,
|
ScoringModule,
|
||||||
|
AdminModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { AppResponse, PaginatedResponse } from '../responses';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor implements NestInterceptor {
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<any>,
|
||||||
|
): Observable<any> | Promise<Observable<any>> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((response) => {
|
||||||
|
if (response instanceof PaginatedResponse) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: response.data,
|
||||||
|
pagination: {
|
||||||
|
...response.pagination,
|
||||||
|
totalPages: Math.ceil(
|
||||||
|
response.pagination.total / response.pagination.limit,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...(response.message && {
|
||||||
|
message: response.message,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response instanceof AppResponse) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
data: response.data,
|
||||||
|
...(response.message && {
|
||||||
|
message: response.message,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
data: response,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export class AppResponse<T> {
|
||||||
|
constructor(
|
||||||
|
public readonly data: T,
|
||||||
|
public readonly message?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './app-response';
|
||||||
|
export * from './paginated-response';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface PaginationMeta {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginatedResponse<T> {
|
||||||
|
constructor(
|
||||||
|
public readonly data: T[],
|
||||||
|
public readonly pagination: PaginationMeta,
|
||||||
|
public readonly message?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"wrongPhoneOrPass": "Entered phone number or password is wrong.",
|
"wrongPhoneOrPass": "Entered phone number or password is wrong.",
|
||||||
"unauthorized": "Unauthorized access",
|
"unauthorized": "Unauthorized access",
|
||||||
"tokenReuse": "Token reuse detected.",
|
"tokenReuse": "Token reuse detected.",
|
||||||
"invalidOTP": "OTP is invalid or expired."
|
"invalidOTP": "OTP is invalid or expired.",
|
||||||
|
"accessDenied": "Access denied. You do not have the required permissions to perform this action."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"otpSent": "OTP sent to {phone}.",
|
"otpSent": "OTP sent to {phone}.",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Description",
|
||||||
|
"accessType": "Access Type",
|
||||||
|
"price": "Price",
|
||||||
|
"isActive": "Is Active"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"intPrice": "Price should be an integer."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"createSuccess": "Test Created Successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,7 @@
|
|||||||
"minLengthField": "{field} should contain atleast {min} characters.",
|
"minLengthField": "{field} should contain atleast {min} characters.",
|
||||||
"maxLengthField": "{field} should contain maximum {max} characters.",
|
"maxLengthField": "{field} should contain maximum {max} characters.",
|
||||||
"wrongField": "{field} is not correct.",
|
"wrongField": "{field} is not correct.",
|
||||||
"fieldsMustMatch": "{field1} must match {field2}"
|
"wrongFieldFormat": "{field} format is wrong.",
|
||||||
|
"fieldsMustMatch": "{field1} must match {field2}.",
|
||||||
|
"duplicateField": "{field} already exists."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.",
|
"wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.",
|
||||||
"unauthorized": "دسترسی غیر مجاز",
|
"unauthorized": "دسترسی غیر مجاز",
|
||||||
"tokenReuse": "توکن قبلا استفاده شده.",
|
"tokenReuse": "توکن قبلا استفاده شده.",
|
||||||
"invalidOTP": "کد تایید اشتباه است یا منقضی شده."
|
"invalidOTP": "کد تایید اشتباه است یا منقضی شده.",
|
||||||
|
"accessDenied": "دسترسی غیرمجاز. شما مجوزهای لازم برای انجام این عملیات را ندارید."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"otpSent": "کد تایید به شماره {phone} ارسال شد.",
|
"otpSent": "کد تایید به شماره {phone} ارسال شد.",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"title": "عنوان",
|
||||||
|
"description": "توضیحات",
|
||||||
|
"accessType": "نوع دسترسی",
|
||||||
|
"price": "قیمت",
|
||||||
|
"isActive": "فعال بودن"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"intPrice": "قیمت باید عدد صحیح باشد."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"createSuccess": "تست با موفقیت ایجاد شد."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,5 +4,6 @@
|
|||||||
"maxLengthField": "{field} باید حداکثر شامل {max} کاراکتر باشد.",
|
"maxLengthField": "{field} باید حداکثر شامل {max} کاراکتر باشد.",
|
||||||
"wrongField": "{field} معتبر نیست.",
|
"wrongField": "{field} معتبر نیست.",
|
||||||
"wrongFieldFormat": "فرمت {field} معتبر نیست.",
|
"wrongFieldFormat": "فرمت {field} معتبر نیست.",
|
||||||
"fieldsMustMatch": "مقادیر {field1} و {field2} باید با هم برابر باشند."
|
"fieldsMustMatch": "مقادیر {field1} و {field2} باید با هم برابر باشند.",
|
||||||
|
"duplicateField": "مقدار {field} تکراری است."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
|
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
@@ -28,6 +29,13 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: ['http://localhost:5173'],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Borna API')
|
.setTitle('Borna API')
|
||||||
.setDescription('Borna API Documentation')
|
.setDescription('Borna API Documentation')
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TestsController } from './controllers/tests/tests.controller';
|
||||||
|
import { TestsModule } from '../tests/tests.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TestsModule],
|
||||||
|
controllers: [TestsController],
|
||||||
|
})
|
||||||
|
export class AdminModule {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Role } from '@/common/enums/roles.enum';
|
||||||
|
import { Roles } from '@/modules/auth/decorators/roles.decorator';
|
||||||
|
import { CreateTestDTO } from '@/modules/tests/dtos/create-test.dto';
|
||||||
|
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('admin/tests')
|
||||||
|
export class TestsController {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* Inject Tests Service
|
||||||
|
*/
|
||||||
|
private readonly testsService: TestsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.SUPERUSER, Role.ADMIN)
|
||||||
|
public async createTest(@Body() createTestDto: CreateTestDTO) {
|
||||||
|
return await this.testsService.createOne(createTestDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
POST http://localhost:3000/admin/tests/
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Test Three",
|
||||||
|
"description": "Description Three",
|
||||||
|
"accessType": "free",
|
||||||
|
"price": "1234000",
|
||||||
|
"isActive": true
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export const IS_PUBLIC_KEY = 'isPublic';
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Role } from '@/common/enums/roles.enum';
|
||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { ROLES_KEY } from '../constants';
|
||||||
|
|
||||||
|
export const Roles = (...roles: Array<Role>) => SetMetadata(ROLES_KEY, roles);
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { IS_PUBLIC_KEY } from '../constants';
|
import { Role } from '@/common/enums/roles.enum';
|
||||||
|
import { IS_PUBLIC_KEY, ROLES_KEY } from '../constants';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import {
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { Observable } from 'rxjs';
|
import type { Request } from 'express';
|
||||||
|
import { User } from '@/modules/users/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalAuthGuard extends JwtAuthGuard {
|
export class GlobalAuthGuard extends JwtAuthGuard {
|
||||||
@@ -15,9 +21,7 @@ export class GlobalAuthGuard extends JwtAuthGuard {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
context: ExecutionContext,
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
@@ -27,6 +31,24 @@ export class GlobalAuthGuard extends JwtAuthGuard {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.canActivate(context);
|
await super.canActivate(context);
|
||||||
|
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<Array<Role>>(
|
||||||
|
ROLES_KEY,
|
||||||
|
[context.getHandler(), context.getClass()],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requiredRoles && requiredRoles.length > 0) {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const user = request.user as User;
|
||||||
|
|
||||||
|
const hasRole = requiredRoles.includes(user.role);
|
||||||
|
|
||||||
|
if (!hasRole) {
|
||||||
|
throw new ForbiddenException('auth.errors.accessDenied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ lang: fa
|
|||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09333026363",
|
"phone": "09333026363",
|
||||||
"password": "rad1.3.5"
|
"password": "Password1@"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
POST http://localhost:3000/auth/refresh
|
POST http://localhost:3000/auth/refresh
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds",
|
||||||
|
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiJmZDRjZGViYS1iMjUzLTQ5MjgtOGIxYS0yZTJhZDMwOTIzYzMiLCJpYXQiOjE3ODAyMjM0MTEsImV4cCI6MTc4MTA4NzQxMSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.EO0OpjQix794Y7yVZ8pjLH5HngYfCTwu3lonEfXtLIQ"
|
||||||
|
// }
|
||||||
|
|
||||||
{
|
{
|
||||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjksImp0aSI6IjljOTY5OWRhLWI0NTgtNDc0Ny04YTRkLTQyODQxMDllOTVkZCIsImlhdCI6MTc3ODMwNDMyNSwiZXhwIjoxNzc5MTY4MzI1LCJhdWQiOiJsb2NhbGhvc3QiLCJpc3MiOiJsb2NhbGhvc3QifQ.oiHuj3vJRQl7rG8H0fFEfU9cG91KrLGdXqNAYFGTXVA"
|
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIyZjQ1YzcxMi01ZGQzLTRhY2ItYjExYS04ZDA5YTc3YTVmYTciLCJpYXQiOjE3Nzk5NTY1OTksImV4cCI6MTc4MDgyMDU5OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.GX4kSCUCIvBVq9l4lQ8dkETvFZ1NwBIm9NGOIjBsQCc"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ POST http://localhost:3000/auth/verify-otp
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09121111114",
|
"phone": "09333026363",
|
||||||
"otp": "99696"
|
"otp": "44875"
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from '../interfaces/jwt.interface';
|
} from '../interfaces/jwt.interface';
|
||||||
import { AuthRedisProvider } from './auth-redis.provider';
|
import { AuthRedisProvider } from './auth-redis.provider';
|
||||||
|
import { AppResponse } from '@/common/responses';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GenerateTokenProvider {
|
export class GenerateTokenProvider {
|
||||||
@@ -75,6 +76,6 @@ export class GenerateTokenProvider {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { access, refresh };
|
return new AppResponse({ access, refresh });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { HashingProvider } from '@/common/modules/hashing/providers/hashing.prov
|
|||||||
import { GenerateTokenProvider } from './generate-token.provider';
|
import { GenerateTokenProvider } from './generate-token.provider';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { I18nService } from 'nestjs-i18n';
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import { AppResponse } from '@/common/responses';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoginProvider {
|
export class LoginProvider {
|
||||||
@@ -57,10 +58,10 @@ export class LoginProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
return {
|
return new AppResponse(
|
||||||
newUser: false,
|
{ newUser: false },
|
||||||
message: this.i18nService.translate('auth.messages.sendPass'),
|
this.i18nService.translate('auth.messages.sendPass'),
|
||||||
};
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordCorrect = await this.hashingProvider.compare(
|
const isPasswordCorrect = await this.hashingProvider.compare(
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Matches,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
||||||
|
import { AccessType } from '../enums/access-types.enum';
|
||||||
|
|
||||||
|
export class CreateTestDTO {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({
|
||||||
|
message: t('validation.requiredField', {
|
||||||
|
field: '$t(tests.fields.title)',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
@MaxLength(255, {
|
||||||
|
message: t('validation.maxLengthField', {
|
||||||
|
field: '$t(tests.fields.title)',
|
||||||
|
max: 255,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({
|
||||||
|
message: t('validation.requiredField', {
|
||||||
|
field: '$t(tests.fields.description)',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@IsEnum(AccessType)
|
||||||
|
@IsOptional()
|
||||||
|
accessType?: AccessType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Matches(/^\d+$/, { message: t('tests.errors.intPrice') })
|
||||||
|
price?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export class Test {
|
|||||||
type: 'varchar',
|
type: 'varchar',
|
||||||
length: 255,
|
length: 255,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
})
|
})
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
RequestTimeoutException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Test } from '../entities/test.entity';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { CreateTestDTO } from '../dtos/create-test.dto';
|
||||||
|
import { AppException } from '@/common/exceptions/app.exception';
|
||||||
|
import { AppResponse } from '@/common/responses';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TestsService {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* Inject Tests Repository
|
||||||
|
*/
|
||||||
|
@InjectRepository(Test)
|
||||||
|
private readonly testRepository: Repository<Test>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject I18n Service
|
||||||
|
*/
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async createOne(createTestDto: CreateTestDTO) {
|
||||||
|
const titleExists = await this.testRepository.findOne({
|
||||||
|
where: {
|
||||||
|
title: createTestDto.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (titleExists) {
|
||||||
|
throw new AppException('validation.duplicateField', HttpStatus.CONFLICT, {
|
||||||
|
field: '$t(tests.fields.title)',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTest = this.testRepository.create(createTestDto);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.testRepository.save(newTest);
|
||||||
|
return new AppResponse(
|
||||||
|
newTest,
|
||||||
|
this.i18nService.translate('tests.messages.createSuccess'),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||||
|
cause: err,
|
||||||
|
description: 'Error connecting to the database',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Test } from './entities/test.entity';
|
import { Test } from './entities/test.entity';
|
||||||
|
import { TestsService } from './providers/tests.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Test])],
|
imports: [TypeOrmModule.forFeature([Test])],
|
||||||
|
providers: [TestsService],
|
||||||
|
exports: [TestsService],
|
||||||
})
|
})
|
||||||
export class TestsModule {}
|
export class TestsModule {}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
GET http://localhost:3000/users/profile
|
GET http://localhost:3000/users/profile
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
PATCH http://localhost:3000/users/profile
|
PATCH http://localhost:3000/users/profile
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzc5OTIzMzU4LCJleHAiOjE3Nzk5MjY5NTgsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.1XFNvOFdfSc2LRybiCNlRPAdP5JNWpCu5MeJCZIE9kc
|
||||||
lang: fa
|
lang: fa
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c",
|
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzc5OTU2NTk5LCJleHAiOjE3Nzk5NjAxOTksImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.e1Qay8cf6jW4OYPY8HCLs5TWUHObICkoAidj0QBhbM4",
|
||||||
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImp0aSI6IjI3ZTk1NGI2LWRjZDktNDVkNy04OWRhLTIwYWM0NjcwZmZmNCIsImlhdCI6MTc3ODM5MTQxNCwiZXhwIjoxNzc5MjU1NDE0LCJhdWQiOiJsb2NhbGhvc3QiLCJpc3MiOiJsb2NhbGhvc3QifQ.mX3KideTda658EEi03cQGxgi4j6qwPeoCg2wr9dj_u0"
|
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIyZjQ1YzcxMi01ZGQzLTRhY2ItYjExYS04ZDA5YTc3YTVmYTciLCJpYXQiOjE3Nzk5NTY1OTksImV4cCI6MTc4MDgyMDU5OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.GX4kSCUCIvBVq9l4lQ8dkETvFZ1NwBIm9NGOIjBsQCc"
|
||||||
// }
|
// }
|
||||||
|
|
||||||
{
|
{
|
||||||
"firstName": "Radi",
|
"firstName": "Radmehr",
|
||||||
"lastName": "Tar"
|
"lastName": "Tarnas",
|
||||||
// "password": "Password1@",
|
"password": "Password1@",
|
||||||
// "confirmPassword": "Password1@"
|
"confirmPassword": "Password1@"
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { User } from '../entities/user.entity';
|
|||||||
import { UpdateProfileDTO } from '../dto/update-profile.dto';
|
import { UpdateProfileDTO } from '../dto/update-profile.dto';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { AppResponse } from '@/common/responses';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserProfileProvider {
|
export class UserProfileProvider {
|
||||||
@@ -27,12 +28,12 @@ export class UserProfileProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public getProfile(user: User) {
|
public getProfile(user: User) {
|
||||||
return {
|
return new AppResponse({
|
||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
dateJoined: user.createdAt,
|
dateJoined: user.createdAt,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateProfile(user: User, updateProfileDto: UpdateProfileDTO) {
|
public async updateProfile(user: User, updateProfileDto: UpdateProfileDTO) {
|
||||||
|
|||||||
Reference in New Issue
Block a user