mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-24 21:32:28 +00:00
feat: working on edit profile ticket
This commit is contained in:
@ -25,7 +25,13 @@ export class AllowanceChangeRequestsRepository {
|
||||
|
||||
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
|
||||
const relations = withRelations
|
||||
? ['allowance', 'allowance.junior', 'allowance.junior.customer', 'allowance.junior.customer.profilePicture']
|
||||
? [
|
||||
'allowance',
|
||||
'allowance.junior',
|
||||
'allowance.junior.customer',
|
||||
'allowance.junior.customer.user',
|
||||
'allowance.junior.customer.user.profilePicture',
|
||||
]
|
||||
: [];
|
||||
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
|
||||
}
|
||||
@ -43,7 +49,8 @@ export class AllowanceChangeRequestsRepository {
|
||||
'allowance',
|
||||
'allowance.junior',
|
||||
'allowance.junior.customer',
|
||||
'allowance.junior.customer.profilePicture',
|
||||
'allowance.junior.customer.user',
|
||||
'allowance.junior.customer.user.profilePicture',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -28,14 +28,14 @@ export class AllowancesRepository {
|
||||
findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||
return this.allowancesRepository.findOne({
|
||||
where: { id: allowanceId, guardianId },
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'],
|
||||
});
|
||||
}
|
||||
|
||||
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||
return this.allowancesRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'],
|
||||
take: query.size,
|
||||
skip: query.size * (query.page - ONE),
|
||||
});
|
||||
|
@ -122,7 +122,7 @@ export class AllowanceChangeRequestsService {
|
||||
this.logger.log(`Preparing allowance change requests images`);
|
||||
return Promise.all(
|
||||
requests.map(async (request) => {
|
||||
const profilePicture = request.allowance.junior.customer.profilePicture;
|
||||
const profilePicture = request.allowance.junior.customer.user.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ export class AllowancesService {
|
||||
this.logger.log(`Preparing document for allowances`);
|
||||
await Promise.all(
|
||||
allowance.map(async (allowance) => {
|
||||
const profilePicture = allowance.junior.customer.profilePicture;
|
||||
const profilePicture = allowance.junior.customer.user.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export class VerifyUserRequestDto {
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
|
||||
lastName!: string;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
@ApiProperty({ example: '2001-01-01' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
dateOfBirth!: Date;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { User } from '~/user/entities';
|
||||
|
||||
export class UserResponseDto {
|
||||
@ -7,42 +7,39 @@ export class UserResponseDto {
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
countryCode!: string;
|
||||
|
||||
@ApiProperty()
|
||||
phoneNumber!: string;
|
||||
|
||||
@ApiProperty()
|
||||
countryCode!: string;
|
||||
email!: string;
|
||||
|
||||
@ApiProperty()
|
||||
isPasswordSet!: boolean;
|
||||
firstName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
isProfileCompleted!: boolean;
|
||||
lastName!: string;
|
||||
|
||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto, nullable: true })
|
||||
profilePicture!: DocumentMetaResponseDto | null;
|
||||
|
||||
@ApiProperty()
|
||||
isSmsEnabled!: boolean;
|
||||
isPhoneVerified!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
isEmailEnabled!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
isPushEnabled!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
roles!: Roles[];
|
||||
isEmailVerified!: boolean;
|
||||
|
||||
constructor(user: User) {
|
||||
this.id = user.id;
|
||||
this.email = user.email;
|
||||
this.phoneNumber = user.phoneNumber;
|
||||
this.countryCode = user.countryCode;
|
||||
this.isPasswordSet = user.isPasswordSet;
|
||||
this.isProfileCompleted = user.isProfileCompleted;
|
||||
this.isSmsEnabled = user.isSmsEnabled;
|
||||
this.isEmailEnabled = user.isEmailEnabled;
|
||||
this.isPushEnabled = user.isPushEnabled;
|
||||
this.roles = user.roles;
|
||||
this.phoneNumber = user.phoneNumber;
|
||||
|
||||
this.email = user.email;
|
||||
this.firstName = user.firstName;
|
||||
this.lastName = user.lastName;
|
||||
this.profilePicture = user.profilePicture ? new DocumentMetaResponseDto(user.profilePicture) : null;
|
||||
this.isEmailVerified = user.isEmailVerified;
|
||||
this.isPhoneVerified = user.isPhoneVerified;
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,13 @@ export class CreateCustomerRequestDto {
|
||||
@IsOptional()
|
||||
gender?: Gender;
|
||||
|
||||
@ApiProperty({ example: 'JO' })
|
||||
@ApiProperty({ enum: CountryIso, example: CountryIso.SAUDI_ARABIA })
|
||||
@IsEnum(CountryIso, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.countryOfResidence' }),
|
||||
})
|
||||
countryOfResidence!: CountryIso;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
@ApiProperty({ example: '2001-01-01' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
dateOfBirth!: Date;
|
||||
|
@ -104,7 +104,5 @@ export class CustomerResponseDto {
|
||||
this.neighborhood = customer.neighborhood;
|
||||
this.street = customer.street;
|
||||
this.building = customer.building;
|
||||
|
||||
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,5 @@ export class InternalCustomerDetailsResponseDto {
|
||||
this.isGuardian = customer.isGuardian;
|
||||
this.civilIdFront = new DocumentMetaResponseDto(customer.civilIdFront);
|
||||
this.civilIdBack = new DocumentMetaResponseDto(customer.civilIdBack);
|
||||
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||
}
|
||||
}
|
||||
|
@ -96,13 +96,6 @@ export class Customer extends BaseEntity {
|
||||
@Column('varchar', { name: 'building', length: 255, nullable: true })
|
||||
building!: string;
|
||||
|
||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||
profilePictureId!: string;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
|
||||
@JoinColumn({ name: 'profile_picture_id' })
|
||||
profilePicture!: Document;
|
||||
|
||||
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
@ -15,7 +15,7 @@ export class CustomerRepository {
|
||||
findOne(where: FindOptionsWhere<Customer>) {
|
||||
return this.customerRepository.findOne({
|
||||
where,
|
||||
relations: ['profilePicture', 'user', 'civilIdFront', 'civilIdBack', 'cards'],
|
||||
relations: ['user', 'civilIdFront', 'civilIdBack', 'cards'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ export class CustomerRepository {
|
||||
|
||||
findCustomers(filters: CustomerFiltersRequestDto) {
|
||||
const query = this.customerRepository.createQueryBuilder('customer');
|
||||
query.leftJoinAndSelect('customer.profilePicture', 'profilePicture');
|
||||
query.leftJoinAndSelect('customer.user', 'user');
|
||||
|
||||
if (filters.name) {
|
||||
|
@ -30,6 +30,9 @@ export class CustomerService {
|
||||
this.logger.log(`Updating customer ${userId}`);
|
||||
|
||||
await this.validateProfilePictureForCustomer(userId, data.profilePictureId);
|
||||
if (data.civilIdBackId || data.civilIdFrontId) {
|
||||
await this.validateCivilIdForCustomer(userId, data.civilIdFrontId!, data.civilIdBackId!);
|
||||
}
|
||||
await this.customerRepository.updateCustomer(userId, data);
|
||||
this.logger.log(`Customer ${userId} updated successfully`);
|
||||
return this.findCustomerById(userId);
|
||||
@ -52,9 +55,6 @@ export class CustomerService {
|
||||
throw new BadRequestException('CUSTOMER.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (customer.profilePicture) {
|
||||
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
|
||||
}
|
||||
this.logger.log(`Customer ${id} found successfully`);
|
||||
return customer;
|
||||
}
|
||||
@ -101,8 +101,6 @@ export class CustomerService {
|
||||
throw new BadRequestException('CUSTOMER.ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
// await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId);
|
||||
|
||||
const customer = await this.customerRepository.createCustomer(userId, body, true);
|
||||
this.logger.log(`customer created for user ${userId}`);
|
||||
|
||||
@ -215,14 +213,7 @@ export class CustomerService {
|
||||
promises.push(this.ociService.generatePreSignedUrl(customer.civilIdFront));
|
||||
promises.push(this.ociService.generatePreSignedUrl(customer.civilIdBack));
|
||||
|
||||
if (customer.profilePicture) {
|
||||
promises.push(this.ociService.generatePreSignedUrl(customer.profilePicture));
|
||||
}
|
||||
|
||||
const [civilIdFrontUrl, civilIdBackUrl, profilePictureUrl] = await Promise.all(promises);
|
||||
if (customer.profilePicture) {
|
||||
customer.profilePicture.url = profilePictureUrl;
|
||||
}
|
||||
|
||||
customer.civilIdFront.url = civilIdFrontUrl;
|
||||
customer.civilIdBack.url = civilIdBackUrl;
|
||||
|
37
src/db/migrations/1754399872619-add-display-name-to-user.ts
Normal file
37
src/db/migrations/1754399872619-add-display-name-to-user.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDisplayNameToUser1754399872619 implements MigrationInterface {
|
||||
name = 'AddDisplayNameToUser1754399872619';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Step 1: Add columns as nullable
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "first_name" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "last_name" character varying(255)`);
|
||||
|
||||
// Step 2: Populate the new columns with fallback to test values
|
||||
await queryRunner.query(`
|
||||
UPDATE "users"
|
||||
SET "first_name" = COALESCE(c."first_name", 'TEST_FIRST_NAME'),
|
||||
"last_name" = COALESCE(c."last_name", 'TEST_LAST_NAME')
|
||||
FROM "customers" c
|
||||
WHERE c.user_id = "users"."id"
|
||||
`);
|
||||
|
||||
// Step 2b: Handle users without a matching customer row
|
||||
await queryRunner.query(`
|
||||
UPDATE "users"
|
||||
SET "first_name" = COALESCE("first_name", 'TEST_FIRST_NAME'),
|
||||
"last_name" = COALESCE("last_name", 'TEST_LAST_NAME')
|
||||
WHERE "first_name" IS NULL OR "last_name" IS NULL
|
||||
`);
|
||||
|
||||
// Step 3: Make the columns NOT NULL
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "first_name" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "last_name" SET NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "last_name"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "first_name"`);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddProfilePictureToUserInsteadOfCustomer1754401348483 implements MigrationInterface {
|
||||
name = 'AddProfilePictureToUserInsteadOfCustomer1754401348483';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "REL_e7574892da11dd01de5cfc4649"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "profile_picture_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "profile_picture_id" uuid`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "UQ_02ec15de199e79a0c46869895f4" UNIQUE ("profile_picture_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_02ec15de199e79a0c46869895f4"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_02ec15de199e79a0c46869895f4"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profile_picture_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "profile_picture_id" uuid`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "REL_e7574892da11dd01de5cfc4649" UNIQUE ("profile_picture_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
@ -6,3 +6,5 @@ export * from './1753874205042-add-neoleap-related-entities';
|
||||
export * from './1753948642040-add-account-number-and-iban-to-account-entity';
|
||||
export * from './1754210729273-add-vpan-to-card';
|
||||
export * from './1754226754947-add-upload-status-to-document-entity';
|
||||
export * from './1754399872619-add-display-name-to-user';
|
||||
export * from './1754401348483-add-profile-picture-to-user-instead-of-customer';
|
||||
|
@ -37,8 +37,8 @@ export class Document {
|
||||
@Column({ type: 'uuid', nullable: true, name: 'created_by_id' })
|
||||
createdById!: string;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
|
||||
customerPicture?: Customer;
|
||||
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'SET NULL' })
|
||||
userPicture?: Customer;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.civilIdFront, { onDelete: 'SET NULL' })
|
||||
customerCivilIdFront?: User;
|
||||
|
@ -51,7 +51,7 @@ export class OciService {
|
||||
}
|
||||
|
||||
const bucketName = BUCKETS[document.documentType];
|
||||
const objectName = document.name;
|
||||
const objectName = document.name + document.extension;
|
||||
const expiration = moment().add(TWO, 'hours').toDate();
|
||||
|
||||
try {
|
||||
|
@ -20,8 +20,8 @@ export class JuniorResponseDto {
|
||||
this.id = junior.id;
|
||||
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
|
||||
this.relationship = junior.relationship;
|
||||
this.profilePicture = junior.customer.profilePicture
|
||||
? new DocumentMetaResponseDto(junior.customer.profilePicture)
|
||||
this.profilePicture = junior.customer.user.profilePicture
|
||||
? new DocumentMetaResponseDto(junior.customer.user.profilePicture)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export class JuniorRepository {
|
||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||
return this.juniorRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['customer', 'customer.user', 'customer.profilePicture'],
|
||||
relations: ['customer', 'customer.user', 'customer.user.profilePicture'],
|
||||
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
||||
take: pageOptions.size,
|
||||
});
|
||||
|
@ -132,7 +132,7 @@ export class JuniorService {
|
||||
this.logger.log(`Preparing junior images`);
|
||||
await Promise.all(
|
||||
juniors.map(async (junior) => {
|
||||
const profilePicture = junior.customer.profilePicture;
|
||||
const profilePicture = junior.customer.user.profilePicture;
|
||||
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
|
@ -34,7 +34,8 @@ export class MoneyRequestsRepository {
|
||||
const query = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
||||
query.leftJoinAndSelect('moneyRequest.requester', 'requester');
|
||||
query.leftJoinAndSelect('requester.customer', 'customer');
|
||||
query.leftJoinAndSelect('customer.profilePicture', 'profilePicture');
|
||||
query.leftJoinAndSelect('customer.user', 'user');
|
||||
query.leftJoinAndSelect('user.profilePicture', 'profilePicture');
|
||||
query.orderBy('moneyRequest.createdAt', 'DESC');
|
||||
query.where('moneyRequest.reviewerId = :reviewerId', { reviewerId });
|
||||
query.andWhere('moneyRequest.status = :status', { status: filters.status });
|
||||
|
@ -106,7 +106,7 @@ export class MoneyRequestsService {
|
||||
this.logger.log(`Preparing document for money requests`);
|
||||
await Promise.all(
|
||||
moneyRequests.map(async (moneyRequest) => {
|
||||
const profilePicture = moneyRequest.requester.customer.profilePicture;
|
||||
const profilePicture = moneyRequest.requester.customer.user.profilePicture;
|
||||
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
|
@ -36,7 +36,8 @@ export class TaskRepository {
|
||||
'image',
|
||||
'assignedTo',
|
||||
'assignedTo.customer',
|
||||
'assignedTo.customer.profilePicture',
|
||||
'assignedTo.customer.user',
|
||||
'assignedTo.customer.user.profilePicture',
|
||||
'submission',
|
||||
'submission.proofOfCompletion',
|
||||
],
|
||||
@ -50,7 +51,8 @@ export class TaskRepository {
|
||||
.leftJoinAndSelect('task.image', 'image')
|
||||
.leftJoinAndSelect('task.assignedTo', 'assignedTo')
|
||||
.leftJoinAndSelect('assignedTo.customer', 'customer')
|
||||
.leftJoinAndSelect('customer.profilePicture', 'profilePicture')
|
||||
.leftJoinAndSelect('customer.user', 'user')
|
||||
.leftJoinAndSelect('user.profilePicture', 'profilePicture')
|
||||
.leftJoinAndSelect('task.submission', 'submission')
|
||||
.leftJoinAndSelect('submission.proofOfCompletion', 'proofOfCompletion');
|
||||
|
||||
|
@ -132,7 +132,7 @@ export class TaskService {
|
||||
const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([
|
||||
this.ociService.generatePreSignedUrl(task.image),
|
||||
this.ociService.generatePreSignedUrl(task.submission?.proofOfCompletion),
|
||||
this.ociService.generatePreSignedUrl(task.assignedTo.customer.profilePicture),
|
||||
this.ociService.generatePreSignedUrl(task.assignedTo.customer.user.profilePicture),
|
||||
]);
|
||||
|
||||
task.image.url = imageUrl;
|
||||
@ -141,8 +141,8 @@ export class TaskService {
|
||||
task.submission.proofOfCompletion.url = submissionUrl;
|
||||
}
|
||||
|
||||
if (task.assignedTo.customer.profilePicture) {
|
||||
task.assignedTo.customer.profilePicture.url = profilePictureUrl;
|
||||
if (task.assignedTo.customer.user.profilePicture) {
|
||||
task.assignedTo.customer.user.profilePicture.url = profilePictureUrl;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { UserResponseDto } from '~/auth/dtos/response';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { AllowedRoles } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateCheckerRequestDto, UserFiltersRequestDto } from '../dtos/request';
|
||||
import { UserService } from '../services';
|
||||
|
||||
@Controller('admin/users')
|
||||
@ApiTags('Users')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.SUPER_ADMIN)
|
||||
@ApiBearerAuth()
|
||||
export class AdminUserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
@Post()
|
||||
@ApiDataResponse(UserResponseDto)
|
||||
async createCheckers(@Body() data: CreateCheckerRequestDto) {
|
||||
const user = await this.userService.createChecker(data);
|
||||
|
||||
return ResponseFactory.data(new UserResponseDto(user));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiDataPageResponse(UserResponseDto)
|
||||
async findUsers(@Query() filters: UserFiltersRequestDto) {
|
||||
const [users, count] = await this.userService.findUsers(filters);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
users.map((user) => new UserResponseDto(user)),
|
||||
{
|
||||
page: filters.page,
|
||||
size: filters.size,
|
||||
itemCount: count,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':userId')
|
||||
@ApiDataResponse(UserResponseDto)
|
||||
async findUserById(@Param('userId', CustomParseUUIDPipe) userId: string) {
|
||||
const user = await this.userService.findUserOrThrow({ id: userId });
|
||||
|
||||
return ResponseFactory.data(new UserResponseDto(user));
|
||||
}
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * from './admin.user.controller';
|
||||
export * from './user.controller';
|
||||
|
@ -1,20 +1,52 @@
|
||||
import { Body, Controller, Headers, HttpCode, HttpStatus, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Headers, HttpCode, HttpStatus, Patch, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { VerifyOtpRequestDto } from '~/auth/dtos/request';
|
||||
import { UserResponseDto } from '~/auth/dtos/response';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { SetInternalPasswordRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { ApiDataResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { UpdateNotificationsSettingsRequestDto, UpdateUserRequestDto } from '../dtos/request';
|
||||
import { UpdateEmailRequestDto } from '../dtos/request/update-email.request.dto';
|
||||
import { UserService } from '../services';
|
||||
|
||||
@Controller('users')
|
||||
@ApiTags('Users')
|
||||
@Controller('profile')
|
||||
@ApiTags('User - Profile')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiBearerAuth()
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiDataResponse(UserResponseDto)
|
||||
async getProfile(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const user = await this.userService.findUserOrThrow({ id: sub }, true);
|
||||
|
||||
return ResponseFactory.data(new UserResponseDto(user));
|
||||
}
|
||||
@Patch('')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async updateProfile(@AuthenticatedUser() user: IJwtPayload, @Body() data: UpdateUserRequestDto) {
|
||||
return this.userService.updateUser(user.sub, data);
|
||||
}
|
||||
|
||||
@Patch('email')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async updateEmail(@AuthenticatedUser() user: IJwtPayload, @Body() data: UpdateEmailRequestDto) {
|
||||
return this.userService.updateUserEmail(user.sub, data.email);
|
||||
}
|
||||
|
||||
@Patch('verify-email')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async verifyEmail(@AuthenticatedUser() user: IJwtPayload, @Body() { otp }: VerifyOtpRequestDto) {
|
||||
return this.userService.verifyEmail(user.sub, otp);
|
||||
}
|
||||
|
||||
@Patch('notifications-settings')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async updateNotificationSettings(
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Body() data: UpdateNotificationsSettingsRequestDto,
|
||||
@ -22,11 +54,4 @@ export class UserController {
|
||||
) {
|
||||
return this.userService.updateNotificationSettings(user.sub, data, deviceId);
|
||||
}
|
||||
|
||||
@Post('internal/set-password')
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async setPassword(@Body() data: SetInternalPasswordRequestDto) {
|
||||
return this.userService.setCheckerPassword(data);
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString, Matches } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
|
||||
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
||||
|
||||
export class CreateCheckerRequestDto {
|
||||
@ApiProperty({ example: 'checker@example.com' })
|
||||
@ApiProperty({ example: 'test@test.com' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: '+962' })
|
||||
@Matches(COUNTRY_CODE_REGEX, {
|
||||
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
|
||||
})
|
||||
countryCode!: string;
|
||||
|
||||
@ApiProperty({ example: '797229134' })
|
||||
@IsValidPhoneNumber({
|
||||
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
|
||||
})
|
||||
phoneNumber!: string;
|
||||
}
|
@ -1,4 +1,2 @@
|
||||
export * from './create-checker.request.dto';
|
||||
export * from './set-internal-password.request.dto';
|
||||
export * from './update-notifications-settings.request.dto';
|
||||
export * from './user-filters.request.dto';
|
||||
export * from './update-user.request.dto';
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
|
||||
export class SetInternalPasswordRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.token' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.token' }) })
|
||||
token!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
|
||||
password!: string;
|
||||
}
|
9
src/user/dtos/request/update-email.request.dto.ts
Normal file
9
src/user/dtos/request/update-email.request.dto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsOptional } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class UpdateEmailRequestDto {
|
||||
@ApiProperty({ example: 'test@test.com' })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'user.email' }) })
|
||||
@IsOptional()
|
||||
email!: string;
|
||||
}
|
21
src/user/dtos/request/update-user.request.dto.ts
Normal file
21
src/user/dtos/request/update-user.request.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class UpdateUserRequestDto {
|
||||
@ApiProperty({ example: 'John' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.firstName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'user.firstName' }) })
|
||||
@IsOptional()
|
||||
firstName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Doe' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.lastName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'user.lastName' }) })
|
||||
@IsOptional()
|
||||
lastName!: string;
|
||||
|
||||
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'user.profilePictureId' }) })
|
||||
@IsOptional()
|
||||
profilePictureId!: string;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
|
||||
export class UserFiltersRequestDto extends PageOptionsRequestDto {
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.search' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'user.search' }) })
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ description: 'Search by email or phone number' })
|
||||
search?: string;
|
||||
|
||||
@IsEnum(Roles, { message: i18n('validation.IsEnum', { path: 'general', property: 'user.role' }) })
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ enum: Roles, enumName: 'Roles', example: Roles.CHECKER, description: 'Role of the user' })
|
||||
role?: string;
|
||||
}
|
@ -3,6 +3,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
@ -21,7 +22,13 @@ export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true, name: 'email' })
|
||||
@Column('varchar', { length: 255, name: 'first_name', nullable: false })
|
||||
firstName!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'last_name', nullable: false })
|
||||
lastName!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'email', nullable: true })
|
||||
email!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'phone_number', nullable: true })
|
||||
@ -81,6 +88,13 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => UserRegistrationToken, (token) => token.user)
|
||||
registrationTokens!: UserRegistrationToken[];
|
||||
|
||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||
profilePictureId!: string;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.userPicture, { cascade: true, nullable: true })
|
||||
@JoinColumn({ name: 'profile_picture_id' })
|
||||
profilePicture!: Document;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { User } from '../../user/entities';
|
||||
import { UserFiltersRequestDto } from '../dtos/request';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
@ -17,7 +16,7 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne({ where });
|
||||
return this.userRepository.findOne({ where, relations: ['profilePicture'] });
|
||||
}
|
||||
|
||||
update(userId: string, data: Partial<User>) {
|
||||
@ -31,24 +30,4 @@ export class UserRepository {
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
findUsers(filters: UserFiltersRequestDto) {
|
||||
const queryBuilder = this.userRepository.createQueryBuilder('user');
|
||||
|
||||
if (filters.role) {
|
||||
queryBuilder.andWhere(`user.roles @> ARRAY[:role]`, { role: filters.role });
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
queryBuilder.andWhere(`user.email ILIKE :search OR user.phoneNumber ILIKE :search`, {
|
||||
search: `%${filters.search}%`,
|
||||
});
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('user.createdAt', 'DESC');
|
||||
queryBuilder.take(filters.size);
|
||||
queryBuilder.skip((filters.page - 1) * filters.size);
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,20 @@
|
||||
import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import moment from 'moment';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { CountryIso } from '~/common/enums';
|
||||
import { NotificationsService } from '~/common/modules/notification/services';
|
||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||
import { OtpService } from '~/common/modules/otp/services';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { DocumentService, OciService } from '~/document/services';
|
||||
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
|
||||
import { Roles } from '../../auth/enums';
|
||||
import {
|
||||
CreateCheckerRequestDto,
|
||||
SetInternalPasswordRequestDto,
|
||||
UpdateNotificationsSettingsRequestDto,
|
||||
UserFiltersRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { UpdateNotificationsSettingsRequestDto, UpdateUserRequestDto } from '../dtos/request';
|
||||
import { User } from '../entities';
|
||||
import { UserType } from '../enums';
|
||||
import { UserRepository } from '../repositories';
|
||||
import { DeviceService } from './device.service';
|
||||
import { UserTokenService } from './user-token.service';
|
||||
const SALT_ROUNDS = 10;
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@ -29,14 +24,21 @@ export class UserService {
|
||||
private readonly userRepository: UserRepository,
|
||||
@Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly userTokenService: UserTokenService,
|
||||
private readonly configService: ConfigService,
|
||||
private customerService: CustomerService,
|
||||
private readonly documentService: DocumentService,
|
||||
private readonly otpService: OtpService,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
async findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[], includeSignedUrl = false) {
|
||||
this.logger.log(`finding user with where clause ${JSON.stringify(where)}`);
|
||||
return this.userRepository.findOne(where);
|
||||
const user = await this.userRepository.findOne(where);
|
||||
|
||||
if (user?.profilePicture && includeSignedUrl) {
|
||||
user.profilePicture.url = await this.ociService.generatePreSignedUrl(user.profilePicture);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
setEmail(userId: string, email: string) {
|
||||
@ -75,14 +77,9 @@ export class UserService {
|
||||
]);
|
||||
}
|
||||
|
||||
findUsers(filters: UserFiltersRequestDto) {
|
||||
this.logger.log(`Getting users with filters ${JSON.stringify(filters)}`);
|
||||
return this.userRepository.findUsers(filters);
|
||||
}
|
||||
|
||||
async findUserOrThrow(where: FindOptionsWhere<User>) {
|
||||
async findUserOrThrow(where: FindOptionsWhere<User>, includeSignedUrl = false) {
|
||||
this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`);
|
||||
const user = await this.findUser(where);
|
||||
const user = await this.findUser(where, includeSignedUrl);
|
||||
|
||||
if (!user) {
|
||||
this.logger.error(`User with not found with where clause ${JSON.stringify(where)}`);
|
||||
@ -107,6 +104,8 @@ export class UserService {
|
||||
phoneNumber: body.phoneNumber,
|
||||
countryCode: body.countryCode,
|
||||
email: body.email,
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
roles: [Roles.GUARDIAN],
|
||||
});
|
||||
}
|
||||
@ -134,29 +133,6 @@ export class UserService {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async createChecker(data: CreateCheckerRequestDto) {
|
||||
const existingUser = await this.userRepository.findOne([
|
||||
{ email: data.email },
|
||||
{ phoneNumber: data.phoneNumber, countryCode: data.countryCode },
|
||||
]);
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('USER.ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
const user = await this.createUser({
|
||||
...data,
|
||||
roles: [Roles.CHECKER],
|
||||
isProfileCompleted: true,
|
||||
});
|
||||
const ONE_DAY = moment().add(1, 'day').toDate();
|
||||
const token = await this.userTokenService.generateToken(user.id, UserType.CHECKER, ONE_DAY);
|
||||
await this.sendCheckerAccountCreatedEmail(data.email, token);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId?: string) {
|
||||
this.logger.log(`Updating notification settings for user ${userId} with data ${JSON.stringify(data)}`);
|
||||
if (data.isPushEnabled && !data.fcmToken) {
|
||||
@ -213,16 +189,9 @@ export class UserService {
|
||||
return this.findUserOrThrow({ id: user.id });
|
||||
}
|
||||
|
||||
async setCheckerPassword(data: SetInternalPasswordRequestDto) {
|
||||
const userId = await this.userTokenService.validateToken(data.token, UserType.CHECKER);
|
||||
this.logger.log(`Setting password for checker ${userId}`);
|
||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||
const hashedPasscode = bcrypt.hashSync(data.password, salt);
|
||||
async updateUser(userId: string, data: UpdateUserRequestDto) {
|
||||
await this.validateProfilePictureId(data.profilePictureId, userId);
|
||||
|
||||
return this.userRepository.update(userId, { password: hashedPasscode, salt, isProfileCompleted: true });
|
||||
}
|
||||
|
||||
async updateUser(userId: string, data: Partial<User>) {
|
||||
this.logger.log(`Updating user ${userId} with data ${JSON.stringify(data)}`);
|
||||
const { affected } = await this.userRepository.update(userId, data);
|
||||
if (affected === 0) {
|
||||
@ -231,12 +200,72 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
private sendCheckerAccountCreatedEmail(email: string, token: string) {
|
||||
return this.notificationsService.sendEmailAsync({
|
||||
to: email,
|
||||
template: 'user-invite',
|
||||
subject: 'Checker Account Created',
|
||||
data: { inviteLink: `${this.adminPortalUrl}?token=${token}` },
|
||||
async updateUserEmail(userId: string, email: string) {
|
||||
const userWithEmail = await this.findUser({ email, isEmailVerified: true });
|
||||
|
||||
if (userWithEmail) {
|
||||
if (userWithEmail.id === userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.error(`Email ${email} is already taken by another user`);
|
||||
throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN');
|
||||
}
|
||||
|
||||
this.logger.log(`Updating email for user ${userId} to ${email}`);
|
||||
const { affected } = await this.userRepository.update(userId, { email, isEmailVerified: false });
|
||||
|
||||
if (affected === 0) {
|
||||
this.logger.error(`User with id ${userId} not found`);
|
||||
throw new BadRequestException('USER.NOT_FOUND');
|
||||
}
|
||||
|
||||
return this.otpService.generateAndSendOtp({
|
||||
userId,
|
||||
recipient: email,
|
||||
otpType: OtpType.EMAIL,
|
||||
scope: OtpScope.VERIFY_EMAIL,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyEmail(userId: string, otp: string) {
|
||||
this.logger.log(`Verifying email for user ${userId} with otp ${otp}`);
|
||||
const user = await this.findUserOrThrow({ id: userId });
|
||||
|
||||
if (user.isEmailVerified) {
|
||||
this.logger.error(`User with id ${userId} already has verified email`);
|
||||
throw new BadRequestException('USER.EMAIL_ALREADY_VERIFIED');
|
||||
}
|
||||
|
||||
await this.otpService.verifyOtp({
|
||||
userId,
|
||||
value: otp,
|
||||
scope: OtpScope.VERIFY_EMAIL,
|
||||
otpType: OtpType.EMAIL,
|
||||
});
|
||||
|
||||
await this.userRepository.update(userId, { isEmailVerified: true });
|
||||
this.logger.log(`Email for user ${userId} verified successfully`);
|
||||
}
|
||||
|
||||
private async validateProfilePictureId(profilePictureId: string, userId: string) {
|
||||
if (!profilePictureId) {
|
||||
return;
|
||||
}
|
||||
this.logger.log(`Validating profile picture id ${profilePictureId}`);
|
||||
|
||||
const document = await this.documentService.findDocumentById(profilePictureId);
|
||||
|
||||
if (!document) {
|
||||
this.logger.error(`Document with id ${profilePictureId} not found`);
|
||||
throw new BadRequestException('DOCUMENT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (document.createdById !== userId) {
|
||||
this.logger.error(`Document with id ${profilePictureId} does not belong to user ${userId}`);
|
||||
throw new BadRequestException('DOCUMENT.NOT_BELONG_TO_USER');
|
||||
}
|
||||
|
||||
this.logger.log(`Profile picture id ${profilePictureId} validated successfully`);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { NotificationModule } from '~/common/modules/notification/notification.module';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { AdminUserController, UserController } from './controllers';
|
||||
import { UserController } from './controllers';
|
||||
import { Device, User, UserRegistrationToken } from './entities';
|
||||
import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories';
|
||||
import { DeviceService, UserService, UserTokenService } from './services';
|
||||
@ -15,6 +15,6 @@ import { DeviceService, UserService, UserTokenService } from './services';
|
||||
],
|
||||
providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService],
|
||||
exports: [UserService, DeviceService, UserTokenService],
|
||||
controllers: [UserController, AdminUserController],
|
||||
controllers: [UserController],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
Reference in New Issue
Block a user