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