Refactor group controller methods and add device product guard

This commit is contained in:
faris Aljohari
2024-04-21 16:27:53 +03:00
parent 18e7e35d35
commit 7abfe29746
8 changed files with 284 additions and 260 deletions

View File

@ -5,17 +5,18 @@ import {
Get,
Post,
UseGuards,
Query,
Param,
Put,
Delete,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import { AddGroupDto } from '../dtos/add.group.dto';
import { GetGroupDto } from '../dtos/get.group.dto';
import { ControlGroupDto } from '../dtos/control.group.dto';
import { RenameGroupDto } from '../dtos/rename.group.dto copy';
import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard';
@ApiTags('Group Module')
@Controller({
@ -25,34 +26,43 @@ import { RenameGroupDto } from '../dtos/rename.group.dto copy';
export class GroupController {
constructor(private readonly groupService: GroupService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async getGroupsByHomeId(@Query() getGroupsDto: GetGroupDto) {
// @ApiBearerAuth()
// @UseGuards(JwtAuthGuard)
@Get('space/:spaceUuid')
async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) {
try {
return await this.groupService.getGroupsByHomeId(getGroupsDto);
} catch (err) {
throw new Error(err);
return await this.groupService.getGroupsBySpaceUuid(spaceUuid);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':groupId')
async getGroupsByGroupId(@Param('groupId') groupId: number) {
// @ApiBearerAuth()
// @UseGuards(JwtAuthGuard)
@Get(':groupUuid')
async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) {
try {
return await this.groupService.getGroupsByGroupId(groupId);
} catch (err) {
throw new Error(err);
return await this.groupService.getGroupsByGroupUuid(groupUuid);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
// @ApiBearerAuth()
@UseGuards(CheckProductUuidForAllDevicesGuard)
@Post()
async addGroup(@Body() addGroupDto: AddGroupDto) {
try {
return await this.groupService.addGroup(addGroupDto);
} catch (err) {
throw new Error(err);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ -67,25 +77,37 @@ export class GroupController {
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('rename')
async renameGroup(@Body() renameGroupDto: RenameGroupDto) {
// @ApiBearerAuth()
// @UseGuards(JwtAuthGuard)
@Put('rename/:groupUuid')
async renameGroupByUuid(
@Param('groupUuid') groupUuid: string,
@Body() renameGroupDto: RenameGroupDto,
) {
try {
return await this.groupService.renameGroup(renameGroupDto);
} catch (err) {
throw new Error(err);
return await this.groupService.renameGroupByUuid(
groupUuid,
renameGroupDto,
);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete(':groupId')
async deleteGroup(@Param('groupId') groupId: number) {
// @ApiBearerAuth()
// @UseGuards(JwtAuthGuard)
@Delete(':groupUuid')
async deleteGroup(@Param('groupUuid') groupUuid: string) {
try {
return await this.groupService.deleteGroup(groupId);
} catch (err) {
throw new Error(err);
return await this.groupService.deleteGroup(groupUuid);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumberString } from 'class-validator';
import { IsNotEmpty, IsString, IsArray } from 'class-validator';
export class AddGroupDto {
@ApiProperty({
@ -11,26 +11,10 @@ export class AddGroupDto {
public groupName: string;
@ApiProperty({
description: 'homeId',
description: 'deviceUuids',
required: true,
})
@IsNumberString()
@IsArray()
@IsNotEmpty()
public homeId: string;
@ApiProperty({
description: 'productId',
required: true,
})
@IsString()
@IsNotEmpty()
public productId: string;
@ApiProperty({
description: 'The list of up to 20 device IDs, separated with commas (,)',
required: true,
})
@IsString()
@IsNotEmpty()
public deviceIds: string;
public deviceUuids: [string];
}

View File

@ -1,28 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumberString } from 'class-validator';
export class GetGroupDto {
@ApiProperty({
description: 'homeId',
required: true,
})
@IsNumberString()
@IsNotEmpty()
public homeId: string;
@ApiProperty({
description: 'pageSize',
required: true,
})
@IsNumberString()
@IsNotEmpty()
public pageSize: number;
@ApiProperty({
description: 'pageNo',
required: true,
})
@IsNumberString()
@IsNotEmpty()
public pageNo: number;
}

View File

@ -1,15 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumberString } from 'class-validator';
import { IsNotEmpty, IsString } from 'class-validator';
export class RenameGroupDto {
@ApiProperty({
description: 'groupId',
required: true,
})
@IsNumberString()
@IsNotEmpty()
public groupId: string;
@ApiProperty({
description: 'groupName',
required: true,

View File

@ -2,10 +2,26 @@ import { Module } from '@nestjs/common';
import { GroupService } from './services/group.service';
import { GroupController } from './controllers/group.controller';
import { ConfigModule } from '@nestjs/config';
import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module';
import { GroupRepository } from '@app/common/modules/group/repositories';
import { GroupDeviceRepositoryModule } from '@app/common/modules/group-device/group.device.repository.module';
import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories';
import { DeviceRepositoryModule } from '@app/common/modules/device';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@Module({
imports: [ConfigModule],
imports: [
ConfigModule,
GroupRepositoryModule,
GroupDeviceRepositoryModule,
DeviceRepositoryModule,
],
controllers: [GroupController],
providers: [GroupService],
providers: [
GroupService,
GroupRepository,
GroupDeviceRepository,
DeviceRepository,
],
exports: [GroupService],
})
export class GroupModule {}

View File

@ -1,25 +1,15 @@
export class GetGroupDetailsInterface {
result: {
id: string;
name: string;
};
export interface GetGroupDetailsInterface {
groupUuid: string;
groupName: string;
createdAt: Date;
updatedAt: Date;
}
export class GetGroupsInterface {
result: {
count: number;
data_list: [];
};
export interface GetGroupsBySpaceUuidInterface {
groupUuid: string;
groupName: string;
}
export class addGroupInterface {
success: boolean;
msg: string;
result: {
id: string;
};
}
export class controlGroupInterface {
export interface controlGroupInterface {
success: boolean;
result: boolean;
msg: string;

View File

@ -1,21 +1,30 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { AddGroupDto } from '../dtos/add.group.dto';
import {
GetGroupDetailsInterface,
GetGroupsInterface,
addGroupInterface,
GetGroupsBySpaceUuidInterface,
controlGroupInterface,
} from '../interfaces/get.group.interface';
import { GetGroupDto } from '../dtos/get.group.dto';
import { ControlGroupDto } from '../dtos/control.group.dto';
import { RenameGroupDto } from '../dtos/rename.group.dto copy';
import { GroupRepository } from '@app/common/modules/group/repositories';
import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories';
@Injectable()
export class GroupService {
private tuya: TuyaContext;
constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
private readonly groupRepository: GroupRepository,
private readonly groupDeviceRepository: GroupDeviceRepository,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
// const clientId = this.configService.get<string>('auth-config.CLIENT_ID');
@ -26,83 +35,80 @@ export class GroupService {
});
}
async getGroupsByHomeId(getGroupDto: GetGroupDto) {
async getGroupsBySpaceUuid(
spaceUuid: string,
): Promise<GetGroupsBySpaceUuidInterface[]> {
try {
const response = await this.getGroupsTuya(getGroupDto);
const groups = response.result.data_list.map((group: any) => ({
groupId: group.id,
groupName: group.name,
}));
return {
count: response.result.count,
groups: groups,
};
} catch (error) {
throw new HttpException(
'Error fetching groups',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getGroupsTuya(getGroupDto: GetGroupDto): Promise<GetGroupsInterface> {
try {
const path = `/v2.0/cloud/thing/group`;
const response = await this.tuya.request({
method: 'GET',
path,
query: {
space_id: getGroupDto.homeId,
page_size: getGroupDto.pageSize,
page_no: getGroupDto.pageNo,
const groupDevices = await this.groupDeviceRepository.find({
relations: ['group', 'device'],
where: {
device: { spaceUuid },
isActive: true,
},
});
return response as unknown as GetGroupsInterface;
// Extract and return only the group entities
const groups = groupDevices.map((groupDevice) => {
return {
groupUuid: groupDevice.uuid,
groupName: groupDevice.group.groupName,
};
});
if (groups.length > 0) {
return groups;
} else {
throw new HttpException(
'this space has no groups',
HttpStatus.NOT_FOUND,
);
}
} catch (error) {
throw new HttpException(
'Error fetching groups ',
HttpStatus.INTERNAL_SERVER_ERROR,
error.message || 'Error fetching groups',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addGroup(addGroupDto: AddGroupDto) {
const response = await this.addGroupTuya(addGroupDto);
try {
const group = await this.groupRepository.save({
groupName: addGroupDto.groupName,
});
if (response.success) {
return {
success: true,
groupId: response.result.id,
};
} else {
const groupDevicePromises = addGroupDto.deviceUuids.map(
async (deviceUuid) => {
await this.saveGroupDevice(group.uuid, deviceUuid);
},
);
await Promise.all(groupDevicePromises);
} catch (err) {
if (err.code === '23505') {
throw new HttpException(
'User already belongs to this group',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
response.msg || 'Unknown error',
HttpStatus.BAD_REQUEST,
err.message || 'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addGroupTuya(addGroupDto: AddGroupDto): Promise<addGroupInterface> {
private async saveGroupDevice(groupUuid: string, deviceUuid: string) {
try {
const path = `/v2.0/cloud/thing/group`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
space_id: addGroupDto.homeId,
name: addGroupDto.groupName,
product_id: addGroupDto.productId,
device_ids: addGroupDto.deviceIds,
await this.groupDeviceRepository.save({
group: {
uuid: groupUuid,
},
device: {
uuid: deviceUuid,
},
});
return response as addGroupInterface;
} catch (error) {
throw new HttpException(
'Error adding group',
HttpStatus.INTERNAL_SERVER_ERROR,
);
throw error;
}
}
@ -141,105 +147,76 @@ export class GroupService {
}
}
async renameGroup(renameGroupDto: RenameGroupDto) {
const response = await this.renameGroupTuya(renameGroupDto);
if (response.success) {
return {
success: response.success,
result: response.result,
msg: response.msg,
};
} else {
throw new HttpException(
response.msg || 'Unknown error',
HttpStatus.BAD_REQUEST,
);
}
}
async renameGroupTuya(
async renameGroupByUuid(
groupUuid: string,
renameGroupDto: RenameGroupDto,
): Promise<controlGroupInterface> {
): Promise<GetGroupsBySpaceUuidInterface> {
try {
const path = `/v2.0/cloud/thing/group/${renameGroupDto.groupId}/${renameGroupDto.groupName}`;
const response = await this.tuya.request({
method: 'PUT',
path,
await this.groupRepository.update(
{ uuid: groupUuid },
{ groupName: renameGroupDto.groupName },
);
// Fetch the updated floor
const updatedGroup = await this.groupRepository.findOneOrFail({
where: { uuid: groupUuid },
});
return response as controlGroupInterface;
} catch (error) {
throw new HttpException(
'Error rename group',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteGroup(groupId: number) {
const response = await this.deleteGroupTuya(groupId);
if (response.success) {
return {
success: response.success,
result: response.result,
msg: response.msg,
};
} else {
throw new HttpException(
response.msg || 'Unknown error',
HttpStatus.BAD_REQUEST,
);
}
}
async deleteGroupTuya(groupId: number): Promise<controlGroupInterface> {
try {
const path = `/v2.0/cloud/thing/group/${groupId}`;
const response = await this.tuya.request({
method: 'DELETE',
path,
});
return response as controlGroupInterface;
} catch (error) {
throw new HttpException(
'Error delete group',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getGroupsByGroupId(groupId: number) {
try {
const response = await this.getGroupsByGroupIdTuya(groupId);
return {
groupId: response.result.id,
groupName: response.result.name,
groupUuid: updatedGroup.uuid,
groupName: updatedGroup.groupName,
};
} catch (error) {
throw new HttpException('Group not found', HttpStatus.NOT_FOUND);
}
}
async deleteGroup(groupUuid: string) {
try {
const group = await this.getGroupsByGroupUuid(groupUuid);
if (!group) {
throw new HttpException('Group not found', HttpStatus.NOT_FOUND);
}
await this.groupRepository.update(
{ uuid: groupUuid },
{ isActive: false },
);
return { message: 'Group deleted successfully' };
} catch (error) {
throw new HttpException(
'Error fetching group',
HttpStatus.INTERNAL_SERVER_ERROR,
error.message || 'Error deleting group',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getGroupsByGroupIdTuya(
groupId: number,
async getGroupsByGroupUuid(
groupUuid: string,
): Promise<GetGroupDetailsInterface> {
try {
const path = `/v2.0/cloud/thing/group/${groupId}`;
const response = await this.tuya.request({
method: 'GET',
path,
const group = await this.groupRepository.findOne({
where: {
uuid: groupUuid,
isActive: true,
},
});
return response as GetGroupDetailsInterface;
} catch (error) {
throw new HttpException(
'Error fetching group ',
HttpStatus.INTERNAL_SERVER_ERROR,
);
if (!group) {
throw new BadRequestException('Invalid group UUID');
}
return {
groupUuid: group.uuid,
groupName: group.groupName,
createdAt: group.createdAt,
updatedAt: group.updatedAt,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Group not found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -0,0 +1,71 @@
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
Injectable,
CanActivate,
HttpStatus,
BadRequestException,
ExecutionContext,
} from '@nestjs/common';
@Injectable()
export class CheckProductUuidForAllDevicesGuard implements CanActivate {
constructor(private readonly deviceRepository: DeviceRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { deviceUuids } = req.body;
console.log(deviceUuids);
await this.checkAllDevicesHaveSameProductUuid(deviceUuids);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
async checkAllDevicesHaveSameProductUuid(deviceUuids: string[]) {
const firstDevice = await this.deviceRepository.findOne({
where: { uuid: deviceUuids[0] },
});
if (!firstDevice) {
throw new BadRequestException('First device not found');
}
const firstProductUuid = firstDevice.productUuid;
for (let i = 1; i < deviceUuids.length; i++) {
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuids[i] },
});
if (!device) {
throw new BadRequestException(`Device ${deviceUuids[i]} not found`);
}
if (device.productUuid !== firstProductUuid) {
throw new BadRequestException(`Devices have different product UUIDs`);
}
}
}
private handleGuardError(error: Error, context: ExecutionContext) {
const response = context.switchToHttp().getResponse();
console.error(error);
if (error instanceof BadRequestException) {
response
.status(HttpStatus.BAD_REQUEST)
.json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message });
} else {
response.status(HttpStatus.NOT_FOUND).json({
statusCode: HttpStatus.NOT_FOUND,
message: 'Device not found',
});
}
}
}