From 09074f83e2e0465083addb1d7ed59c67f5963c89 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:08 +0300 Subject: [PATCH 1/9] Add Door Lock Controller with temporary password functionality --- .../controllers/door.lock.controller.ts | 182 ++++++++++++++++++ src/door-lock/controllers/index.ts | 1 + 2 files changed, 183 insertions(+) create mode 100644 src/door-lock/controllers/door.lock.controller.ts create mode 100644 src/door-lock/controllers/index.ts diff --git a/src/door-lock/controllers/door.lock.controller.ts b/src/door-lock/controllers/door.lock.controller.ts new file mode 100644 index 0000000..f3605ef --- /dev/null +++ b/src/door-lock/controllers/door.lock.controller.ts @@ -0,0 +1,182 @@ +import { DoorLockService } from '../services/door.lock.service'; +import { + Body, + Controller, + Post, + Param, + HttpException, + HttpStatus, + Get, + Delete, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AddDoorLockOnlineDto } from '../dtos/add.online-temp.dto'; +import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Door Lock Module') +@Controller({ + version: '1', + path: 'door-lock', +}) +export class DoorLockController { + constructor(private readonly doorLockService: DoorLockService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/online/:doorLockUuid') + async addOnlineTemporaryPassword( + @Body() addDoorLockDto: AddDoorLockOnlineDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOnlineTemporaryPassword( + addDoorLockDto, + doorLockUuid, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'online temporary password added successfully', + data: { + id: temporaryPassword.id, + }, + }; + } 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/:doorLockUuid') + async addOfflineOneTimeTemporaryPassword( + @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + ); + + 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/:doorLockUuid') + async addOfflineMultipleTimeTemporaryPassword( + @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + ); + + 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) + @Get('temporary-password/online/:doorLockUuid') + async getOnlineTemporaryPasswords( + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + return await this.doorLockService.getOnlineTemporaryPasswords( + doorLockUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('temporary-password/offline/one-time/:doorLockUuid') + async getOfflineOneTimeTemporaryPasswords( + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + return await this.doorLockService.getOfflineOneTimeTemporaryPasswords( + doorLockUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('temporary-password/offline/multiple-time/:doorLockUuid') + async getOfflineMultipleTimeTemporaryPasswords( + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + return await this.doorLockService.getOfflineMultipleTimeTemporaryPasswords( + doorLockUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('temporary-password/:doorLockUuid/:passwordId') + async deleteDoorLockPassword( + @Param('doorLockUuid') doorLockUuid: string, + @Param('passwordId') passwordId: string, + ) { + try { + await this.doorLockService.deleteDoorLockPassword( + doorLockUuid, + passwordId, + ); + return { + statusCode: HttpStatus.OK, + message: 'Temporary Password deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/door-lock/controllers/index.ts b/src/door-lock/controllers/index.ts new file mode 100644 index 0000000..d7d3c84 --- /dev/null +++ b/src/door-lock/controllers/index.ts @@ -0,0 +1 @@ +export * from './door.lock.controller'; From 062eb7660b91af06b5ded59c05a6dd421050268e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:20 +0300 Subject: [PATCH 2/9] Add door lock DTOs for adding offline and online temporary passwords --- src/door-lock/dtos/add.offline-temp.dto.ts | 36 ++++++++++ src/door-lock/dtos/add.online-temp.dto.ts | 84 ++++++++++++++++++++++ src/door-lock/dtos/index.ts | 1 + 3 files changed, 121 insertions(+) create mode 100644 src/door-lock/dtos/add.offline-temp.dto.ts create mode 100644 src/door-lock/dtos/add.online-temp.dto.ts create mode 100644 src/door-lock/dtos/index.ts diff --git a/src/door-lock/dtos/add.offline-temp.dto.ts b/src/door-lock/dtos/add.offline-temp.dto.ts new file mode 100644 index 0000000..f30cd10 --- /dev/null +++ b/src/door-lock/dtos/add.offline-temp.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, Length } from 'class-validator'; + +export class AddDoorLockOfflineTempDto { + @ApiProperty({ + description: 'name', + required: true, + }) + @IsString() + @IsNotEmpty() + public name: 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; +} diff --git a/src/door-lock/dtos/add.online-temp.dto.ts b/src/door-lock/dtos/add.online-temp.dto.ts new file mode 100644 index 0000000..bb84068 --- /dev/null +++ b/src/door-lock/dtos/add.online-temp.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsEnum, + Length, +} 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 AddDoorLockOnlineDto { + @ApiProperty({ + description: 'name', + required: true, + }) + @IsString() + @IsNotEmpty() + public name: 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 }) + @Type(() => ScheduleDto) + public scheduleList: ScheduleDto[]; +} diff --git a/src/door-lock/dtos/index.ts b/src/door-lock/dtos/index.ts new file mode 100644 index 0000000..3b78a05 --- /dev/null +++ b/src/door-lock/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.online-temp.dto'; From 46debaaafc9cf758a8d7dfbea6b1f1f3bd840c38 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:31 +0300 Subject: [PATCH 3/9] Add door lock interfaces --- .../interfaces/door.lock.interface.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/door-lock/interfaces/door.lock.interface.ts diff --git a/src/door-lock/interfaces/door.lock.interface.ts b/src/door-lock/interfaces/door.lock.interface.ts new file mode 100644 index 0000000..5b89061 --- /dev/null +++ b/src/door-lock/interfaces/door.lock.interface.ts @@ -0,0 +1,57 @@ +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; + }; + msg?: string; +} +export interface addDeviceObjectInterface { + name: string; + encryptedPassword: string; + effectiveTime: string; + invalidTime: string; + ticketId: string; + scheduleList?: any[]; +} + +export interface ScheduleDto { + effectiveTime: string; + invalidTime: string; + workingDay: WorkingDays[]; +} + +export interface AddDoorLockOnlineInterface { + name: string; + password: string; + effectiveTime: string; + invalidTime: string; + scheduleList?: ScheduleDto[]; +} +export interface getPasswordInterface { + success: boolean; + result: [ + { + effective_time: number; + id: number; + invalid_time: number; + name: 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; +} From b9a15134a21b8a4501e2f8ef8766cf03338ec8b6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:41 +0300 Subject: [PATCH 4/9] feat: Add DoorLockService and PasswordEncryptionService --- src/door-lock/services/door.lock.service.ts | 575 ++++++++++++++++++ src/door-lock/services/encryption.services.ts | 57 ++ src/door-lock/services/index.ts | 1 + 3 files changed, 633 insertions(+) create mode 100644 src/door-lock/services/door.lock.service.ts create mode 100644 src/door-lock/services/encryption.services.ts create mode 100644 src/door-lock/services/index.ts diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts new file mode 100644 index 0000000..d60c4bd --- /dev/null +++ b/src/door-lock/services/door.lock.service.ts @@ -0,0 +1,575 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { + AddDoorLockOnlineInterface, + addDeviceObjectInterface, + createTickInterface, + deleteTemporaryPasswordInterface, + getPasswordInterface, +} from '../interfaces/door.lock.interface'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProductType } from '@app/common/constants/product-type.enum'; + +import { PasswordEncryptionService } from './encryption.services'; +import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class DoorLockService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly deviceRepository: DeviceRepository, + 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 deleteDoorLockPassword(doorLockUuid: string, passwordId: 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 deletePass = await this.deleteDoorLockPasswordTuya( + deviceDetails.deviceTuyaUuid, + passwordId, + ); + + if (!deletePass.success) { + throw new HttpException('PasswordId not found', HttpStatus.NOT_FOUND); + } + return deletePass; + } catch (error) { + throw new HttpException( + error.message || 'Error deleting temporary password', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async deleteDoorLockPasswordTuya( + doorLockUuid: string, + passwordId: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords/${passwordId}`; + + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as deleteTemporaryPasswordInterface; + } catch (error) { + throw new HttpException( + 'Error deleting temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOfflineMultipleTimeTemporaryPasswords(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.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result.filter( + (item) => + (!item.schedule_list || item.schedule_list.length === 0) && + item.type === 0, + ); + + return convertKeysToCamelCase(passwordFiltered); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || + 'Error getting offline multiple time temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOfflineOneTimeTemporaryPasswords(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.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result.filter( + (item) => + (!item.schedule_list || item.schedule_list.length === 0) && + item.type === 1, + ); + + return convertKeysToCamelCase(passwordFiltered); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || 'Error getting offline one time temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOnlineTemporaryPasswords(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.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result + .filter((item) => item.schedule_list && item.schedule_list.length > 0) + .map((password: any) => { + 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 getTemporaryPasswordsTuya( + doorLockUuid: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords?valid=true`; + + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as getPasswordInterface; + } catch (error) { + throw new HttpException( + 'Error getting temporary passwords from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + doorLockUuid: string, + ) { + try { + const createOnlinePass = await this.addOnlineTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + 'multiple', + false, + ); + if (!createOnlinePass) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( + addDoorLockOfflineTempDto, + createOnlinePass.id, + createOnlinePass.deviceTuyaUuid, + 'multiple', + ); + if (!createOnceOfflinePass.success) { + throw new HttpException( + createOnceOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + return { + result: createOnceOfflinePass.result, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error adding offline temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + doorLockUuid: string, + ) { + try { + const createOnlinePass = await this.addOnlineTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + 'once', + false, + ); + if (!createOnlinePass) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( + addDoorLockOfflineTempDto, + createOnlinePass.id, + createOnlinePass.deviceTuyaUuid, + 'once', + ); + if (!createOnceOfflinePass.success) { + throw new HttpException( + createOnceOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + return { + result: createOnceOfflinePass.result, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error adding offline temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineTemporaryPasswordTuya( + addDoorLockDto: AddDoorLockOnlineInterface, + onlinePassId: number, + doorLockUuid: string, + type: string, + ): Promise { + try { + const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`; + + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: addDoorLockDto.name, + ...(type === 'multiple' && { + effective_time: addDoorLockDto.effectiveTime, + invalid_time: addDoorLockDto.invalidTime, + }), + + type, + password_id: onlinePassId, + }, + }); + + return response as createTickInterface; + } catch (error) { + throw new HttpException( + 'Error adding offline temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOnlineTemporaryPassword( + addDoorLockDto: AddDoorLockOnlineInterface, + doorLockUuid: string, + type: string = 'once', + isOnline: boolean = true, + ) { + try { + const passwordData = await this.getTicketAndEncryptedPassword( + doorLockUuid, + addDoorLockDto.password, + ); + if ( + !passwordData.ticketKey || + !passwordData.encryptedPassword || + !passwordData.deviceTuyaUuid + ) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const addDeviceObj: addDeviceObjectInterface = { + ...addDoorLockDto, + ...passwordData, + }; + const createPass = await this.addOnlineTemporaryPasswordTuya( + addDeviceObj, + passwordData.deviceTuyaUuid, + type, + isOnline, + ); + + if (!createPass.success) { + throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST); + } + return { + id: createPass.result.id, + deviceTuyaUuid: passwordData.deviceTuyaUuid, + }; + } catch (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 addOnlineTemporaryPasswordTuya( + addDeviceObj: addDeviceObjectInterface, + doorLockUuid: string, + type: string, + isOnline: boolean = true, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; + let scheduleList; + if (isOnline) { + 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.name, + password: addDeviceObj.encryptedPassword, + effective_time: addDeviceObj.effectiveTime, + invalid_time: addDeviceObj.invalidTime, + password_type: 'ticket', + ticket_id: addDeviceObj.ticketId, + ...(isOnline && { + schedule_list: scheduleList, + }), + type: type === 'multiple' ? '0' : '1', + }, + }); + + return response as createTickInterface; + } catch (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); + } + } +} diff --git a/src/door-lock/services/encryption.services.ts b/src/door-lock/services/encryption.services.ts new file mode 100644 index 0000000..7e56afe --- /dev/null +++ b/src/door-lock/services/encryption.services.ts @@ -0,0 +1,57 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import * as CryptoJS from 'crypto-js'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class PasswordEncryptionService { + constructor(private readonly configService: ConfigService) {} + + encrypt(plainText: string, secretKey: string): string { + const keyBytes = CryptoJS.enc.Utf8.parse(secretKey); + + const encrypted = CryptoJS.AES.encrypt(plainText, keyBytes, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + + return encrypted.ciphertext.toString(CryptoJS.enc.Hex); + } + + decrypt(encryptedText: string, secretKey: string): string { + const keyBytes = CryptoJS.enc.Utf8.parse(secretKey); + + const decrypted = CryptoJS.AES.decrypt( + { + ciphertext: CryptoJS.enc.Hex.parse(encryptedText), + } as any, + keyBytes, + { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }, + ); + + return decrypted.toString(CryptoJS.enc.Utf8); + } + + generateEncryptedPassword( + plainTextPassword: string, + ticketKey: string, + ): string { + try { + const accessSecret = this.configService.get( + 'auth-config.SECRET_KEY', + ); + // The accessSecret must be 32 bytes, ensure it is properly padded or truncated + const paddedAccessSecret = accessSecret.padEnd(32, '0').slice(0, 32); + const plainTextTicketKey = this.decrypt(ticketKey, paddedAccessSecret); + + return this.encrypt(plainTextPassword, plainTextTicketKey); + } catch (error) { + throw new HttpException( + `Error encrypting password: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/door-lock/services/index.ts b/src/door-lock/services/index.ts new file mode 100644 index 0000000..e847a10 --- /dev/null +++ b/src/door-lock/services/index.ts @@ -0,0 +1 @@ +export * from './door.lock.service'; From a0a5b992db54f9ec04988a66f065156c9959e0ae Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:50 +0300 Subject: [PATCH 5/9] Add DoorLockModule with controller, service, and dependencies --- src/door-lock/door.lock.module.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/door-lock/door.lock.module.ts diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts new file mode 100644 index 0000000..3b1920e --- /dev/null +++ b/src/door-lock/door.lock.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DoorLockService } from './services/door.lock.service'; +import { DoorLockController } from './controllers/door.lock.controller'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { PasswordEncryptionService } from './services/encryption.services'; +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [DoorLockController], + providers: [DoorLockService, PasswordEncryptionService, DeviceRepository], + exports: [DoorLockService], +}) +export class DoorLockModule {} From e2359e28a90fa11455e07ee83bb6ae1781b5e92c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:55 +0300 Subject: [PATCH 6/9] Add enum for working days in common library --- libs/common/src/constants/working-days.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 libs/common/src/constants/working-days.ts diff --git a/libs/common/src/constants/working-days.ts b/libs/common/src/constants/working-days.ts new file mode 100644 index 0000000..d870d3c --- /dev/null +++ b/libs/common/src/constants/working-days.ts @@ -0,0 +1,9 @@ +export enum WorkingDays { + Sun = 'Sun', + Mon = 'Mon', + Tue = 'Tue', + Wed = 'Wed', + Thu = 'Thu', + Fri = 'Fri', + Sat = 'Sat', +} From 88685f40b773de1e248685f0e94dbad6918affc2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:48:00 +0300 Subject: [PATCH 7/9] Add DoorLockModule to app module imports --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 59b4275..c95d1bf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { SeederModule } from '@app/common/seed/seeder.module'; import { UserNotificationModule } from './user-notification/user-notification.module'; import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; import { SceneModule } from './scene/scene.module'; +import { DoorLockModule } from './door-lock/door.lock.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -38,6 +39,7 @@ import { SceneModule } from './scene/scene.module'; UserNotificationModule, SeederModule, SceneModule, + DoorLockModule, ], controllers: [AuthenticationController], }) From 05e51c79ebc3a665a127abce9f5e851b2d3fb471 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:54:01 +0300 Subject: [PATCH 8/9] Remove unnecessary comment and improve code readability in DoorLockService. --- src/door-lock/services/door.lock.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index d60c4bd..fbd520c 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -481,7 +481,6 @@ export class DoorLockService { } }); - // Convert the binary string to an integer const workingDayValue = parseInt(binaryString, 2); return workingDayValue; From 53e2be63db457c38e67c2bd7652995f168a4e63c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:54:35 +0300 Subject: [PATCH 9/9] Convert binary string to integer in DoorLockService --- src/door-lock/services/door.lock.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index fbd520c..d60c4bd 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -481,6 +481,7 @@ export class DoorLockService { } }); + // Convert the binary string to an integer const workingDayValue = parseInt(binaryString, 2); return workingDayValue;