Merge branch 'dev' into feat/project-tag

This commit is contained in:
hannathkadher
2025-03-02 00:22:28 +04:00
26 changed files with 320 additions and 88 deletions

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
/node_modules
/build
#github
/.github
# Logs
logs
*.log

View File

@ -558,6 +558,15 @@ export class ControllerRoute {
};
};
static DEVICE_PROJECT = class {
public static readonly ROUTE = '/projects/:projectUuid/devices';
static ACTIONS = class {
public static readonly GET_ALL_DEVICES_SUMMARY = 'Get all devices';
public static readonly GET_ALL_DEVICES_DESCRIPTION =
'This endpoint retrieves all devices in the system.';
};
};
static DEVICE_PERMISSION = class {
public static readonly ROUTE = 'device-permission';
@ -698,6 +707,8 @@ export class ControllerRoute {
};
static VISITOR_PASSWORD = class {
public static readonly ROUTE = 'visitor-password';
public static readonly PROJECT_ROUTE =
'/projects/:projectUuid/visitor-password';
static ACTIONS = class {
public static readonly ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY =

View File

@ -10,7 +10,13 @@ import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { firebaseDataBase } from '../../firebase.config';
import { Database, DataSnapshot, get, ref, set } from 'firebase/database';
import {
Database,
DataSnapshot,
get,
ref,
runTransaction,
} from 'firebase/database';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@Injectable()
export class DeviceStatusFirebaseService {
@ -154,39 +160,48 @@ export class DeviceStatusFirebaseService {
this.firebaseDb,
`device-status/${addDeviceStatusDto.deviceUuid}`,
);
const snapshot: DataSnapshot = await get(dataRef);
const existingData = snapshot.val() || {};
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData.status) {
existingData.status = [];
}
// Use a transaction to handle concurrent updates
await runTransaction(dataRef, (existingData) => {
if (!existingData) {
existingData = {};
}
// Create a map to track existing status codes
const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]),
);
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData.status) {
existingData.status = [];
}
// Update or add status codes
// Create a map to track existing status codes
const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]),
);
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Update or add status codes
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
return existingData;
});
// Save logs to your repository
const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
@ -200,10 +215,9 @@ export class DeviceStatusFirebaseService {
});
});
await this.deviceStatusLogRepository.save(newLogs);
// Save the updated data to Firebase
await set(dataRef, existingData);
// Return the updated data
return existingData;
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();
}
}

View File

@ -16,6 +16,7 @@ import {
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -32,6 +33,7 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneRepository,
SceneDeviceRepository,
AutomationRepository,
ProjectRepository,
],
exports: [AutomationService],
})

View File

@ -0,0 +1,29 @@
import { DeviceService } from '../services/device.service';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
import { ProjectParam } from '../dtos';
@ApiTags('Device Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.DEVICE_PROJECT.ROUTE,
})
export class DeviceProjectController {
constructor(private readonly deviceService: DeviceService) {}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('DEVICE_VIEW')
@Get()
@ApiOperation({
summary: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_SUMMARY,
description: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_DESCRIPTION,
})
async getAllDevices(@Param() param: ProjectParam) {
return await this.deviceService.getAllDevices(param);
}
}

View File

@ -224,17 +224,6 @@ export class DeviceController {
async getDevicesInGateway(@Param('gatewayUuid') gatewayUuid: string) {
return await this.deviceService.getDevicesInGateway(gatewayUuid);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('DEVICE_VIEW')
@Get()
@ApiOperation({
summary: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_SUMMARY,
description: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_DESCRIPTION,
})
async getAllDevices() {
return await this.deviceService.getAllDevices();
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)

View File

@ -20,6 +20,8 @@ import {
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { DeviceProjectController } from './controllers/device-project.controller';
@Module({
imports: [
ConfigModule,
@ -28,13 +30,14 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
DeviceRepositoryModule,
DeviceStatusFirebaseModule,
],
controllers: [DeviceController],
controllers: [DeviceController, DeviceProjectController],
providers: [
DeviceService,
ProductRepository,
DeviceUserPermissionRepository,
PermissionTypeRepository,
SpaceRepository,
ProjectRepository,
DeviceRepository,
UserRepository,
TuyaService,

View File

@ -1,3 +1,4 @@
export * from './add.device.dto';
export * from './control.device.dto';
export * from './get.device.dto';
export * from './project.param.dto';

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class ProjectParam {
@ApiProperty({
description: 'UUID of the project this community belongs to',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
projectUuid: string;
}

View File

@ -60,6 +60,8 @@ import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProjectParam } from '../dtos';
@Injectable()
export class DeviceService {
@ -74,6 +76,7 @@ export class DeviceService {
@Inject(forwardRef(() => SceneService))
private readonly sceneService: SceneService,
private readonly tuyaService: TuyaService,
private readonly projectRepository: ProjectRepository,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -950,10 +953,19 @@ export class DeviceService {
);
}
}
async getAllDevices(): Promise<GetDeviceDetailsInterface[]> {
async getAllDevices(
param: ProjectParam,
): Promise<GetDeviceDetailsInterface[]> {
try {
await this.validateProject(param.projectUuid);
const devices = await this.deviceRepository.find({
where: { isActive: true },
where: {
isActive: true,
spaceDevice: {
community: { project: { uuid: param.projectUuid } },
},
},
relations: [
'spaceDevice.parent',
'spaceDevice.community',
@ -1009,10 +1021,13 @@ export class DeviceService {
}
}
const spaceHierarchy = await this.getFullSpaceHierarchy(
const spaceHierarchy = await this.getParentHierarchy(
device?.spaceDevice,
);
const orderedHierarchy = spaceHierarchy.reverse();
const orderedHierarchy = [
device?.spaceDevice,
...spaceHierarchy.reverse(),
];
return {
spaces: orderedHierarchy.map((space) => ({
@ -1490,4 +1505,45 @@ export class DeviceService {
);
}
}
private async validateProject(uuid: string) {
const project = await this.projectRepository.findOne({
where: { uuid },
});
if (!project) {
throw new HttpException(
`A project with the uuid '${uuid}' doesn't exists.`,
HttpStatus.BAD_REQUEST,
);
}
return project;
}
async getParentHierarchy(
space: SpaceEntity,
): Promise<{ uuid: string; spaceName: string }[]> {
try {
const targetSpace = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['parent'],
});
if (!targetSpace) {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
const ancestors = await this.fetchAncestors(targetSpace);
return ancestors.map((parentSpace) => ({
uuid: parentSpace.uuid,
spaceName: parentSpace.spaceName,
}));
} catch (error) {
console.error('Error fetching parent hierarchy:', error.message);
throw new HttpException(
'Error fetching parent hierarchy',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -19,6 +19,7 @@ import {
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
@Module({
imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController],
@ -32,6 +33,7 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
DeviceStatusFirebaseService,
SpaceRepository,
DeviceStatusLogRepository,
ProjectRepository,
TuyaService,
SceneService,
SceneIconRepository,

View File

@ -15,6 +15,7 @@ import {
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -28,6 +29,7 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
ProductRepository,
SceneIconRepository,
SceneRepository,
ProjectRepository,
SceneDeviceRepository,
AutomationRepository,
],

View File

@ -1,9 +1,11 @@
import { SpaceModelEntity } from '@app/common/modules/space-model';
import { QueryRunner } from 'typeorm';
export class PropogateDeleteSpaceModelCommand {
constructor(
public readonly param: {
spaceModel: SpaceModelEntity;
queryRunner: QueryRunner;
},
) {}
}

View File

@ -19,8 +19,7 @@ export class PropogateDeleteSpaceModelHandler
) {}
async execute(command: PropogateDeleteSpaceModelCommand): Promise<void> {
const { spaceModel } = command.param;
const queryRunner = this.dataSource.createQueryRunner();
const { spaceModel, queryRunner } = command.param;
try {
await queryRunner.connect();

View File

@ -25,6 +25,7 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommandBus } from '@nestjs/cqrs';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
import { PropogateDeleteSpaceModelCommand } from '../commands';
import {
SpaceProductAllocationRepository,
SpaceRepository,
@ -283,6 +284,13 @@ export class SpaceModelService {
{ disabled: true },
);
await this.commandBus.execute(
new PropogateDeleteSpaceModelCommand({
spaceModel: spaceModel,
queryRunner,
}),
);
await queryRunner.commitTransaction();
return new SuccessResponseDto({

View File

@ -32,10 +32,10 @@ export class SpaceDeviceService {
);
}
// Fetch space hierarchy **once** and reverse it to get an ordered hierarchy
const spaceHierarchy =
await this.validationService.getFullSpaceHierarchy(space);
const orderedHierarchy = spaceHierarchy.reverse();
await this.validationService.getParentHierarchy(space);
const orderedHierarchy = [space, ...spaceHierarchy.reverse()];
// Fetch Tuya details for each device in parallel using Promise.allSettled
const deviceDetailsPromises = space.devices.map((device) =>

View File

@ -222,4 +222,32 @@ export class ValidationService {
return descendants;
}
async getParentHierarchy(
space: SpaceEntity,
): Promise<{ uuid: string; spaceName: string }[]> {
try {
const targetSpace = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['parent'],
});
if (!targetSpace) {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
const ancestors = await this.fetchAncestors(targetSpace);
return ancestors.map((parentSpace) => ({
uuid: parentSpace.uuid,
spaceName: parentSpace.spaceName,
}));
} catch (error) {
console.error('Error fetching parent hierarchy:', error.message);
throw new HttpException(
'Error fetching parent hierarchy',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -84,7 +84,7 @@ export class SubspaceDeviceService {
if (device.tag?.subspace?.uuid !== subspace.uuid) {
await this.tagRepository.update(
{ uuid: device.tag.uuid },
{ subspace },
{ subspace, space: null },
);
}

View File

@ -30,7 +30,7 @@ export class UserService {
where: {
uuid: userUuid,
},
relations: ['region', 'timezone', 'roleType'],
relations: ['region', 'timezone', 'roleType', 'project'],
});
if (!user) {
throw new BadRequestException('Invalid room UUID');
@ -52,6 +52,7 @@ export class UserService {
hasAcceptedAppAgreement: user?.hasAcceptedAppAgreement,
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
role: user?.roleType,
project: user?.project,
};
} catch (err) {
if (err instanceof BadRequestException) {

View File

@ -1 +1,2 @@
export * from './visitor-password.controller';
export * from './project-visitor-password.controller';

View File

@ -0,0 +1,48 @@
import { VisitorPasswordService } from '../services/visitor-password.service';
import { Controller, UseGuards, Get, Param } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { ProjectParam } from 'src/community/dtos';
import { PermissionsGuard } from 'src/guards/permissions.guard';
@ApiTags('Visitor Password Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.VISITOR_PASSWORD.PROJECT_ROUTE,
})
export class VisitorPasswordProjectController {
constructor(
private readonly visitorPasswordService: VisitorPasswordService,
) {}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('VISITOR_PASSWORD_ADD')
@Get()
@ApiOperation({
summary:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_SUMMARY,
description:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_DESCRIPTION,
})
async GetVisitorPassword(@Param() param: ProjectParam) {
return await this.visitorPasswordService.getPasswords(param);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('VISITOR_PASSWORD_VIEW')
@Get('/devices')
@ApiOperation({
summary:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_SUMMARY,
description:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_DESCRIPTION,
})
async GetVisitorDevices(@Param() param: ProjectParam) {
return await this.visitorPasswordService.getAllPassDevices(param);
}
}

View File

@ -5,7 +5,6 @@ import {
Post,
HttpStatus,
UseGuards,
Get,
Req,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
@ -146,32 +145,4 @@ export class VisitorPasswordController {
data: temporaryPassword,
};
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('VISITOR_PASSWORD_VIEW')
@Get()
@ApiOperation({
summary:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_SUMMARY,
description:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_DESCRIPTION,
})
async GetVisitorPassword() {
return await this.visitorPasswordService.getPasswords();
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('VISITOR_PASSWORD_VIEW')
@Get('/devices')
@ApiOperation({
summary:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_SUMMARY,
description:
ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_DESCRIPTION,
})
async GetVisitorDevices() {
return await this.visitorPasswordService.getAllPassDevices();
}
}

View File

@ -1 +1,2 @@
export * from './temp-pass.dto';
export * from './project.param.dto';

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class ProjectParam {
@ApiProperty({
description: 'UUID of the project this community belongs to',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
projectUuid: string;
}

View File

@ -14,6 +14,7 @@ import {
AddDoorLockOfflineOneTimeDto,
AddDoorLockOnlineMultipleDto,
AddDoorLockOnlineOneTimeDto,
ProjectParam,
} from '../dtos';
import { EmailService } from '@app/common/util/email.service';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
@ -30,6 +31,7 @@ import {
CommonHourMinutes,
CommonHours,
} from '@app/common/constants/hours-minutes.enum';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
@Injectable()
export class VisitorPasswordService {
@ -42,6 +44,7 @@ export class VisitorPasswordService {
private readonly doorLockService: DoorLockService,
private readonly deviceService: DeviceService,
private readonly passwordEncryptionService: PasswordEncryptionService,
private readonly projectRepository: ProjectRepository,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -444,12 +447,21 @@ export class VisitorPasswordService {
);
}
}
async getPasswords() {
async getPasswords(param: ProjectParam) {
await this.validateProject(param.projectUuid);
const deviceIds = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
spaceDevice: {
community: {
project: {
uuid: param.projectUuid,
},
},
},
isActive: true,
},
});
@ -487,15 +499,25 @@ export class VisitorPasswordService {
});
}
async getAllPassDevices() {
async getAllPassDevices(param: ProjectParam) {
await this.validateProject(param.projectUuid);
const devices = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
spaceDevice: {
community: {
project: {
uuid: param.projectUuid,
},
},
},
isActive: true,
},
relations: ['productDevice'],
relations: ['productDevice', 'spaceDevice'],
});
const devicesData = await Promise.all(
devices?.map(async (device) => {
@ -509,6 +531,7 @@ export class VisitorPasswordService {
productType: device.productDevice.prodType,
...deviceDetails,
uuid: device.uuid,
spaceName: device.spaceDevice.spaceName,
} as GetDeviceDetailsInterface;
} catch (error) {
console.error(
@ -907,4 +930,17 @@ export class VisitorPasswordService {
);
}
}
private async validateProject(uuid: string) {
const project = await this.projectRepository.findOne({
where: { uuid },
});
if (!project) {
throw new HttpException(
`A project with the uuid '${uuid}' doesn't exists.`,
HttpStatus.BAD_REQUEST,
);
}
return project;
}
}

View File

@ -21,9 +21,11 @@ import {
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { VisitorPasswordProjectController } from './controllers';
@Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController],
controllers: [VisitorPasswordController, VisitorPasswordProjectController],
providers: [
VisitorPasswordService,
EmailService,
@ -41,6 +43,7 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneRepository,
SceneDeviceRepository,
AutomationRepository,
ProjectRepository,
],
exports: [VisitorPasswordService],
})