From 613f9de731e06aa095b423c0f462138a1766d3e8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:45:11 +0300 Subject: [PATCH 1/8] Refactor email sending method name to improve clarity --- libs/common/src/util/email.service.ts | 2 +- src/auth/services/user-auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts index 1b87334..f3a389d 100644 --- a/libs/common/src/util/email.service.ts +++ b/libs/common/src/util/email.service.ts @@ -19,7 +19,7 @@ export class EmailService { }; } - async sendOTPEmail( + async sendEmail( email: string, subject: string, message: string, diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index a514c1c..3c45794 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -136,7 +136,7 @@ export class UserAuthService { }); const subject = 'OTP send successfully'; const message = `Your OTP code is ${otpCode}`; - this.emailService.sendOTPEmail(data.email, subject, message); + this.emailService.sendEmail(data.email, subject, message); return otpCode; } From 39d3e18d9dd37b923ade063ab34062ae4c37e2a2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:45:33 +0300 Subject: [PATCH 2/8] Add Visitor Password Management Module --- src/vistor-password/controllers/index.ts | 1 + .../visitor-password.controller.ts | 125 +++ src/vistor-password/dtos/index.ts | 1 + src/vistor-password/dtos/temp-pass.dto.ts | 213 +++++ .../interfaces/visitor-password.interface.ts | 66 ++ src/vistor-password/services/index.ts | 1 + .../services/visitor-password.service.ts | 762 ++++++++++++++++++ .../visitor-password.module.ts | 20 + 8 files changed, 1189 insertions(+) create mode 100644 src/vistor-password/controllers/index.ts create mode 100644 src/vistor-password/controllers/visitor-password.controller.ts create mode 100644 src/vistor-password/dtos/index.ts create mode 100644 src/vistor-password/dtos/temp-pass.dto.ts create mode 100644 src/vistor-password/interfaces/visitor-password.interface.ts create mode 100644 src/vistor-password/services/index.ts create mode 100644 src/vistor-password/services/visitor-password.service.ts create mode 100644 src/vistor-password/visitor-password.module.ts diff --git a/src/vistor-password/controllers/index.ts b/src/vistor-password/controllers/index.ts new file mode 100644 index 0000000..ca4b986 --- /dev/null +++ b/src/vistor-password/controllers/index.ts @@ -0,0 +1 @@ +export * from './visitor-password.controller'; diff --git a/src/vistor-password/controllers/visitor-password.controller.ts b/src/vistor-password/controllers/visitor-password.controller.ts new file mode 100644 index 0000000..5e1e39d --- /dev/null +++ b/src/vistor-password/controllers/visitor-password.controller.ts @@ -0,0 +1,125 @@ +import { VisitorPasswordService } from '../services/visitor-password.service'; +import { + Body, + Controller, + Post, + HttpException, + HttpStatus, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { + AddDoorLockOfflineMultipleDto, + AddDoorLockOfflineOneTimeDto, + AddDoorLockOnlineMultipleDto, + AddDoorLockOnlineOneTimeDto, +} from '../dtos/temp-pass.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Visitor Password Module') +@Controller({ + version: '1', + path: 'visitor-password', +}) +export class VisitorPasswordController { + constructor( + private readonly visitorPasswordService: VisitorPasswordService, + ) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/online/multiple-time') + async addOnlineTemporaryPasswordMultipleTime( + @Body() addDoorLockOnlineMultipleDto: AddDoorLockOnlineMultipleDto, + ) { + try { + const temporaryPasswords = + await this.visitorPasswordService.addOnlineTemporaryPasswordMultipleTime( + addDoorLockOnlineMultipleDto, + ); + + return { + statusCode: HttpStatus.CREATED, + data: temporaryPasswords, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/online/one-time') + async addOnlineTemporaryPassword( + @Body() addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, + ) { + try { + const temporaryPasswords = + await this.visitorPasswordService.addOnlineTemporaryPasswordOneTime( + addDoorLockOnlineOneTimeDto, + ); + + return { + statusCode: HttpStatus.CREATED, + data: temporaryPasswords, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/offline/one-time') + async addOfflineOneTimeTemporaryPassword( + @Body() addDoorLockOfflineOneTimeDto: AddDoorLockOfflineOneTimeDto, + ) { + try { + const temporaryPassword = + await this.visitorPasswordService.addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineOneTimeDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'offline temporary password added successfully', + data: temporaryPassword, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/offline/multiple-time') + async addOfflineMultipleTimeTemporaryPassword( + @Body() + addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto, + ) { + try { + const temporaryPassword = + await this.visitorPasswordService.addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineMultipleDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'offline temporary password added successfully', + data: temporaryPassword, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/vistor-password/dtos/index.ts b/src/vistor-password/dtos/index.ts new file mode 100644 index 0000000..aa0f905 --- /dev/null +++ b/src/vistor-password/dtos/index.ts @@ -0,0 +1 @@ +export * from './temp-pass.dto'; diff --git a/src/vistor-password/dtos/temp-pass.dto.ts b/src/vistor-password/dtos/temp-pass.dto.ts new file mode 100644 index 0000000..0590935 --- /dev/null +++ b/src/vistor-password/dtos/temp-pass.dto.ts @@ -0,0 +1,213 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsEnum, + Length, + IsOptional, + IsEmail, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { WorkingDays } from '@app/common/constants/working-days'; + +class ScheduleDto { + @ApiProperty({ + description: 'effectiveTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public effectiveTime: string; + + @ApiProperty({ + description: 'invalidTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public invalidTime: string; + + @ApiProperty({ + description: 'workingDay', + enum: WorkingDays, + isArray: true, + required: true, + }) + @IsArray() + @IsEnum(WorkingDays, { each: true }) + @IsNotEmpty() + public workingDay: WorkingDays[]; +} + +export class AddDoorLockOnlineMultipleDto { + @ApiProperty({ + description: 'email', + required: true, + }) + @IsEmail() + @IsNotEmpty() + public email: string; + @ApiProperty({ + description: 'devicesUuid', + required: true, + }) + @IsArray() + @IsNotEmpty() + public devicesUuid: [string]; + + @ApiProperty({ + description: 'password name', + required: true, + }) + @IsString() + @IsNotEmpty() + public passwordName: string; + @ApiProperty({ + description: 'password', + required: true, + }) + @IsString() + @IsNotEmpty() + @Length(7, 7) + public password: string; + + @ApiProperty({ + description: 'effectiveTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public effectiveTime: string; + + @ApiProperty({ + description: 'invalidTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public invalidTime: string; + + @ApiProperty({ + description: 'scheduleList', + type: [ScheduleDto], + required: false, + }) + @IsArray() + @ValidateNested({ each: true }) + @IsOptional() + @Type(() => ScheduleDto) + public scheduleList: ScheduleDto[]; +} +export class AddDoorLockOnlineOneTimeDto { + @ApiProperty({ + description: 'email', + required: true, + }) + @IsEmail() + @IsNotEmpty() + public email: string; + @ApiProperty({ + description: 'devicesUuid', + required: true, + }) + @IsArray() + @IsNotEmpty() + public devicesUuid: [string]; + + @ApiProperty({ + description: 'password name', + required: true, + }) + @IsString() + @IsNotEmpty() + public passwordName: string; + @ApiProperty({ + description: 'password', + required: true, + }) + @IsString() + @IsNotEmpty() + @Length(7, 7) + public password: string; + + @ApiProperty({ + description: 'effectiveTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public effectiveTime: string; + + @ApiProperty({ + description: 'invalidTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public invalidTime: string; +} +export class AddDoorLockOfflineOneTimeDto { + @ApiProperty({ + description: 'email', + required: true, + }) + @IsEmail() + @IsNotEmpty() + public email: string; + @ApiProperty({ + description: 'password name', + required: true, + }) + @IsString() + @IsNotEmpty() + public passwordName: string; + @ApiProperty({ + description: 'devicesUuid', + required: true, + }) + @IsArray() + @IsNotEmpty() + public devicesUuid: [string]; +} +export class AddDoorLockOfflineMultipleDto { + @ApiProperty({ + description: 'email', + required: true, + }) + @IsEmail() + @IsNotEmpty() + public email: string; + @ApiProperty({ + description: 'devicesUuid', + required: true, + }) + @IsArray() + @IsNotEmpty() + public devicesUuid: [string]; + + @ApiProperty({ + description: 'password name', + required: true, + }) + @IsString() + @IsNotEmpty() + public passwordName: string; + + @ApiProperty({ + description: 'effectiveTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public effectiveTime: string; + + @ApiProperty({ + description: 'invalidTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public invalidTime: string; +} diff --git a/src/vistor-password/interfaces/visitor-password.interface.ts b/src/vistor-password/interfaces/visitor-password.interface.ts new file mode 100644 index 0000000..6dda2eb --- /dev/null +++ b/src/vistor-password/interfaces/visitor-password.interface.ts @@ -0,0 +1,66 @@ +import { WorkingDays } from '@app/common/constants/working-days'; + +export interface createTickInterface { + success: boolean; + result: { + expire_time: number; + ticket_id: string; + ticket_key: string; + id?: number; + offline_temp_password_id?: string; + offline_temp_password?: string; + }; + msg?: string; +} +export interface addDeviceObjectInterface { + passwordName: string; + encryptedPassword: string; + effectiveTime: string; + invalidTime: string; + ticketId: string; + scheduleList?: any[]; +} + +export interface ScheduleDto { + effectiveTime: string; + invalidTime: string; + workingDay: WorkingDays[]; +} + +export interface AddDoorLockOnlineInterface { + passwordName: string; + password: string; + effectiveTime: string; + invalidTime: string; + scheduleList?: ScheduleDto[]; +} +export interface getPasswordInterface { + success: boolean; + result: [ + { + effective_time: number; + id: number; + invalid_time: number; + passwordName: string; + phase: number; + phone: string; + schedule_list?: []; + sn: number; + time_zone: string; + type: number; + }, + ]; + msg?: string; +} +export interface deleteTemporaryPasswordInterface { + success: boolean; + result: boolean; + msg?: string; +} +export interface getPasswordOfflineInterface { + success: boolean; + result: { + records: []; + }; + msg?: string; +} diff --git a/src/vistor-password/services/index.ts b/src/vistor-password/services/index.ts new file mode 100644 index 0000000..14d8814 --- /dev/null +++ b/src/vistor-password/services/index.ts @@ -0,0 +1 @@ +export * from './visitor-password.service'; diff --git a/src/vistor-password/services/visitor-password.service.ts b/src/vistor-password/services/visitor-password.service.ts new file mode 100644 index 0000000..66da260 --- /dev/null +++ b/src/vistor-password/services/visitor-password.service.ts @@ -0,0 +1,762 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { + addDeviceObjectInterface, + createTickInterface, +} from '../interfaces/visitor-password.interface'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProductType } from '@app/common/constants/product-type.enum'; + +import { + AddDoorLockOfflineMultipleDto, + AddDoorLockOfflineOneTimeDto, + AddDoorLockOnlineMultipleDto, + AddDoorLockOnlineOneTimeDto, +} from '../dtos'; +import { EmailService } from '@app/common/util/email.service'; +import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; + +@Injectable() +export class VisitorPasswordService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly deviceRepository: DeviceRepository, + private readonly emailService: EmailService, + private readonly passwordEncryptionService: PasswordEncryptionService, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto, + ) { + try { + const deviceResults = await Promise.allSettled( + addDoorLockOfflineMultipleDto.devicesUuid.map(async (deviceUuid) => { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); + } + + const createMultipleOfflinePass = + await this.addOfflineTemporaryPasswordTuya( + deviceDetails.deviceTuyaUuid, + 'multiple', + addDoorLockOfflineMultipleDto, + addDoorLockOfflineMultipleDto.passwordName, + ); + + if (!createMultipleOfflinePass.success) { + throw new HttpException( + createMultipleOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + + // Send email if the password creation is successful + if (createMultipleOfflinePass.result.offline_temp_password_id) { + const emailSubject = 'One-Time Password Creation Success'; + const emailBody = `Your multiple time offline temporary password "${createMultipleOfflinePass.result.offline_temp_password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`; + + await this.emailService.sendEmail( + addDoorLockOfflineMultipleDto.email, + emailSubject, + emailBody, + ); + } + + return { + success: true, + result: createMultipleOfflinePass.result, + deviceUuid, + }; + } catch (error) { + return { + success: false, + deviceUuid, + error: error.message || 'Error processing device', + }; + } + }), + ); + + // Process results: separate successful and failed operations + const successfulResults = deviceResults + .filter( + (result) => + result.status === 'fulfilled' && + (result as PromiseFulfilledResult).value.success, + ) + .map((result) => (result as PromiseFulfilledResult).value); + + const failedResults = deviceResults + .filter( + (result) => + result.status === 'rejected' || + !(result as PromiseFulfilledResult).value.success, + ) + .map((result) => + result.status === 'rejected' + ? { + success: false, + error: + (result as PromiseRejectedResult).reason.message || + 'Error processing device', + } + : (result as PromiseFulfilledResult).value, + ); + + // Return results if there are successful operations + if (successfulResults.length > 0) { + return { + successOperations: successfulResults, + failedOperations: failedResults, + }; + } + + // If all failed, throw an error with all failure messages + throw new HttpException( + failedResults.map((res) => res.error).join('; ') || + 'All device operations failed', + HttpStatus.BAD_REQUEST, + ); + } catch (error) { + console.error(error); + + throw new HttpException( + error.message || + 'Error adding offline multiple-time temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineOneTimeDto: AddDoorLockOfflineOneTimeDto, + ) { + try { + const deviceResults = await Promise.allSettled( + addDoorLockOfflineOneTimeDto.devicesUuid.map(async (deviceUuid) => { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); + } + + const createOnceOfflinePass = + await this.addOfflineTemporaryPasswordTuya( + deviceDetails.deviceTuyaUuid, + 'once', + null, + addDoorLockOfflineOneTimeDto.passwordName, + ); + + if (!createOnceOfflinePass.success) { + throw new HttpException( + createOnceOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + + // Send email if the password creation is successful + if (createOnceOfflinePass.result.offline_temp_password_id) { + const emailSubject = 'One-Time Password Creation Success'; + const emailBody = `Your one-time offline temporary password "${createOnceOfflinePass.result.offline_temp_password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`; + + await this.emailService.sendEmail( + addDoorLockOfflineOneTimeDto.email, + emailSubject, + emailBody, + ); + } + + return { + success: true, + result: createOnceOfflinePass.result, + deviceUuid, + }; + } catch (error) { + return { + success: false, + deviceUuid, + error: error.message || 'Error processing device', + }; + } + }), + ); + + // Process results: separate successful and failed operations + const successfulResults = deviceResults + .filter( + (result) => + result.status === 'fulfilled' && + (result as PromiseFulfilledResult).value.success, + ) + .map((result) => (result as PromiseFulfilledResult).value); + + const failedResults = deviceResults + .filter( + (result) => + result.status === 'rejected' || + !(result as PromiseFulfilledResult).value.success, + ) + .map((result) => + result.status === 'rejected' + ? { + success: false, + error: + (result as PromiseRejectedResult).reason.message || + 'Error processing device', + } + : (result as PromiseFulfilledResult).value, + ); + + // Return results if there are successful operations + if (successfulResults.length > 0) { + return { + successOperations: successfulResults, + failedOperations: failedResults, + }; + } + + // If all failed, throw an error with all failure messages + throw new HttpException( + failedResults.map((res) => res.error).join('; ') || + 'All device operations failed', + HttpStatus.BAD_REQUEST, + ); + } catch (error) { + console.error(error); + + throw new HttpException( + error.message || + 'Error adding offline one-time temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addOfflineTemporaryPasswordTuya( + doorLockUuid: string, + type: string, + addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto, + passwordName: string, + ): Promise { + try { + const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`; + + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + ...(type === 'multiple' && { + effective_time: addDoorLockOfflineMultipleDto.effectiveTime, + invalid_time: addDoorLockOfflineMultipleDto.invalidTime, + }), + name: passwordName, + type, + }, + }); + + return response as createTickInterface; + } catch (error) { + throw new HttpException( + 'Error adding offline temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOnlineTemporaryPasswordMultipleTime( + addDoorLockOnlineMultipleDto: AddDoorLockOnlineMultipleDto, + ) { + try { + const deviceResults = await Promise.allSettled( + addDoorLockOnlineMultipleDto.devicesUuid.map(async (deviceUuid) => { + try { + const passwordData = await this.getTicketAndEncryptedPassword( + deviceUuid, + addDoorLockOnlineMultipleDto.password, + ); + + if ( + !passwordData.ticketKey || + !passwordData.encryptedPassword || + !passwordData.deviceTuyaUuid + ) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + const addDeviceObj: addDeviceObjectInterface = { + ...addDoorLockOnlineMultipleDto, + ...passwordData, + }; + + const createPass = + await this.addOnlineTemporaryPasswordMultipleTuya( + addDeviceObj, + passwordData.deviceTuyaUuid, + ); + + if (!createPass.success) { + throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST); + } + + // Send email if the password creation is successful + if (createPass.result.id) { + const emailSubject = 'Password Creation Success'; + const emailBody = `Your temporary password "${addDoorLockOnlineMultipleDto.password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`; + + await this.emailService.sendEmail( + addDoorLockOnlineMultipleDto.email, + emailSubject, + emailBody, + ); + } + + return { + success: true, + id: createPass.result.id, + deviceUuid, + }; + } catch (error) { + return { + success: false, + deviceUuid, + error: error.message || 'Error processing device', + }; + } + }), + ); + + // Process results: separate successful and failed operations + const successfulResults = deviceResults + .filter( + (result) => + result.status === 'fulfilled' && + (result as PromiseFulfilledResult).value.success, + ) + .map((result) => (result as PromiseFulfilledResult).value); + + const failedResults = deviceResults + .filter( + (result) => + result.status === 'rejected' || + !(result as PromiseFulfilledResult).value.success, + ) + .map((result) => + result.status === 'rejected' + ? { + success: false, + error: + (result as PromiseRejectedResult).reason.message || + 'Error processing device', + } + : (result as PromiseFulfilledResult).value, + ); + + // Return results if there are successful operations + if (successfulResults.length > 0) { + return { + successOperations: successfulResults, + failedOperations: failedResults, + }; + } + + // Throw an error if all operations failed + throw new HttpException( + failedResults.map((res) => res.error).join('; ') || + 'All device operations failed', + HttpStatus.BAD_REQUEST, + ); + } catch (error) { + console.error(error); + + throw new HttpException( + error.message || 'Error adding online temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOnlineTemporaryPasswordOneTime( + addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, + ) { + try { + const deviceResults = await Promise.allSettled( + addDoorLockOnlineOneTimeDto.devicesUuid.map(async (deviceUuid) => { + try { + const passwordData = await this.getTicketAndEncryptedPassword( + deviceUuid, + addDoorLockOnlineOneTimeDto.password, + ); + + if ( + !passwordData.ticketKey || + !passwordData.encryptedPassword || + !passwordData.deviceTuyaUuid + ) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + const addDeviceObj: addDeviceObjectInterface = { + ...addDoorLockOnlineOneTimeDto, + ...passwordData, + }; + + const createPass = await this.addOnlineTemporaryPasswordOneTimeTuya( + addDeviceObj, + passwordData.deviceTuyaUuid, + ); + + if (!createPass.success) { + throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST); + } + + // Send email if the password creation is successful + if (createPass.result.id) { + const emailSubject = 'Password Creation Success'; + const emailBody = `Your temporary password "${addDoorLockOnlineOneTimeDto.password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`; + + await this.emailService.sendEmail( + addDoorLockOnlineOneTimeDto.email, + emailSubject, + emailBody, + ); + } + + return { + success: true, + id: createPass.result.id, + deviceUuid, + }; + } catch (error) { + return { + success: false, + deviceUuid, + error: error.message || 'Error processing device', + }; + } + }), + ); + + // Process results: separate successful and failed operations + const successfulResults = deviceResults + .filter( + (result) => + result.status === 'fulfilled' && + (result as PromiseFulfilledResult).value.success, + ) + .map((result) => (result as PromiseFulfilledResult).value); + + const failedResults = deviceResults + .filter( + (result) => + result.status === 'rejected' || + !(result as PromiseFulfilledResult).value.success, + ) + .map((result) => + result.status === 'rejected' + ? { + success: false, + error: + (result as PromiseRejectedResult).reason.message || + 'Error processing device', + } + : (result as PromiseFulfilledResult).value, + ); + + // Return results if there are successful operations + if (successfulResults.length > 0) { + return { + successOperations: successfulResults, + failedOperations: failedResults, + }; + } + + // Throw an error if all operations failed + throw new HttpException( + failedResults.map((res) => res.error).join('; ') || + 'All device operations failed', + HttpStatus.BAD_REQUEST, + ); + } catch (error) { + console.error(error); + + throw new HttpException( + error.message || 'Error adding online temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getTicketAndEncryptedPassword( + doorLockUuid: string, + passwordPlan: string, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); + } + const ticketDetails = await this.createDoorLockTicketTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (!ticketDetails.result.ticket_id || !ticketDetails.result.ticket_key) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const decrypted = + this.passwordEncryptionService.generateEncryptedPassword( + passwordPlan, + ticketDetails.result.ticket_key, + ); + return { + ticketId: ticketDetails.result.ticket_id, + ticketKey: ticketDetails.result.ticket_key, + encryptedPassword: decrypted, + deviceTuyaUuid: deviceDetails.deviceTuyaUuid, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error processing the request', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async createDoorLockTicketTuya( + deviceUuid: string, + ): Promise { + try { + const path = `/v1.0/smart-lock/devices/${deviceUuid}/password-ticket`; + const response = await this.tuya.request({ + method: 'POST', + path, + }); + + return response as createTickInterface; + } catch (error) { + throw new HttpException( + 'Error creating door lock ticket from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addOnlineTemporaryPasswordMultipleTuya( + addDeviceObj: addDeviceObjectInterface, + doorLockUuid: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; + let scheduleList; + if (addDeviceObj.scheduleList.length > 0) { + scheduleList = addDeviceObj.scheduleList.map((schedule) => ({ + effective_time: this.timeToMinutes(schedule.effectiveTime), + invalid_time: this.timeToMinutes(schedule.invalidTime), + working_day: this.getWorkingDayValue(schedule.workingDay), + })); + } + + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: addDeviceObj.passwordName, + password: addDeviceObj.encryptedPassword, + effective_time: addDeviceObj.effectiveTime, + invalid_time: addDeviceObj.invalidTime, + password_type: 'ticket', + ticket_id: addDeviceObj.ticketId, + ...(addDeviceObj.scheduleList.length > 0 && { + schedule_list: scheduleList, + }), + + type: '0', + }, + }); + console.log('response', response); + + return response as createTickInterface; + } catch (error) { + console.log('error', error); + + throw new HttpException( + error.msg || 'Error adding online temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + getWorkingDayValue(days) { + // Array representing the days of the week + const weekDays = ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun']; + + // Initialize a binary string with 7 bits + let binaryString = '0000000'; + + // Iterate through the input array and update the binary string + days.forEach((day) => { + const index = weekDays.indexOf(day); + if (index !== -1) { + // Set the corresponding bit to '1' + binaryString = + binaryString.substring(0, index) + + '1' + + binaryString.substring(index + 1); + } + }); + + // Convert the binary string to an integer + const workingDayValue = parseInt(binaryString, 2); + + return workingDayValue; + } + getDaysFromWorkingDayValue(workingDayValue) { + // Array representing the days of the week + const weekDays = ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun']; + + // Convert the integer to a binary string and pad with leading zeros to ensure 7 bits + const binaryString = workingDayValue.toString(2).padStart(7, '0'); + + // Initialize an array to hold the days of the week + const days = []; + + // Iterate through the binary string and weekDays array + for (let i = 0; i < binaryString.length; i++) { + if (binaryString[i] === '1') { + days.push(weekDays[i]); + } + } + + return days; + } + timeToMinutes(timeStr) { + try { + // Special case for "24:00" + if (timeStr === '24:00') { + return 1440; + } + + // Regular expression to validate the 24-hour time format (HH:MM) + const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/; + const match = timeStr.match(timePattern); + + if (!match) { + throw new Error('Invalid time format'); + } + + // Extract hours and minutes from the matched groups + const hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + + // Calculate the total minutes + const totalMinutes = hours * 60 + minutes; + + return totalMinutes; + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + minutesToTime(totalMinutes) { + try { + if ( + typeof totalMinutes !== 'number' || + totalMinutes < 0 || + totalMinutes > 1440 + ) { + throw new Error('Invalid minutes value'); + } + + if (totalMinutes === 1440) { + return '24:00'; + } + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + const formattedHours = String(hours).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + + return `${formattedHours}:${formattedMinutes}`; + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { + try { + return await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + ...(withProductDevice && { relations: ['productDevice'] }), + }); + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + } + async addOnlineTemporaryPasswordOneTimeTuya( + addDeviceObj: addDeviceObjectInterface, + doorLockUuid: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: addDeviceObj.passwordName, + password: addDeviceObj.encryptedPassword, + effective_time: addDeviceObj.effectiveTime, + invalid_time: addDeviceObj.invalidTime, + password_type: 'ticket', + ticket_id: addDeviceObj.ticketId, + type: '1', + }, + }); + console.log('response', response); + + return response as createTickInterface; + } catch (error) { + console.log('error', error); + + throw new HttpException( + error.msg || 'Error adding online temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts new file mode 100644 index 0000000..50ea9fb --- /dev/null +++ b/src/vistor-password/visitor-password.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { VisitorPasswordService } from './services/visitor-password.service'; +import { VisitorPasswordController } from './controllers/visitor-password.controller'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { EmailService } from '@app/common/util/email.service'; +import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [VisitorPasswordController], + providers: [ + VisitorPasswordService, + EmailService, + PasswordEncryptionService, + DeviceRepository, + ], + exports: [VisitorPasswordService], +}) +export class VisitorPasswordModule {} From da1117f432694d2adee4831df2dcf960223b2e8d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:45:52 +0300 Subject: [PATCH 3/8] Add VisitorPasswordModule to AppModule --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 88c0550..2970185 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { LoggingInterceptor } from './interceptors/logging.interceptor'; import { AutomationModule } from './automation/automation.module'; import { RegionModule } from './region/region.module'; import { TimeZoneModule } from './timezone/timezone.module'; +import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -48,6 +49,7 @@ import { TimeZoneModule } from './timezone/timezone.module'; DoorLockModule, RegionModule, TimeZoneModule, + VisitorPasswordModule, ], controllers: [AuthenticationController], providers: [ From 0b7a87b6bd52f140b94efe2d59f39052b398b515 Mon Sep 17 00:00:00 2001 From: yousef-alkhrissat Date: Sun, 11 Aug 2024 23:02:37 +0300 Subject: [PATCH 4/8] added devices and passwords of devices --- src/door-lock/services/door.lock.service.ts | 52 ++++++++++++++++ .../visitor-password.controller.ts | 23 ++++++++ .../services/visitor-password.service.ts | 59 +++++++++++++++++++ .../visitor-password.module.ts | 3 +- 4 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index a5892a6..0d90822 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -192,6 +192,58 @@ export class DoorLockService { ); } } + + async getOnlineTemporaryPasswordsOneTime(doorLockUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); + } + const passwords = await this.getOnlineTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result + .filter((item) => item.type === 1) + .map((password: any) => { + if (password.schedule_list?.length > 0) { + password.schedule_list = password.schedule_list.map( + (schedule) => { + schedule.working_day = this.getDaysFromWorkingDayValue( + schedule.working_day, + ); + schedule.effective_time = this.minutesToTime( + schedule.effective_time, + ); + schedule.invalid_time = this.minutesToTime( + schedule.invalid_time, + ); + return schedule; + }, + ); + } + return password; + }); + + return convertKeysToCamelCase(passwordFiltered); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || 'Error getting online temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOnlineTemporaryPasswordsTuya( doorLockUuid: string, ): Promise { diff --git a/src/vistor-password/controllers/visitor-password.controller.ts b/src/vistor-password/controllers/visitor-password.controller.ts index 5e1e39d..c0f09b6 100644 --- a/src/vistor-password/controllers/visitor-password.controller.ts +++ b/src/vistor-password/controllers/visitor-password.controller.ts @@ -6,6 +6,7 @@ import { HttpException, HttpStatus, UseGuards, + Get, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { @@ -122,4 +123,26 @@ export class VisitorPasswordController { ); } } + @Get('') + async GetVisitorPassword() { + try { + return await this.visitorPasswordService.getPasswords(); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @Get('/devices') + async GetVisitorDevices() { + try { + return await this.visitorPasswordService.getAllPassDevices(); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/vistor-password/services/visitor-password.service.ts b/src/vistor-password/services/visitor-password.service.ts index 66da260..2e64faa 100644 --- a/src/vistor-password/services/visitor-password.service.ts +++ b/src/vistor-password/services/visitor-password.service.ts @@ -16,6 +16,7 @@ import { } from '../dtos'; import { EmailService } from '@app/common/util/email.service'; import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; +import { DoorLockService } from 'src/door-lock/services'; @Injectable() export class VisitorPasswordService { @@ -24,6 +25,7 @@ export class VisitorPasswordService { private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, private readonly emailService: EmailService, + private readonly doorLockService: DoorLockService, private readonly passwordEncryptionService: PasswordEncryptionService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -403,6 +405,63 @@ export class VisitorPasswordService { ); } } + async getPasswords() { + const deviceIds = await this.deviceRepository.find({ + where: { + productDevice: { + prodType: ProductType.DL, + }, + }, + }); + const data = []; + deviceIds.forEach((deviceId) => { + data.push( + this.doorLockService + .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid) + .catch(() => {}), + this.doorLockService + .getOnlineTemporaryPasswordsOneTime(deviceId.uuid) + .catch(() => {}), + this.doorLockService + .getOnlineTemporaryPasswords(deviceId.uuid) + .catch(() => {}), + this.doorLockService + .getOfflineOneTimeTemporaryPasswords(deviceId.uuid) + .catch(() => {}), + ); + }); + return (await Promise.all(data)).flat().map((item) => { + return { + id: item.pwdId || item.id || null, + name: item.pwdName || item.name || '', + type: item.pwdTypeCode || item.type || '', + startTime: item.gmtStart || item.effectiveTime || null, + endTime: item.gmtExpired || item.invalidTime || null, + status: item.status || item.success || null, + additionalInfo: { + optUid: item.optUid || null, + hasClearPwd: item.hasClearPwd || false, + phase: item.phase || null, + phone: item.phone || '', + timeZone: item.timeZone || '', + result: item.result || null, + tid: item.tid || null, + t: item.t || null, + }, + }; + }); + } + + async getAllPassDevices() { + return await this.deviceRepository.find({ + where: { + productDevice: { + prodType: ProductType.DL, + }, + }, + }); + } + async addOnlineTemporaryPasswordOneTime( addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, ) { diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 50ea9fb..51239f4 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -6,8 +6,9 @@ import { DeviceRepositoryModule } from '@app/common/modules/device'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { EmailService } from '@app/common/util/email.service'; import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; +import { DoorLockModule } from 'src/door-lock/door.lock.module'; @Module({ - imports: [ConfigModule, DeviceRepositoryModule], + imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], providers: [ VisitorPasswordService, From 4d7ea8c9bd402ae1c6dce340e9fe5078b7b960f3 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 12 Aug 2024 15:37:13 +0300 Subject: [PATCH 5/8] added password return data --- src/door-lock/services/door.lock.service.ts | 114 ++++++++++++++---- .../visitor-password.controller.ts | 4 +- .../services/visitor-password.service.ts | 29 +---- 3 files changed, 101 insertions(+), 46 deletions(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index 0d90822..7a3cd72 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -1,4 +1,9 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { @@ -83,7 +88,10 @@ export class DoorLockService { ); } } - async getOfflineMultipleTimeTemporaryPasswords(doorLockUuid: string) { + async getOfflineMultipleTimeTemporaryPasswords( + doorLockUuid: string, + fromVisitor?: boolean, + ) { try { const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); @@ -99,11 +107,25 @@ export class DoorLockService { deviceDetails.deviceTuyaUuid, 'multiple', ); - - if (passwords.result.records.length > 0) { - return convertKeysToCamelCase(passwords.result.records); + if (!passwords.result.records.length && fromVisitor) { + throw new BadRequestException(); + } + if (passwords.result.records.length > 0) { + return fromVisitor + ? convertKeysToCamelCase(passwords.result.records).map((password) => { + return { + passwodId: `${password.pwdId}`, + passwodExpired: `${password.gmtExpired}`, + passwordStart: `${password.gmtStart}`, + passwordCreated: `${password.gmtCreate}`, + passwodName: password.pwdName, + passwordStatus: password.status, + passwordType: 'OFFLINE_MULTIPLE', + deviceUuid: doorLockUuid, + }; + }) + : convertKeysToCamelCase(passwords.result.records); } - return passwords; } catch (error) { throw new HttpException( @@ -113,10 +135,12 @@ export class DoorLockService { ); } } - async getOfflineOneTimeTemporaryPasswords(doorLockUuid: string) { + async getOfflineOneTimeTemporaryPasswords( + doorLockUuid: string, + fromVisitor?: boolean, + ) { try { const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); - if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { @@ -129,9 +153,24 @@ export class DoorLockService { deviceDetails.deviceTuyaUuid, 'once', ); - + if (!passwords.result.records.length && fromVisitor) { + throw new BadRequestException(); + } if (passwords.result.records.length > 0) { - return convertKeysToCamelCase(passwords.result.records); + return fromVisitor + ? convertKeysToCamelCase(passwords.result.records).map((password) => { + return { + passwodId: `${password.pwdId}`, + passwodExpired: `${password.gmtExpired}`, + passwordStart: `${password.gmtStart}`, + passwordCreated: `${password.gmtCreate}`, + passwodName: password.pwdName, + passwordStatus: password.status, + passwordType: 'OFFLINE_ONETIME', + deviceUuid: doorLockUuid, + }; + }) + : convertKeysToCamelCase(passwords.result.records); } return passwords; @@ -142,7 +181,10 @@ export class DoorLockService { ); } } - async getOnlineTemporaryPasswords(doorLockUuid: string) { + async getOnlineTemporaryPasswords( + doorLockUuid: string, + fromVisitor?: boolean, + ) { try { const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); @@ -157,8 +199,7 @@ export class DoorLockService { const passwords = await this.getOnlineTemporaryPasswordsTuya( deviceDetails.deviceTuyaUuid, ); - - if (passwords.result.length > 0) { + if (passwords.result?.length > 0) { const passwordFiltered = passwords.result .filter((item) => item.type === 0) .map((password: any) => { @@ -181,9 +222,24 @@ export class DoorLockService { return password; }); - return convertKeysToCamelCase(passwordFiltered); + return fromVisitor + ? convertKeysToCamelCase(passwordFiltered).map((password) => { + return { + passwodId: `${password.id}`, + passwodExpired: `${password.invalidTime}`, + passwordStart: `${password.effectiveTime}`, + passwordCreated: '', + passwodName: password.name, + passwordStatus: '', + passwordType: 'ONLINE_MULTIPLE', + deviceUuid: doorLockUuid, + }; + }) + : convertKeysToCamelCase(passwordFiltered); + } + if (fromVisitor) { + throw new BadRequestException(); } - return passwords; } catch (error) { throw new HttpException( @@ -192,8 +248,10 @@ export class DoorLockService { ); } } - - async getOnlineTemporaryPasswordsOneTime(doorLockUuid: string) { + async getOnlineTemporaryPasswordsOneTime( + doorLockUuid: string, + fromVisitor?: boolean, + ) { try { const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); @@ -208,8 +266,7 @@ export class DoorLockService { const passwords = await this.getOnlineTemporaryPasswordsTuya( deviceDetails.deviceTuyaUuid, ); - - if (passwords.result.length > 0) { + if (passwords.result?.length > 0) { const passwordFiltered = passwords.result .filter((item) => item.type === 1) .map((password: any) => { @@ -232,9 +289,24 @@ export class DoorLockService { return password; }); - return convertKeysToCamelCase(passwordFiltered); + return fromVisitor + ? convertKeysToCamelCase(passwordFiltered).map((password) => { + return { + passwodId: `${password.id}`, + passwodExpired: `${password.invalidTime}`, + passwordStart: `${password.effectiveTime}`, + passwordCreated: '', + passwodName: password.name, + passwordStatus: '', + passwordType: 'ONLINE_ONETIME', + deviceUuid: doorLockUuid, + }; + }) + : convertKeysToCamelCase(passwordFiltered); + } + if (fromVisitor) { + throw new BadRequestException(); } - return passwords; } catch (error) { throw new HttpException( diff --git a/src/vistor-password/controllers/visitor-password.controller.ts b/src/vistor-password/controllers/visitor-password.controller.ts index c0f09b6..c3fdfc8 100644 --- a/src/vistor-password/controllers/visitor-password.controller.ts +++ b/src/vistor-password/controllers/visitor-password.controller.ts @@ -49,8 +49,8 @@ export class VisitorPasswordController { ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) @Post('temporary-password/online/one-time') async addOnlineTemporaryPassword( @Body() addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, diff --git a/src/vistor-password/services/visitor-password.service.ts b/src/vistor-password/services/visitor-password.service.ts index 2e64faa..98b3a1e 100644 --- a/src/vistor-password/services/visitor-password.service.ts +++ b/src/vistor-password/services/visitor-password.service.ts @@ -417,38 +417,21 @@ export class VisitorPasswordService { deviceIds.forEach((deviceId) => { data.push( this.doorLockService - .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid) + .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true) .catch(() => {}), this.doorLockService - .getOnlineTemporaryPasswordsOneTime(deviceId.uuid) + .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true) .catch(() => {}), this.doorLockService - .getOnlineTemporaryPasswords(deviceId.uuid) + .getOnlineTemporaryPasswords(deviceId.uuid, true) .catch(() => {}), this.doorLockService - .getOfflineOneTimeTemporaryPasswords(deviceId.uuid) + .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true) .catch(() => {}), ); }); - return (await Promise.all(data)).flat().map((item) => { - return { - id: item.pwdId || item.id || null, - name: item.pwdName || item.name || '', - type: item.pwdTypeCode || item.type || '', - startTime: item.gmtStart || item.effectiveTime || null, - endTime: item.gmtExpired || item.invalidTime || null, - status: item.status || item.success || null, - additionalInfo: { - optUid: item.optUid || null, - hasClearPwd: item.hasClearPwd || false, - phase: item.phase || null, - phone: item.phone || '', - timeZone: item.timeZone || '', - result: item.result || null, - tid: item.tid || null, - t: item.t || null, - }, - }; + return (await Promise.all(data)).flat().filter((datum) => { + return datum != null; }); } From 3aff8d59d20beaba1483a548a745dd7dbf6f83d8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:41:01 +0300 Subject: [PATCH 6/8] Refactor DoorLockService to standardize password property names --- src/door-lock/services/door.lock.service.ts | 35 +++++++++++---------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index 7a3cd72..3175bd7 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -114,12 +114,12 @@ export class DoorLockService { return fromVisitor ? convertKeysToCamelCase(passwords.result.records).map((password) => { return { - passwodId: `${password.pwdId}`, - passwodExpired: `${password.gmtExpired}`, - passwordStart: `${password.gmtStart}`, + passwordId: `${password.pwdId}`, + invalidTime: `${password.gmtExpired}`, + effectiveTime: `${password.gmtStart}`, passwordCreated: `${password.gmtCreate}`, - passwodName: password.pwdName, - passwordStatus: password.status, + createdTime: password.pwdName, + passwordStatus: `${password.status}`, passwordType: 'OFFLINE_MULTIPLE', deviceUuid: doorLockUuid, }; @@ -160,12 +160,12 @@ export class DoorLockService { return fromVisitor ? convertKeysToCamelCase(passwords.result.records).map((password) => { return { - passwodId: `${password.pwdId}`, - passwodExpired: `${password.gmtExpired}`, - passwordStart: `${password.gmtStart}`, - passwordCreated: `${password.gmtCreate}`, - passwodName: password.pwdName, - passwordStatus: password.status, + passwordId: `${password.pwdId}`, + invalidTime: `${password.gmtExpired}`, + effectiveTime: `${password.gmtStart}`, + createdTime: `${password.gmtCreate}`, + passwordName: password.pwdName, + passwordStatus: `${password.status}`, passwordType: 'OFFLINE_ONETIME', deviceUuid: doorLockUuid, }; @@ -226,9 +226,10 @@ export class DoorLockService { ? convertKeysToCamelCase(passwordFiltered).map((password) => { return { passwodId: `${password.id}`, - passwodExpired: `${password.invalidTime}`, - passwordStart: `${password.effectiveTime}`, - passwordCreated: '', + invalidTime: `${password.invalidTime}`, + effectiveTime: `${password.effectiveTime}`, + createdTime: '', + scheduleList: password?.scheduleList, passwodName: password.name, passwordStatus: '', passwordType: 'ONLINE_MULTIPLE', @@ -293,9 +294,9 @@ export class DoorLockService { ? convertKeysToCamelCase(passwordFiltered).map((password) => { return { passwodId: `${password.id}`, - passwodExpired: `${password.invalidTime}`, - passwordStart: `${password.effectiveTime}`, - passwordCreated: '', + invalidTime: `${password.invalidTime}`, + effectiveTime: `${password.effectiveTime}`, + createdTime: '', passwodName: password.name, passwordStatus: '', passwordType: 'ONLINE_ONETIME', From 764cfbe7e2e492ea9710577790f12685b45ee4c8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:41:23 +0300 Subject: [PATCH 7/8] Refactor Visitor Password Controller and Service for Enhanced Security and Functionality --- .../visitor-password.controller.ts | 14 +++---- .../services/visitor-password.service.ts | 37 ++++++++++++------- .../visitor-password.module.ts | 8 ++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/vistor-password/controllers/visitor-password.controller.ts b/src/vistor-password/controllers/visitor-password.controller.ts index c3fdfc8..f1a84bb 100644 --- a/src/vistor-password/controllers/visitor-password.controller.ts +++ b/src/vistor-password/controllers/visitor-password.controller.ts @@ -49,8 +49,8 @@ export class VisitorPasswordController { ); } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post('temporary-password/online/one-time') async addOnlineTemporaryPassword( @Body() addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, @@ -86,8 +86,6 @@ export class VisitorPasswordController { return { statusCode: HttpStatus.CREATED, - success: true, - message: 'offline temporary password added successfully', data: temporaryPassword, }; } catch (error) { @@ -112,8 +110,6 @@ export class VisitorPasswordController { return { statusCode: HttpStatus.CREATED, - success: true, - message: 'offline temporary password added successfully', data: temporaryPassword, }; } catch (error) { @@ -123,7 +119,9 @@ export class VisitorPasswordController { ); } } - @Get('') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() async GetVisitorPassword() { try { return await this.visitorPasswordService.getPasswords(); @@ -134,6 +132,8 @@ export class VisitorPasswordController { ); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('/devices') async GetVisitorDevices() { try { diff --git a/src/vistor-password/services/visitor-password.service.ts b/src/vistor-password/services/visitor-password.service.ts index 98b3a1e..e8b2df4 100644 --- a/src/vistor-password/services/visitor-password.service.ts +++ b/src/vistor-password/services/visitor-password.service.ts @@ -17,6 +17,8 @@ import { import { EmailService } from '@app/common/util/email.service'; import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; import { DoorLockService } from 'src/door-lock/services'; +import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface'; +import { DeviceService } from 'src/device/services'; @Injectable() export class VisitorPasswordService { @@ -26,6 +28,7 @@ export class VisitorPasswordService { private readonly deviceRepository: DeviceRepository, private readonly emailService: EmailService, private readonly doorLockService: DoorLockService, + private readonly deviceService: DeviceService, private readonly passwordEncryptionService: PasswordEncryptionService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -383,7 +386,7 @@ export class VisitorPasswordService { ); // Return results if there are successful operations - if (successfulResults.length > 0) { + if (successfulResults?.length > 0) { return { successOperations: successfulResults, failedOperations: failedResults, @@ -416,9 +419,6 @@ export class VisitorPasswordService { const data = []; deviceIds.forEach((deviceId) => { data.push( - this.doorLockService - .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true) - .catch(() => {}), this.doorLockService .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true) .catch(() => {}), @@ -428,6 +428,9 @@ export class VisitorPasswordService { this.doorLockService .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true) .catch(() => {}), + this.doorLockService + .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true) + .catch(() => {}), ); }); return (await Promise.all(data)).flat().filter((datum) => { @@ -436,13 +439,27 @@ export class VisitorPasswordService { } async getAllPassDevices() { - return await this.deviceRepository.find({ + const devices = await this.deviceRepository.find({ where: { productDevice: { prodType: ProductType.DL, }, }, + relations: ['productDevice'], }); + const devicesData = await Promise.all( + devices?.map(async (device) => { + return { + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + ...(await this.deviceService.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + } as GetDeviceDetailsInterface; + }), + ); + return devicesData; } async addOnlineTemporaryPasswordOneTime( @@ -622,7 +639,7 @@ export class VisitorPasswordService { try { const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; let scheduleList; - if (addDeviceObj.scheduleList.length > 0) { + if (addDeviceObj?.scheduleList?.length > 0) { scheduleList = addDeviceObj.scheduleList.map((schedule) => ({ effective_time: this.timeToMinutes(schedule.effectiveTime), invalid_time: this.timeToMinutes(schedule.invalidTime), @@ -640,19 +657,16 @@ export class VisitorPasswordService { invalid_time: addDeviceObj.invalidTime, password_type: 'ticket', ticket_id: addDeviceObj.ticketId, - ...(addDeviceObj.scheduleList.length > 0 && { + ...(addDeviceObj?.scheduleList?.length > 0 && { schedule_list: scheduleList, }), type: '0', }, }); - console.log('response', response); return response as createTickInterface; } catch (error) { - console.log('error', error); - throw new HttpException( error.msg || 'Error adding online temporary password from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, @@ -789,12 +803,9 @@ export class VisitorPasswordService { type: '1', }, }); - console.log('response', response); return response as createTickInterface; } catch (error) { - console.log('error', error); - throw new HttpException( error.msg || 'Error adding online temporary password from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 51239f4..33454a3 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -7,6 +7,10 @@ import { DeviceRepository } from '@app/common/modules/device/repositories'; import { EmailService } from '@app/common/util/email.service'; import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; import { DoorLockModule } from 'src/door-lock/door.lock.module'; +import { DeviceService } from 'src/device/services'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -14,6 +18,10 @@ import { DoorLockModule } from 'src/door-lock/door.lock.module'; VisitorPasswordService, EmailService, PasswordEncryptionService, + DeviceService, + ProductRepository, + DeviceStatusFirebaseService, + SpaceRepository, DeviceRepository, ], exports: [VisitorPasswordService], From c6b8945978bfea4a690cc695be7a0b435cb02dc1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:44:02 +0300 Subject: [PATCH 8/8] Update .env.example with new environment variables for Firebase integration --- .env.example | 91 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index 54819e3..944dba0 100644 --- a/.env.example +++ b/.env.example @@ -1,75 +1,90 @@ ACCESS_KEY= - + AZURE_POSTGRESQL_DATABASE= - + AZURE_POSTGRESQL_HOST= - + AZURE_POSTGRESQL_PASSWORD= - + AZURE_POSTGRESQL_PORT= - + AZURE_POSTGRESQL_SSL= - + AZURE_POSTGRESQL_SYNC= - + AZURE_POSTGRESQL_USER= - + AZURE_REDIS_CONNECTIONSTRING= - + BASE_URL= - + DB_SYNC= - + DOCKER_REGISTRY_SERVER_PASSWORD= - + DOCKER_REGISTRY_SERVER_URL= - + DOCKER_REGISTRY_SERVER_USERNAME= - + DOPPLER_CONFIG= - + DOPPLER_ENVIRONMENT= - + DOPPLER_PROJECT= - + JWT_EXPIRE_TIME= - + JWT_EXPIRE_TIME_REFRESH= - + JWT_SECRET= - + JWT_SECRET_REFRESH= - + SECRET_KEY= - + SMTP_HOST= - + SMTP_PASSWORD= - + SMTP_PORT= - + SMTP_SECURE= - + SMTP_USER= - + WEBSITES_ENABLE_APP_SERVICE_STORAGE= - + PORT= - + SUPER_ADMIN_EMAIL= - + SUPER_ADMIN_PASSWORD= - + TUYA_ACCESS_ID= - + TUYA_ACCESS_KEY= - + ONESIGNAL_APP_ID= - + ONESIGNAL_API_KEY= - + TRUN_ON_TUYA_SOCKET= - + TUYA_EU_URL= - -MONGODB_URI= \ No newline at end of file + +FIREBASE_API_KEY= + +FIREBASE_AUTH_DOMAIN= + +FIREBASE_PROJECT_ID= + +FIREBASE_STORAGE_BUCKET= + +FIREBASE_MESSAGING_SENDER_ID= + +FIREBASE_APP_ID=d + +FIREBASE_MEASUREMENT_ID= + +FIREBASE_DATABASE_URL= +