feat: working on edit profile ticket

This commit is contained in:
Abdalhamid Alhamad
2025-08-05 17:53:38 +03:00
parent 1e2b859b92
commit 275984954e
37 changed files with 298 additions and 275 deletions

View File

@ -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',
], ],
}); });
} }

View File

@ -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),
}); });

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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;

View File

@ -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;
} }
} }

View File

@ -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;

View File

@ -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;
} }
} }

View File

@ -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;
} }
} }

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View 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"`);
}
}

View File

@ -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`,
);
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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;
} }
} }

View File

@ -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,
}); });

View File

@ -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);

View File

@ -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 });

View File

@ -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);

View File

@ -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');

View File

@ -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;
} }
}), }),
); );

View File

@ -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));
}
}

View File

@ -1,2 +1 @@
export * from './admin.user.controller';
export * from './user.controller'; export * from './user.controller';

View File

@ -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);
}
} }

View File

@ -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;
}

View File

@ -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';

View File

@ -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;
}

View 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;
}

View 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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}
} }

View File

@ -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`);
}
} }

View File

@ -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 {}