feat: add admin module

This commit is contained in:
2026-06-01 14:45:01 +03:30
parent 7dda2564a8
commit 967b96184a
30 changed files with 335 additions and 30 deletions
+2
View File
@@ -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,
};
}),
);
}
}
+6
View File
@@ -0,0 +1,6 @@
export class AppResponse<T> {
constructor(
public readonly data: T,
public readonly message?: string,
) {}
}
+2
View File
@@ -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,
) {}
}
+2 -1
View File
@@ -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}.",
+15
View File
@@ -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 -1
View File
@@ -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."
} }
+2 -1
View File
@@ -8,7 +8,8 @@
"wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.", "wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.",
"unauthorized": "دسترسی غیر مجاز", "unauthorized": "دسترسی غیر مجاز",
"tokenReuse": "توکن قبلا استفاده شده.", "tokenReuse": "توکن قبلا استفاده شده.",
"invalidOTP": "کد تایید اشتباه است یا منقضی شده." "invalidOTP": "کد تایید اشتباه است یا منقضی شده.",
"accessDenied": "دسترسی غیرمجاز. شما مجوزهای لازم برای انجام این عملیات را ندارید."
}, },
"messages": { "messages": {
"otpSent": "کد تایید به شماره {phone} ارسال شد.", "otpSent": "کد تایید به شماره {phone} ارسال شد.",
+15
View File
@@ -0,0 +1,15 @@
{
"fields": {
"title": "عنوان",
"description": "توضیحات",
"accessType": "نوع دسترسی",
"price": "قیمت",
"isActive": "فعال بودن"
},
"errors": {
"intPrice": "قیمت باید عدد صحیح باشد."
},
"messages": {
"createSuccess": "تست با موفقیت ایجاد شد."
}
}
+2 -1
View File
@@ -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} تکراری است."
} }
+8
View File
@@ -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')
+9
View File
@@ -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
View File
@@ -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);
+29 -7
View File
@@ -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 });
} }
} }
+5 -4
View File
@@ -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(
+48
View File
@@ -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',
});
}
}
}
+3
View File
@@ -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) {