mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +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) {
|
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
|
||||||
const relations = withRelations
|
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 });
|
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
|
||||||
}
|
}
|
||||||
@ -43,7 +49,8 @@ export class AllowanceChangeRequestsRepository {
|
|||||||
'allowance',
|
'allowance',
|
||||||
'allowance.junior',
|
'allowance.junior',
|
||||||
'allowance.junior.customer',
|
'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) {
|
findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||||
return this.allowancesRepository.findOne({
|
return this.allowancesRepository.findOne({
|
||||||
where: { id: allowanceId, guardianId },
|
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) {
|
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||||
return this.allowancesRepository.findAndCount({
|
return this.allowancesRepository.findAndCount({
|
||||||
where: { guardianId },
|
where: { guardianId },
|
||||||
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'],
|
||||||
take: query.size,
|
take: query.size,
|
||||||
skip: query.size * (query.page - ONE),
|
skip: query.size * (query.page - ONE),
|
||||||
});
|
});
|
||||||
|
@ -122,7 +122,7 @@ export class AllowanceChangeRequestsService {
|
|||||||
this.logger.log(`Preparing allowance change requests images`);
|
this.logger.log(`Preparing allowance change requests images`);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
requests.map(async (request) => {
|
requests.map(async (request) => {
|
||||||
const profilePicture = request.allowance.junior.customer.profilePicture;
|
const profilePicture = request.allowance.junior.customer.user.profilePicture;
|
||||||
if (profilePicture) {
|
if (profilePicture) {
|
||||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ export class AllowancesService {
|
|||||||
this.logger.log(`Preparing document for allowances`);
|
this.logger.log(`Preparing document for allowances`);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
allowance.map(async (allowance) => {
|
allowance.map(async (allowance) => {
|
||||||
const profilePicture = allowance.junior.customer.profilePicture;
|
const profilePicture = allowance.junior.customer.user.profilePicture;
|
||||||
if (profilePicture) {
|
if (profilePicture) {
|
||||||
profilePicture.url = await this.ociService.generatePreSignedUrl(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' }) })
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
|
||||||
lastName!: string;
|
lastName!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '2021-01-01' })
|
@ApiProperty({ example: '2001-01-01' })
|
||||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
dateOfBirth!: Date;
|
dateOfBirth!: Date;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Roles } from '~/auth/enums';
|
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||||
import { User } from '~/user/entities';
|
import { User } from '~/user/entities';
|
||||||
|
|
||||||
export class UserResponseDto {
|
export class UserResponseDto {
|
||||||
@ -7,42 +7,39 @@ export class UserResponseDto {
|
|||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
email!: string;
|
countryCode!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
phoneNumber!: string;
|
phoneNumber!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
countryCode!: string;
|
email!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isPasswordSet!: boolean;
|
firstName!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isProfileCompleted!: boolean;
|
lastName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: DocumentMetaResponseDto, nullable: true })
|
||||||
|
profilePicture!: DocumentMetaResponseDto | null;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isSmsEnabled!: boolean;
|
isPhoneVerified!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isEmailEnabled!: boolean;
|
isEmailVerified!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
isPushEnabled!: boolean;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
roles!: Roles[];
|
|
||||||
|
|
||||||
constructor(user: User) {
|
constructor(user: User) {
|
||||||
this.id = user.id;
|
this.id = user.id;
|
||||||
this.email = user.email;
|
|
||||||
this.phoneNumber = user.phoneNumber;
|
|
||||||
this.countryCode = user.countryCode;
|
this.countryCode = user.countryCode;
|
||||||
this.isPasswordSet = user.isPasswordSet;
|
this.phoneNumber = user.phoneNumber;
|
||||||
this.isProfileCompleted = user.isProfileCompleted;
|
|
||||||
this.isSmsEnabled = user.isSmsEnabled;
|
this.email = user.email;
|
||||||
this.isEmailEnabled = user.isEmailEnabled;
|
this.firstName = user.firstName;
|
||||||
this.isPushEnabled = user.isPushEnabled;
|
this.lastName = user.lastName;
|
||||||
this.roles = user.roles;
|
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()
|
@IsOptional()
|
||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
|
|
||||||
@ApiProperty({ example: 'JO' })
|
@ApiProperty({ enum: CountryIso, example: CountryIso.SAUDI_ARABIA })
|
||||||
@IsEnum(CountryIso, {
|
@IsEnum(CountryIso, {
|
||||||
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.countryOfResidence' }),
|
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.countryOfResidence' }),
|
||||||
})
|
})
|
||||||
countryOfResidence!: CountryIso;
|
countryOfResidence!: CountryIso;
|
||||||
|
|
||||||
@ApiProperty({ example: '2021-01-01' })
|
@ApiProperty({ example: '2001-01-01' })
|
||||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
dateOfBirth!: Date;
|
dateOfBirth!: Date;
|
||||||
|
@ -104,7 +104,5 @@ export class CustomerResponseDto {
|
|||||||
this.neighborhood = customer.neighborhood;
|
this.neighborhood = customer.neighborhood;
|
||||||
this.street = customer.street;
|
this.street = customer.street;
|
||||||
this.building = customer.building;
|
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.isGuardian = customer.isGuardian;
|
||||||
this.civilIdFront = new DocumentMetaResponseDto(customer.civilIdFront);
|
this.civilIdFront = new DocumentMetaResponseDto(customer.civilIdFront);
|
||||||
this.civilIdBack = new DocumentMetaResponseDto(customer.civilIdBack);
|
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 })
|
@Column('varchar', { name: 'building', length: 255, nullable: true })
|
||||||
building!: string;
|
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' })
|
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'user_id' })
|
@JoinColumn({ name: 'user_id' })
|
||||||
user!: User;
|
user!: User;
|
||||||
|
@ -15,7 +15,7 @@ export class CustomerRepository {
|
|||||||
findOne(where: FindOptionsWhere<Customer>) {
|
findOne(where: FindOptionsWhere<Customer>) {
|
||||||
return this.customerRepository.findOne({
|
return this.customerRepository.findOne({
|
||||||
where,
|
where,
|
||||||
relations: ['profilePicture', 'user', 'civilIdFront', 'civilIdBack', 'cards'],
|
relations: ['user', 'civilIdFront', 'civilIdBack', 'cards'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,6 @@ export class CustomerRepository {
|
|||||||
|
|
||||||
findCustomers(filters: CustomerFiltersRequestDto) {
|
findCustomers(filters: CustomerFiltersRequestDto) {
|
||||||
const query = this.customerRepository.createQueryBuilder('customer');
|
const query = this.customerRepository.createQueryBuilder('customer');
|
||||||
query.leftJoinAndSelect('customer.profilePicture', 'profilePicture');
|
|
||||||
query.leftJoinAndSelect('customer.user', 'user');
|
query.leftJoinAndSelect('customer.user', 'user');
|
||||||
|
|
||||||
if (filters.name) {
|
if (filters.name) {
|
||||||
|
@ -30,6 +30,9 @@ export class CustomerService {
|
|||||||
this.logger.log(`Updating customer ${userId}`);
|
this.logger.log(`Updating customer ${userId}`);
|
||||||
|
|
||||||
await this.validateProfilePictureForCustomer(userId, data.profilePictureId);
|
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);
|
await this.customerRepository.updateCustomer(userId, data);
|
||||||
this.logger.log(`Customer ${userId} updated successfully`);
|
this.logger.log(`Customer ${userId} updated successfully`);
|
||||||
return this.findCustomerById(userId);
|
return this.findCustomerById(userId);
|
||||||
@ -52,9 +55,6 @@ export class CustomerService {
|
|||||||
throw new BadRequestException('CUSTOMER.NOT_FOUND');
|
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`);
|
this.logger.log(`Customer ${id} found successfully`);
|
||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
@ -101,8 +101,6 @@ export class CustomerService {
|
|||||||
throw new BadRequestException('CUSTOMER.ALREADY_EXISTS');
|
throw new BadRequestException('CUSTOMER.ALREADY_EXISTS');
|
||||||
}
|
}
|
||||||
|
|
||||||
// await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId);
|
|
||||||
|
|
||||||
const customer = await this.customerRepository.createCustomer(userId, body, true);
|
const customer = await this.customerRepository.createCustomer(userId, body, true);
|
||||||
this.logger.log(`customer created for user ${userId}`);
|
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.civilIdFront));
|
||||||
promises.push(this.ociService.generatePreSignedUrl(customer.civilIdBack));
|
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);
|
const [civilIdFrontUrl, civilIdBackUrl, profilePictureUrl] = await Promise.all(promises);
|
||||||
if (customer.profilePicture) {
|
|
||||||
customer.profilePicture.url = profilePictureUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
customer.civilIdFront.url = civilIdFrontUrl;
|
customer.civilIdFront.url = civilIdFrontUrl;
|
||||||
customer.civilIdBack.url = civilIdBackUrl;
|
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 './1753948642040-add-account-number-and-iban-to-account-entity';
|
||||||
export * from './1754210729273-add-vpan-to-card';
|
export * from './1754210729273-add-vpan-to-card';
|
||||||
export * from './1754226754947-add-upload-status-to-document-entity';
|
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' })
|
@Column({ type: 'uuid', nullable: true, name: 'created_by_id' })
|
||||||
createdById!: string;
|
createdById!: string;
|
||||||
|
|
||||||
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
|
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'SET NULL' })
|
||||||
customerPicture?: Customer;
|
userPicture?: Customer;
|
||||||
|
|
||||||
@OneToOne(() => Customer, (customer) => customer.civilIdFront, { onDelete: 'SET NULL' })
|
@OneToOne(() => Customer, (customer) => customer.civilIdFront, { onDelete: 'SET NULL' })
|
||||||
customerCivilIdFront?: User;
|
customerCivilIdFront?: User;
|
||||||
|
@ -51,7 +51,7 @@ export class OciService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bucketName = BUCKETS[document.documentType];
|
const bucketName = BUCKETS[document.documentType];
|
||||||
const objectName = document.name;
|
const objectName = document.name + document.extension;
|
||||||
const expiration = moment().add(TWO, 'hours').toDate();
|
const expiration = moment().add(TWO, 'hours').toDate();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -20,8 +20,8 @@ export class JuniorResponseDto {
|
|||||||
this.id = junior.id;
|
this.id = junior.id;
|
||||||
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
|
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
|
||||||
this.relationship = junior.relationship;
|
this.relationship = junior.relationship;
|
||||||
this.profilePicture = junior.customer.profilePicture
|
this.profilePicture = junior.customer.user.profilePicture
|
||||||
? new DocumentMetaResponseDto(junior.customer.profilePicture)
|
? new DocumentMetaResponseDto(junior.customer.user.profilePicture)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export class JuniorRepository {
|
|||||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||||
return this.juniorRepository.findAndCount({
|
return this.juniorRepository.findAndCount({
|
||||||
where: { guardianId },
|
where: { guardianId },
|
||||||
relations: ['customer', 'customer.user', 'customer.profilePicture'],
|
relations: ['customer', 'customer.user', 'customer.user.profilePicture'],
|
||||||
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
||||||
take: pageOptions.size,
|
take: pageOptions.size,
|
||||||
});
|
});
|
||||||
|
@ -132,7 +132,7 @@ export class JuniorService {
|
|||||||
this.logger.log(`Preparing junior images`);
|
this.logger.log(`Preparing junior images`);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
juniors.map(async (junior) => {
|
juniors.map(async (junior) => {
|
||||||
const profilePicture = junior.customer.profilePicture;
|
const profilePicture = junior.customer.user.profilePicture;
|
||||||
|
|
||||||
if (profilePicture) {
|
if (profilePicture) {
|
||||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
@ -34,7 +34,8 @@ export class MoneyRequestsRepository {
|
|||||||
const query = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
const query = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
||||||
query.leftJoinAndSelect('moneyRequest.requester', 'requester');
|
query.leftJoinAndSelect('moneyRequest.requester', 'requester');
|
||||||
query.leftJoinAndSelect('requester.customer', 'customer');
|
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.orderBy('moneyRequest.createdAt', 'DESC');
|
||||||
query.where('moneyRequest.reviewerId = :reviewerId', { reviewerId });
|
query.where('moneyRequest.reviewerId = :reviewerId', { reviewerId });
|
||||||
query.andWhere('moneyRequest.status = :status', { status: filters.status });
|
query.andWhere('moneyRequest.status = :status', { status: filters.status });
|
||||||
|
@ -106,7 +106,7 @@ export class MoneyRequestsService {
|
|||||||
this.logger.log(`Preparing document for money requests`);
|
this.logger.log(`Preparing document for money requests`);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
moneyRequests.map(async (moneyRequest) => {
|
moneyRequests.map(async (moneyRequest) => {
|
||||||
const profilePicture = moneyRequest.requester.customer.profilePicture;
|
const profilePicture = moneyRequest.requester.customer.user.profilePicture;
|
||||||
|
|
||||||
if (profilePicture) {
|
if (profilePicture) {
|
||||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
@ -36,7 +36,8 @@ export class TaskRepository {
|
|||||||
'image',
|
'image',
|
||||||
'assignedTo',
|
'assignedTo',
|
||||||
'assignedTo.customer',
|
'assignedTo.customer',
|
||||||
'assignedTo.customer.profilePicture',
|
'assignedTo.customer.user',
|
||||||
|
'assignedTo.customer.user.profilePicture',
|
||||||
'submission',
|
'submission',
|
||||||
'submission.proofOfCompletion',
|
'submission.proofOfCompletion',
|
||||||
],
|
],
|
||||||
@ -50,7 +51,8 @@ export class TaskRepository {
|
|||||||
.leftJoinAndSelect('task.image', 'image')
|
.leftJoinAndSelect('task.image', 'image')
|
||||||
.leftJoinAndSelect('task.assignedTo', 'assignedTo')
|
.leftJoinAndSelect('task.assignedTo', 'assignedTo')
|
||||||
.leftJoinAndSelect('assignedTo.customer', 'customer')
|
.leftJoinAndSelect('assignedTo.customer', 'customer')
|
||||||
.leftJoinAndSelect('customer.profilePicture', 'profilePicture')
|
.leftJoinAndSelect('customer.user', 'user')
|
||||||
|
.leftJoinAndSelect('user.profilePicture', 'profilePicture')
|
||||||
.leftJoinAndSelect('task.submission', 'submission')
|
.leftJoinAndSelect('task.submission', 'submission')
|
||||||
.leftJoinAndSelect('submission.proofOfCompletion', 'proofOfCompletion');
|
.leftJoinAndSelect('submission.proofOfCompletion', 'proofOfCompletion');
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ export class TaskService {
|
|||||||
const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([
|
const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([
|
||||||
this.ociService.generatePreSignedUrl(task.image),
|
this.ociService.generatePreSignedUrl(task.image),
|
||||||
this.ociService.generatePreSignedUrl(task.submission?.proofOfCompletion),
|
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;
|
task.image.url = imageUrl;
|
||||||
@ -141,8 +141,8 @@ export class TaskService {
|
|||||||
task.submission.proofOfCompletion.url = submissionUrl;
|
task.submission.proofOfCompletion.url = submissionUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.assignedTo.customer.profilePicture) {
|
if (task.assignedTo.customer.user.profilePicture) {
|
||||||
task.assignedTo.customer.profilePicture.url = profilePictureUrl;
|
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';
|
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 { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { VerifyOtpRequestDto } from '~/auth/dtos/request';
|
||||||
|
import { UserResponseDto } from '~/auth/dtos/response';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||||
import { AuthenticatedUser, Public } from '~/common/decorators';
|
import { AuthenticatedUser } from '~/common/decorators';
|
||||||
import { AccessTokenGuard } from '~/common/guards';
|
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';
|
import { UserService } from '../services';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('profile')
|
||||||
@ApiTags('Users')
|
@ApiTags('User - Profile')
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private userService: UserService) {}
|
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')
|
@Patch('notifications-settings')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async updateNotificationSettings(
|
async updateNotificationSettings(
|
||||||
@AuthenticatedUser() user: IJwtPayload,
|
@AuthenticatedUser() user: IJwtPayload,
|
||||||
@Body() data: UpdateNotificationsSettingsRequestDto,
|
@Body() data: UpdateNotificationsSettingsRequestDto,
|
||||||
@ -22,11 +54,4 @@ export class UserController {
|
|||||||
) {
|
) {
|
||||||
return this.userService.updateNotificationSettings(user.sub, data, deviceId);
|
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 './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,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -21,7 +22,13 @@ export class User extends BaseEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
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;
|
email!: string;
|
||||||
|
|
||||||
@Column('varchar', { length: 255, name: 'phone_number', nullable: true })
|
@Column('varchar', { length: 255, name: 'phone_number', nullable: true })
|
||||||
@ -81,6 +88,13 @@ export class User extends BaseEntity {
|
|||||||
@OneToMany(() => UserRegistrationToken, (token) => token.user)
|
@OneToMany(() => UserRegistrationToken, (token) => token.user)
|
||||||
registrationTokens!: UserRegistrationToken[];
|
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' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||||
import { User } from '../../user/entities';
|
import { User } from '../../user/entities';
|
||||||
import { UserFiltersRequestDto } from '../dtos/request';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository {
|
export class UserRepository {
|
||||||
@ -17,7 +16,7 @@ export class UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||||
return this.userRepository.findOne({ where });
|
return this.userRepository.findOne({ where, relations: ['profilePicture'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
update(userId: string, data: Partial<User>) {
|
update(userId: string, data: Partial<User>) {
|
||||||
@ -31,24 +30,4 @@ export class UserRepository {
|
|||||||
|
|
||||||
return this.userRepository.save(user);
|
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 { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import moment from 'moment';
|
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
import { Transactional } from 'typeorm-transactional';
|
import { Transactional } from 'typeorm-transactional';
|
||||||
import { CountryIso } from '~/common/enums';
|
import { CountryIso } from '~/common/enums';
|
||||||
import { NotificationsService } from '~/common/modules/notification/services';
|
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 { CustomerService } from '~/customer/services';
|
||||||
|
import { DocumentService, OciService } from '~/document/services';
|
||||||
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
|
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
|
||||||
import { Roles } from '../../auth/enums';
|
import { Roles } from '../../auth/enums';
|
||||||
import {
|
import { UpdateNotificationsSettingsRequestDto, UpdateUserRequestDto } from '../dtos/request';
|
||||||
CreateCheckerRequestDto,
|
|
||||||
SetInternalPasswordRequestDto,
|
|
||||||
UpdateNotificationsSettingsRequestDto,
|
|
||||||
UserFiltersRequestDto,
|
|
||||||
} from '../dtos/request';
|
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
import { UserType } from '../enums';
|
|
||||||
import { UserRepository } from '../repositories';
|
import { UserRepository } from '../repositories';
|
||||||
import { DeviceService } from './device.service';
|
import { DeviceService } from './device.service';
|
||||||
import { UserTokenService } from './user-token.service';
|
|
||||||
const SALT_ROUNDS = 10;
|
const SALT_ROUNDS = 10;
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@ -29,14 +24,21 @@ export class UserService {
|
|||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
@Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService,
|
@Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService,
|
||||||
private readonly deviceService: DeviceService,
|
private readonly deviceService: DeviceService,
|
||||||
private readonly userTokenService: UserTokenService,
|
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private customerService: CustomerService,
|
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)}`);
|
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) {
|
setEmail(userId: string, email: string) {
|
||||||
@ -75,14 +77,9 @@ export class UserService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
findUsers(filters: UserFiltersRequestDto) {
|
async findUserOrThrow(where: FindOptionsWhere<User>, includeSignedUrl = false) {
|
||||||
this.logger.log(`Getting users with filters ${JSON.stringify(filters)}`);
|
|
||||||
return this.userRepository.findUsers(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findUserOrThrow(where: FindOptionsWhere<User>) {
|
|
||||||
this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`);
|
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) {
|
if (!user) {
|
||||||
this.logger.error(`User with not found with where clause ${JSON.stringify(where)}`);
|
this.logger.error(`User with not found with where clause ${JSON.stringify(where)}`);
|
||||||
@ -107,6 +104,8 @@ export class UserService {
|
|||||||
phoneNumber: body.phoneNumber,
|
phoneNumber: body.phoneNumber,
|
||||||
countryCode: body.countryCode,
|
countryCode: body.countryCode,
|
||||||
email: body.email,
|
email: body.email,
|
||||||
|
firstName: body.firstName,
|
||||||
|
lastName: body.lastName,
|
||||||
roles: [Roles.GUARDIAN],
|
roles: [Roles.GUARDIAN],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -134,29 +133,6 @@ export class UserService {
|
|||||||
return user;
|
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) {
|
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId?: string) {
|
||||||
this.logger.log(`Updating notification settings for user ${userId} with data ${JSON.stringify(data)}`);
|
this.logger.log(`Updating notification settings for user ${userId} with data ${JSON.stringify(data)}`);
|
||||||
if (data.isPushEnabled && !data.fcmToken) {
|
if (data.isPushEnabled && !data.fcmToken) {
|
||||||
@ -213,16 +189,9 @@ export class UserService {
|
|||||||
return this.findUserOrThrow({ id: user.id });
|
return this.findUserOrThrow({ id: user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setCheckerPassword(data: SetInternalPasswordRequestDto) {
|
async updateUser(userId: string, data: UpdateUserRequestDto) {
|
||||||
const userId = await this.userTokenService.validateToken(data.token, UserType.CHECKER);
|
await this.validateProfilePictureId(data.profilePictureId, userId);
|
||||||
this.logger.log(`Setting password for checker ${userId}`);
|
|
||||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
|
||||||
const hashedPasscode = bcrypt.hashSync(data.password, salt);
|
|
||||||
|
|
||||||
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)}`);
|
this.logger.log(`Updating user ${userId} with data ${JSON.stringify(data)}`);
|
||||||
const { affected } = await this.userRepository.update(userId, data);
|
const { affected } = await this.userRepository.update(userId, data);
|
||||||
if (affected === 0) {
|
if (affected === 0) {
|
||||||
@ -231,12 +200,72 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendCheckerAccountCreatedEmail(email: string, token: string) {
|
async updateUserEmail(userId: string, email: string) {
|
||||||
return this.notificationsService.sendEmailAsync({
|
const userWithEmail = await this.findUser({ email, isEmailVerified: true });
|
||||||
to: email,
|
|
||||||
template: 'user-invite',
|
if (userWithEmail) {
|
||||||
subject: 'Checker Account Created',
|
if (userWithEmail.id === userId) {
|
||||||
data: { inviteLink: `${this.adminPortalUrl}?token=${token}` },
|
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 { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { NotificationModule } from '~/common/modules/notification/notification.module';
|
import { NotificationModule } from '~/common/modules/notification/notification.module';
|
||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
import { AdminUserController, UserController } from './controllers';
|
import { UserController } from './controllers';
|
||||||
import { Device, User, UserRegistrationToken } from './entities';
|
import { Device, User, UserRegistrationToken } from './entities';
|
||||||
import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories';
|
import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories';
|
||||||
import { DeviceService, UserService, UserTokenService } from './services';
|
import { DeviceService, UserService, UserTokenService } from './services';
|
||||||
@ -15,6 +15,6 @@ import { DeviceService, UserService, UserTokenService } from './services';
|
|||||||
],
|
],
|
||||||
providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService],
|
providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService],
|
||||||
exports: [UserService, DeviceService, UserTokenService],
|
exports: [UserService, DeviceService, UserTokenService],
|
||||||
controllers: [UserController, AdminUserController],
|
controllers: [UserController],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
Reference in New Issue
Block a user