From 423533df0428c8ee1afb3dd880fc24e2e9a5148e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 29 May 2024 23:29:53 +0300 Subject: [PATCH] feat(device): add endpoint to add device to user --- .../src/modules/device/dtos/device.dto.ts | 4 + .../modules/device/entities/device.entity.ts | 8 +- .../src/modules/user/entities/user.entity.ts | 4 + src/device/controllers/device.controller.ts | 37 ++++++-- src/device/dtos/add.device.dto.ts | 19 +++- src/device/services/device.service.ts | 91 ++++++++++++------- src/guards/device.guard.ts | 89 ++++++++++++++++++ src/guards/group.guard.ts | 2 +- src/guards/room.guard.ts | 34 +++---- 9 files changed, 223 insertions(+), 65 deletions(-) create mode 100644 src/guards/device.guard.ts diff --git a/libs/common/src/modules/device/dtos/device.dto.ts b/libs/common/src/modules/device/dtos/device.dto.ts index 8873f31..7a6ba0c 100644 --- a/libs/common/src/modules/device/dtos/device.dto.ts +++ b/libs/common/src/modules/device/dtos/device.dto.ts @@ -9,6 +9,10 @@ export class DeviceDto { @IsNotEmpty() spaceUuid: string; + @IsString() + @IsNotEmpty() + userUuid: string; + @IsString() @IsNotEmpty() deviceTuyaUuid: string; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 53dbbb3..c6b1859 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -5,9 +5,10 @@ import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; +import { UserEntity } from '../../user/entities'; @Entity({ name: 'device' }) -@Unique(['spaceDevice', 'deviceTuyaUuid']) +@Unique(['deviceTuyaUuid']) export class DeviceEntity extends AbstractEntity { @Column({ nullable: false, @@ -20,6 +21,9 @@ export class DeviceEntity extends AbstractEntity { }) isActive: true; + @ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) + user: UserEntity; + @OneToMany( () => DeviceUserPermissionEntity, (permission) => permission.device, @@ -36,7 +40,7 @@ export class DeviceEntity extends AbstractEntity { userGroupDevices: GroupDeviceEntity[]; @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { - nullable: false, + nullable: true, }) spaceDevice: SpaceEntity; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 17a9268..9d93671 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -4,6 +4,7 @@ import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user-space/entities'; import { UserRoleEntity } from '../../user-role/entities'; +import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -53,6 +54,9 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) userSpaces: UserSpaceEntity[]; + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + @OneToMany( () => DeviceUserPermissionEntity, (userPermission) => userPermission.user, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index c102775..9d16e2b 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -10,11 +10,13 @@ import { HttpStatus, UseGuards, Req, + Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { + AddDeviceDto, AddDeviceInGroupDto, - AddDeviceInRoomDto, + UpdateDeviceInRoomDto, } from '../dtos/add.device.dto'; import { GetDeviceByGroupIdDto, @@ -26,6 +28,7 @@ import { CheckGroupGuard } from 'src/guards/group.guard'; import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { CheckDeviceGuard } from 'src/guards/device.guard'; @ApiTags('Device Module') @Controller({ @@ -34,7 +37,26 @@ import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; }) export class DeviceController { constructor(private readonly deviceService: DeviceService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckDeviceGuard) + @Post() + async addDevice(@Body() addDeviceDto: AddDeviceDto) { + try { + const device = await this.deviceService.addDevice(addDeviceDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device added successfully', + data: device, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') @@ -58,16 +80,19 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) - @Post('room') - async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { + @Put('room') + async updateDeviceInRoom( + @Body() updateDeviceInRoomDto: UpdateDeviceInRoomDto, + ) { try { - const device = - await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + const device = await this.deviceService.updateDeviceInRoom( + updateDeviceInRoomDto, + ); return { statusCode: HttpStatus.CREATED, success: true, - message: 'device added in room successfully', + message: 'device updated in room successfully', data: device, }; } catch (error) { diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index 4adc470..88c3712 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; -export class AddDeviceInRoomDto { +export class AddDeviceDto { @ApiProperty({ description: 'deviceTuyaUuid', required: true, @@ -10,6 +10,23 @@ export class AddDeviceInRoomDto { @IsNotEmpty() public deviceTuyaUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} +export class UpdateDeviceInRoomDto { + @ApiProperty({ + description: 'deviceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceUuid: string; + @ApiProperty({ description: 'roomUuid', required: true, diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 5f59d59..b9e232e 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -8,8 +8,9 @@ import { import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { + AddDeviceDto, AddDeviceInGroupDto, - AddDeviceInRoomDto, + UpdateDeviceInRoomDto, } from '../dtos/add.device.dto'; import { DeviceInstructionResponse, @@ -47,6 +48,38 @@ export class DeviceService { }); } + async addDevice(addDeviceDto: AddDeviceDto) { + try { + const device = await this.getDeviceDetailsByDeviceIdTuya( + addDeviceDto.deviceTuyaUuid, + ); + + if (!device.productUuid) { + throw new Error('Product UUID is missing for the device.'); + } + + return await this.deviceRepository.save({ + deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, + productDevice: { uuid: device.productUuid }, + user: { + uuid: addDeviceDto.userUuid, + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'Device already exists', + HttpStatus.BAD_REQUEST, + ); + } else { + throw new HttpException( + error.message || 'Failed to add device in room', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, userUuid: string, @@ -92,7 +125,31 @@ export class DeviceService { ); } } - + async updateDeviceInRoom(updateDeviceInRoomDto: UpdateDeviceInRoomDto) { + try { + await this.deviceRepository.update( + { uuid: updateDeviceInRoomDto.deviceUuid }, + { + spaceDevice: { uuid: updateDeviceInRoomDto.roomUuid }, + }, + ); + const device = await this.deviceRepository.findOne({ + where: { + uuid: updateDeviceInRoomDto.deviceUuid, + }, + relations: ['spaceDevice'], + }); + return { + uuid: device.uuid, + roomUuid: device.spaceDevice.uuid, + }; + } catch (error) { + throw new HttpException( + 'Failed to add device in room', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDevicesByGroupId( getDeviceByGroupIdDto: GetDeviceByGroupIdDto, userUuid: string, @@ -126,7 +183,6 @@ export class DeviceService { uuid: device.device.uuid, productUuid: device.device.productDevice.uuid, productType: device.device.productDevice.prodType, - permissionType: device.device.permission[0].permissionType.type, } as GetDeviceDetailsInterface; }), ); @@ -139,35 +195,6 @@ export class DeviceService { ); } } - async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { - try { - const device = await this.getDeviceDetailsByDeviceIdTuya( - addDeviceInRoomDto.deviceTuyaUuid, - ); - - if (!device.productUuid) { - throw new Error('Product UUID is missing for the device.'); - } - - return await this.deviceRepository.save({ - deviceTuyaUuid: addDeviceInRoomDto.deviceTuyaUuid, - spaceDevice: { uuid: addDeviceInRoomDto.roomUuid }, - productDevice: { uuid: device.productUuid }, - }); - } catch (error) { - if (error.code === '23505') { - throw new HttpException( - 'Device already exists in the room', - HttpStatus.BAD_REQUEST, - ); - } else { - throw new HttpException( - 'Failed to add device in room', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - } async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { try { diff --git a/src/guards/device.guard.ts b/src/guards/device.guard.ts new file mode 100644 index 0000000..5d08598 --- /dev/null +++ b/src/guards/device.guard.ts @@ -0,0 +1,89 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ConfigService } from '@nestjs/config'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckDeviceGuard implements CanActivate { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + private readonly deviceRepository: DeviceRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + if (req.body && req.body.userUuid && req.body.deviceTuyaUuid) { + const { userUuid, deviceTuyaUuid } = req.body; + await this.checkUserIsFound(userUuid); + await this.checkDeviceIsFoundFromTuya(deviceTuyaUuid); + } else { + throw new BadRequestException('Invalid request parameters'); + } + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const user = await this.userRepository.findOne({ + where: { + uuid: userUuid, + }, + }); + if (!user) { + throw new NotFoundException('User not found'); + } + } + async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { + const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + if (!response.success) { + throw new NotFoundException('Device not found from Tuya'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: error.message || 'Invalid UUID', + }); + } + } +} diff --git a/src/guards/group.guard.ts b/src/guards/group.guard.ts index 1fe70e4..d2d994b 100644 --- a/src/guards/group.guard.ts +++ b/src/guards/group.guard.ts @@ -74,7 +74,7 @@ export class CheckGroupGuard implements CanActivate { } else { response.status(HttpStatus.BAD_REQUEST).json({ statusCode: HttpStatus.BAD_REQUEST, - message: 'Invalid UUID', + message: error.message || 'Invalid UUID', }); } } diff --git a/src/guards/room.guard.ts b/src/guards/room.guard.ts index 15b34b5..c5ed514 100644 --- a/src/guards/room.guard.ts +++ b/src/guards/room.guard.ts @@ -4,29 +4,17 @@ import { Injectable, HttpStatus, } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class CheckRoomGuard implements CanActivate { - private tuya: TuyaContext; constructor( - private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly deviceRepository: DeviceRepository, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', - accessKey, - secretKey, - }); - } + ) {} async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); @@ -35,10 +23,10 @@ export class CheckRoomGuard implements CanActivate { if (req.query && req.query.roomUuid) { const { roomUuid } = req.query; await this.checkRoomIsFound(roomUuid); - } else if (req.body && req.body.roomUuid && req.body.deviceTuyaUuid) { - const { roomUuid, deviceTuyaUuid } = req.body; + } else if (req.body && req.body.roomUuid && req.body.deviceUuid) { + const { roomUuid, deviceUuid } = req.body; await this.checkRoomIsFound(roomUuid); - await this.checkDeviceIsFoundFromTuya(deviceTuyaUuid); + await this.checkDeviceIsFound(deviceUuid); } else { throw new BadRequestException('Invalid request parameters'); } @@ -63,14 +51,14 @@ export class CheckRoomGuard implements CanActivate { throw new NotFoundException('Room not found'); } } - async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { - const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; - const response = await this.tuya.request({ - method: 'GET', - path, + async checkDeviceIsFound(deviceUuid: string) { + const response = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, }); - if (!response.success) { + if (!response.uuid) { throw new NotFoundException('Device not found'); } } @@ -88,7 +76,7 @@ export class CheckRoomGuard implements CanActivate { } else { response.status(HttpStatus.BAD_REQUEST).json({ statusCode: HttpStatus.BAD_REQUEST, - message: 'Invalid UUID', + message: error.message || 'Invalid UUID', }); } }