diff --git a/libs/common/src/helper/randomString.ts b/libs/common/src/helper/randomString.ts new file mode 100644 index 0000000..5a664e3 --- /dev/null +++ b/libs/common/src/helper/randomString.ts @@ -0,0 +1,10 @@ +export function generateRandomString(length: number): string { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let randomString = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + randomString += characters.charAt(randomIndex); + } + return randomString; +} 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 fee939a..3389fec 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -6,9 +6,10 @@ import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; import { DeviceNotificationEntity } from '../../device-notification/entities'; +import { UserEntity } from '../../user/entities'; @Entity({ name: 'device' }) -@Unique(['spaceDevice', 'deviceTuyaUuid']) +@Unique(['deviceTuyaUuid']) export class DeviceEntity extends AbstractEntity { @Column({ nullable: false, @@ -21,6 +22,9 @@ export class DeviceEntity extends AbstractEntity { }) isActive: true; + @ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) + user: UserEntity; + @OneToMany( () => DeviceUserPermissionEntity, (permission) => permission.device, @@ -44,7 +48,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/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts index cc5ad83..98706d0 100644 --- a/libs/common/src/modules/space/dtos/space.dto.ts +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -16,4 +16,8 @@ export class SpaceDto { @IsString() @IsNotEmpty() public spaceTypeUuid: string; + + @IsString() + @IsNotEmpty() + public invitationCode: string; } diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 56f7010..b337e91 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceTypeEntity } from '../../space-type/entities'; @@ -6,6 +6,7 @@ import { UserSpaceEntity } from '../../user-space/entities'; import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'space' }) +@Unique(['invitationCode']) export class SpaceEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -18,6 +19,11 @@ export class SpaceEntity extends AbstractEntity { nullable: false, }) public spaceName: string; + + @Column({ + nullable: true, + }) + public invitationCode: string; @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) parent: SpaceEntity; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 06c7c91..581955f 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -6,6 +6,7 @@ import { UserSpaceEntity } from '../../user-space/entities'; import { UserRoleEntity } from '../../user-role/entities'; import { DeviceNotificationEntity } from '../../device-notification/entities'; import { UserNotificationEntity } from '../../user-notification/entities'; +import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -55,6 +56,15 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) userSpaces: UserSpaceEntity[]; + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + @OneToMany( () => UserNotificationEntity, (userNotification) => userNotification.user, diff --git a/package-lock.json b/package-lock.json index 0fd8ba8..22aab51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5365,20 +5365,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7378,6 +7364,23 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 34c973a..a76d620 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -30,7 +30,7 @@ export class BuildingController { constructor(private readonly buildingService: BuildingService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckCommunityTypeGuard) + @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index df59979..c88acf1 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -32,7 +32,7 @@ export class CommunityController { constructor(private readonly communityService: CommunityService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(JwtAuthGuard) @Post() async addCommunity(@Body() addCommunityDto: AddCommunityDto) { try { diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index c102775..3276e39 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,8 @@ 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'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; @ApiTags('Device Module') @Controller({ @@ -34,7 +38,39 @@ import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; }) export class DeviceController { constructor(private readonly deviceService: DeviceService) {} + @ApiBearerAuth() + @UseGuards(SuperAdminRoleGuard, CheckDeviceGuard) + @Post() + async addDeviceUser(@Body() addDeviceDto: AddDeviceDto) { + try { + const device = await this.deviceService.addDeviceUser(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) + @Get('user/:userUuid') + async getDevicesByUser(@Param('userUuid') userUuid: string) { + try { + return await this.deviceService.getDevicesByUser(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') @@ -58,16 +94,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..d48a65e 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,82 @@ export class DeviceService { }); } + async addDeviceUser(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 getDevicesByUser( + userUuid: string, + ): Promise { + try { + const devices = await this.deviceRepository.find({ + where: { + user: { uuid: userUuid }, + permission: { + userUuid, + permissionType: { + type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), + }, + }, + }, + relations: [ + 'spaceDevice', + 'productDevice', + 'permission', + 'permission.permissionType', + ], + }); + const devicesData = await Promise.all( + devices.map(async (device) => { + return { + haveRoom: device.spaceDevice ? true : false, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + } as GetDeviceDetailsInterface; + }), + ); + + return devicesData; + } catch (error) { + // Handle the error here + throw new HttpException( + 'User does not have any devices', + HttpStatus.NOT_FOUND, + ); + } + } async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, userUuid: string, @@ -72,13 +149,14 @@ export class DeviceService { const devicesData = await Promise.all( devices.map(async (device) => { return { + haveRoom: device.spaceDevice ? true : false, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), uuid: device.uuid, - productUuid: device.productDevice.uuid, - productType: device.productDevice.prodType, - permissionType: device.permission[0].permissionType.type, } as GetDeviceDetailsInterface; }), ); @@ -92,7 +170,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 +228,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 +240,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/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 0df9efc..b4940fe 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -30,7 +30,7 @@ export class FloorController { constructor(private readonly floorService: FloorService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckBuildingTypeGuard) + @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { 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', }); } } diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 434370d..0a92e57 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -28,7 +28,7 @@ export class RoomController { constructor(private readonly roomService: RoomService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUnitTypeGuard) + @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 7c410b6..5c55bc7 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -12,7 +12,11 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddUnitDto, AddUserUnitDto } from '../dtos/add.unit.dto'; +import { + AddUnitDto, + AddUserUnitDto, + AddUserUnitUsingCodeDto, +} from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @@ -30,7 +34,7 @@ export class UnitController { constructor(private readonly unitService: UnitService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckFloorTypeGuard) + @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { @@ -147,4 +151,39 @@ export class UnitController { ); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid/invitation-code') + async getUnitInvitationCode(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitInvitationCode(unitUuid); + return unit; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('user/verify-code') + async verifyCodeAndAddUserUnit( + @Body() addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + await this.unitService.verifyCodeAndAddUserUnit(addUserUnitUsingCodeDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user unit added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts index e42d1bb..6d6c52a 100644 --- a/src/unit/dtos/add.unit.dto.ts +++ b/src/unit/dtos/add.unit.dto.ts @@ -40,3 +40,29 @@ export class AddUserUnitDto { Object.assign(this, dto); } } +export class AddUserUnitUsingCodeDto { + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 11d654f..f0472af 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -7,7 +7,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddUnitDto, AddUserUnitDto } from '../dtos'; +import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, @@ -18,6 +18,7 @@ import { import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { generateRandomString } from '@app/common/helper/randomString'; @Injectable() export class UnitService { @@ -227,7 +228,7 @@ export class UnitService { async addUserUnit(addUserUnitDto: AddUserUnitDto) { try { - await this.userSpaceRepository.save({ + return await this.userSpaceRepository.save({ user: { uuid: addUserUnitDto.userUuid }, space: { uuid: addUserUnitDto.unitUuid }, }); @@ -282,4 +283,61 @@ export class UnitService { } } } + async getUnitInvitationCode(unitUuid: string): Promise { + try { + // Generate a 6-character random invitation code + const invitationCode = generateRandomString(6); + + // Update the unit with the new invitation code + await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); + + // Fetch the updated unit + const updatedUnit = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedUnit.uuid, + invitationCode: updatedUnit.invitationCode, + type: updatedUnit.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async verifyCodeAndAddUserUnit( + addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + const unit = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: addUserUnitUsingCodeDto.inviteCode, + spaceType: { type: 'unit' }, + }, + relations: ['spaceType'], + }); + if (unit.invitationCode) { + const user = await this.addUserUnit({ + userUuid: addUserUnitUsingCodeDto.userUuid, + unitUuid: unit.uuid, + }); + if (user.uuid) { + await this.spaceRepository.update( + { uuid: unit.uuid }, + { invitationCode: null }, + ); + } + } + } catch (err) { + throw new HttpException( + 'Invalid invitation code', + HttpStatus.BAD_REQUEST, + ); + } + } }