merged with dev

This commit is contained in:
unknown
2024-08-21 11:16:05 +03:00
19 changed files with 1592 additions and 63 deletions

View File

@ -1,75 +1,90 @@
ACCESS_KEY=
AZURE_POSTGRESQL_DATABASE=
AZURE_POSTGRESQL_HOST=
AZURE_POSTGRESQL_PASSWORD=
AZURE_POSTGRESQL_PORT=
AZURE_POSTGRESQL_SSL=
AZURE_POSTGRESQL_SYNC=
AZURE_POSTGRESQL_USER=
AZURE_REDIS_CONNECTIONSTRING=
BASE_URL=
DB_SYNC=
DOCKER_REGISTRY_SERVER_PASSWORD=
DOCKER_REGISTRY_SERVER_URL=
DOCKER_REGISTRY_SERVER_USERNAME=
DOPPLER_CONFIG=
DOPPLER_ENVIRONMENT=
DOPPLER_PROJECT=
JWT_EXPIRE_TIME=
JWT_EXPIRE_TIME_REFRESH=
JWT_SECRET=
JWT_SECRET_REFRESH=
SECRET_KEY=
SMTP_HOST=
SMTP_PASSWORD=
SMTP_PORT=
SMTP_SECURE=
SMTP_USER=
WEBSITES_ENABLE_APP_SERVICE_STORAGE=
PORT=
SUPER_ADMIN_EMAIL=
SUPER_ADMIN_PASSWORD=
TUYA_ACCESS_ID=
TUYA_ACCESS_KEY=
ONESIGNAL_APP_ID=
ONESIGNAL_API_KEY=
TRUN_ON_TUYA_SOCKET=
TUYA_EU_URL=
MONGODB_URI=
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=d
FIREBASE_MEASUREMENT_ID=
FIREBASE_DATABASE_URL=

View File

@ -0,0 +1 @@
export const defaultDoorLockPass = '1233214';

View File

@ -129,7 +129,7 @@ export const allCountries = [
'Oman',
'Pakistan',
'Palau',
'Palestine State',
'Palestine',
'Panama',
'Papua New Guinea',
'Paraguay',

View File

@ -19,7 +19,7 @@ export class EmailService {
};
}
async sendOTPEmail(
async sendEmail(
email: string,
subject: string,
message: string,

View File

@ -23,6 +23,7 @@ import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { AutomationModule } from './automation/automation.module';
import { RegionModule } from './region/region.module';
import { TimeZoneModule } from './timezone/timezone.module';
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -48,6 +49,7 @@ import { TimeZoneModule } from './timezone/timezone.module';
DoorLockModule,
RegionModule,
TimeZoneModule,
VisitorPasswordModule,
],
controllers: [AuthenticationController],
providers: [

View File

@ -199,7 +199,7 @@ export class UserAuthService {
});
const subject = 'OTP send successfully';
const message = `Your OTP code is ${otpCode}`;
this.emailService.sendOTPEmail(data.email, subject, message);
this.emailService.sendEmail(data.email, subject, message);
return otpCode;
}

View File

@ -18,10 +18,10 @@ import {
} from '../dtos/add.community.dto';
import { GetCommunityChildDto } from '../dtos/get.community.dto';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { CheckUserCommunityGuard } from 'src/guards/user.community.guard';
// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { CommunityPermissionGuard } from 'src/guards/community.permission.guard';
// import { CommunityPermissionGuard } from 'src/guards/community.permission.guard';
@ApiTags('Community Module')
@Controller({
@ -53,7 +53,7 @@ export class CommunityController {
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CommunityPermissionGuard)
@UseGuards(JwtAuthGuard)
@Get(':communityUuid')
async getCommunityByUuid(@Param('communityUuid') communityUuid: string) {
try {
@ -67,9 +67,22 @@ export class CommunityController {
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CommunityPermissionGuard)
@UseGuards(JwtAuthGuard)
@Get()
async getCommunities() {
try {
const communities = await this.communityService.getCommunities();
return communities;
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('child/:communityUuid')
async getCommunityChildByUuid(
@Param('communityUuid') communityUuid: string,
@ -103,7 +116,7 @@ export class CommunityController {
}
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard, CheckUserCommunityGuard)
@UseGuards(AdminRoleGuard)
@Post('user')
async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) {
try {
@ -121,7 +134,7 @@ export class CommunityController {
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CommunityPermissionGuard)
@UseGuards(JwtAuthGuard)
@Put('rename/:communityUuid')
async renameCommunityByUuid(
@Param('communityUuid') communityUuid: string,

View File

@ -24,3 +24,12 @@ export interface GetCommunityByUserUuidInterface {
name: string;
type: string;
}
export interface Community {
uuid: string;
createdAt: Date;
updatedAt: Date;
name: string;
type: string;
}
export interface GetCommunitiesInterface extends Array<Community> {}

View File

@ -10,6 +10,7 @@ import { SpaceRepository } from '@app/common/modules/space/repositories';
import { AddCommunityDto, AddUserCommunityDto } from '../dtos';
import {
CommunityChildInterface,
GetCommunitiesInterface,
GetCommunityByUserUuidInterface,
GetCommunityByUuidInterface,
RenameCommunityByUuidInterface,
@ -79,6 +80,23 @@ export class CommunityService {
}
}
}
async getCommunities(): Promise<GetCommunitiesInterface> {
try {
const community = await this.spaceRepository.find({
where: { spaceType: { type: 'community' } },
relations: ['spaceType'],
});
return community.map((community) => ({
uuid: community.uuid,
createdAt: community.createdAt,
updatedAt: community.updatedAt,
name: community.spaceName,
type: community.spaceType.type,
}));
} catch (err) {
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getCommunityChildByUuid(
communityUuid: string,
getCommunityChildDto: GetCommunityChildDto,

View File

@ -211,4 +211,23 @@ export class DoorLockController {
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('open/:doorLockUuid')
async openDoorLock(@Param('doorLockUuid') doorLockUuid: string) {
try {
await this.doorLockService.openDoorLock(doorLockUuid);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'door lock opened successfully',
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,4 +1,9 @@
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 {
@ -16,6 +21,7 @@ import { PasswordEncryptionService } from './encryption.services';
import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto';
import { defaultDoorLockPass } from '@app/common/constants/default.door-lock-pass';
@Injectable()
export class DoorLockService {
@ -83,7 +89,10 @@ export class DoorLockService {
);
}
}
async getOfflineMultipleTimeTemporaryPasswords(doorLockUuid: string) {
async getOfflineMultipleTimeTemporaryPasswords(
doorLockUuid: string,
fromVisitor?: boolean,
) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid);
@ -99,11 +108,25 @@ export class DoorLockService {
deviceDetails.deviceTuyaUuid,
'multiple',
);
if (passwords.result.records.length > 0) {
return convertKeysToCamelCase(passwords.result.records);
if (!passwords.result.records.length && fromVisitor) {
throw new BadRequestException();
}
if (passwords.result.records.length > 0) {
return fromVisitor
? convertKeysToCamelCase(passwords.result.records).map((password) => {
return {
passwordId: `${password.pwdId}`,
invalidTime: `${password.gmtExpired}`,
effectiveTime: `${password.gmtStart}`,
passwordCreated: `${password.gmtCreate}`,
createdTime: password.pwdName,
passwordStatus: `${password.status}`,
passwordType: 'OFFLINE_MULTIPLE',
deviceUuid: doorLockUuid,
};
})
: convertKeysToCamelCase(passwords.result.records);
}
return passwords;
} catch (error) {
throw new HttpException(
@ -113,10 +136,12 @@ export class DoorLockService {
);
}
}
async getOfflineOneTimeTemporaryPasswords(doorLockUuid: string) {
async getOfflineOneTimeTemporaryPasswords(
doorLockUuid: string,
fromVisitor?: boolean,
) {
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) {
@ -129,9 +154,24 @@ export class DoorLockService {
deviceDetails.deviceTuyaUuid,
'once',
);
if (!passwords.result.records.length && fromVisitor) {
throw new BadRequestException();
}
if (passwords.result.records.length > 0) {
return convertKeysToCamelCase(passwords.result.records);
return fromVisitor
? convertKeysToCamelCase(passwords.result.records).map((password) => {
return {
passwordId: `${password.pwdId}`,
invalidTime: `${password.gmtExpired}`,
effectiveTime: `${password.gmtStart}`,
createdTime: `${password.gmtCreate}`,
passwordName: password.pwdName,
passwordStatus: `${password.status}`,
passwordType: 'OFFLINE_ONETIME',
deviceUuid: doorLockUuid,
};
})
: convertKeysToCamelCase(passwords.result.records);
}
return passwords;
@ -142,7 +182,10 @@ export class DoorLockService {
);
}
}
async getOnlineTemporaryPasswords(doorLockUuid: string) {
async getOnlineTemporaryPasswords(
doorLockUuid: string,
fromVisitor?: boolean,
) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid);
@ -157,8 +200,7 @@ export class DoorLockService {
const passwords = await this.getOnlineTemporaryPasswordsTuya(
deviceDetails.deviceTuyaUuid,
);
if (passwords.result.length > 0) {
if (passwords.result?.length > 0) {
const passwordFiltered = passwords.result
.filter((item) => item.type === 0)
.map((password: any) => {
@ -181,9 +223,25 @@ export class DoorLockService {
return password;
});
return convertKeysToCamelCase(passwordFiltered);
return fromVisitor
? convertKeysToCamelCase(passwordFiltered).map((password) => {
return {
passwodId: `${password.id}`,
invalidTime: `${password.invalidTime}`,
effectiveTime: `${password.effectiveTime}`,
createdTime: '',
scheduleList: password?.scheduleList,
passwodName: password.name,
passwordStatus: '',
passwordType: 'ONLINE_MULTIPLE',
deviceUuid: doorLockUuid,
};
})
: convertKeysToCamelCase(passwordFiltered);
}
if (fromVisitor) {
throw new BadRequestException();
}
return passwords;
} catch (error) {
throw new HttpException(
@ -192,6 +250,74 @@ export class DoorLockService {
);
}
}
async getOnlineTemporaryPasswordsOneTime(
doorLockUuid: string,
fromVisitor?: boolean,
) {
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.getOnlineTemporaryPasswordsTuya(
deviceDetails.deviceTuyaUuid,
);
if (passwords.result?.length > 0) {
const passwordFiltered = passwords.result
.filter((item) => item.type === 1)
.map((password: any) => {
if (password.schedule_list?.length > 0) {
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 fromVisitor
? convertKeysToCamelCase(passwordFiltered).map((password) => {
return {
passwodId: `${password.id}`,
invalidTime: `${password.invalidTime}`,
effectiveTime: `${password.effectiveTime}`,
createdTime: '',
passwodName: password.name,
passwordStatus: '',
passwordType: 'ONLINE_ONETIME',
deviceUuid: doorLockUuid,
};
})
: convertKeysToCamelCase(passwordFiltered);
}
if (fromVisitor) {
throw new BadRequestException();
}
return passwords;
} catch (error) {
throw new HttpException(
error.message || 'Error getting online temporary passwords',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getOnlineTemporaryPasswordsTuya(
doorLockUuid: string,
): Promise<getPasswordInterface> {
@ -639,4 +765,56 @@ export class DoorLockService {
);
}
}
async openDoorLock(
doorLockUuid: string,
): Promise<deleteTemporaryPasswordInterface> {
try {
// Fetch ticket and encrypted password
const { ticketKey, encryptedPassword, deviceTuyaUuid, ticketId } =
await this.getTicketAndEncryptedPassword(
doorLockUuid,
defaultDoorLockPass,
);
// Validate ticket and device Tuya UUID
if (!ticketKey || !encryptedPassword || !deviceTuyaUuid) {
throw new HttpException(
'Invalid Ticket or Device UUID',
HttpStatus.NOT_FOUND,
);
}
// Retrieve device details
const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid);
// Validate device details
if (!deviceDetails?.deviceTuyaUuid) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (deviceDetails.productDevice.prodType !== ProductType.DL) {
throw new HttpException(
'This is not a door lock device',
HttpStatus.BAD_REQUEST,
);
}
// Construct the path for the Tuya API request
const path = `/v1.0/devices/${deviceDetails.deviceTuyaUuid}/door-lock/password-free/open-door`;
// Make the Tuya API request to open the door
const response = await this.tuya.request({
method: 'POST',
path,
body: { ticket_id: ticketId },
});
return response as deleteTemporaryPasswordInterface;
} catch (error) {
const status = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
const message = error.message || 'Error opening door lock from Tuya';
throw new HttpException(message, status);
}
}
}

View File

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

View File

@ -0,0 +1,148 @@
import { VisitorPasswordService } from '../services/visitor-password.service';
import {
Body,
Controller,
Post,
HttpException,
HttpStatus,
UseGuards,
Get,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import {
AddDoorLockOfflineMultipleDto,
AddDoorLockOfflineOneTimeDto,
AddDoorLockOnlineMultipleDto,
AddDoorLockOnlineOneTimeDto,
} from '../dtos/temp-pass.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
@ApiTags('Visitor Password Module')
@Controller({
version: '1',
path: 'visitor-password',
})
export class VisitorPasswordController {
constructor(
private readonly visitorPasswordService: VisitorPasswordService,
) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('temporary-password/online/multiple-time')
async addOnlineTemporaryPasswordMultipleTime(
@Body() addDoorLockOnlineMultipleDto: AddDoorLockOnlineMultipleDto,
) {
try {
const temporaryPasswords =
await this.visitorPasswordService.addOnlineTemporaryPasswordMultipleTime(
addDoorLockOnlineMultipleDto,
);
return {
statusCode: HttpStatus.CREATED,
data: temporaryPasswords,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('temporary-password/online/one-time')
async addOnlineTemporaryPassword(
@Body() addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto,
) {
try {
const temporaryPasswords =
await this.visitorPasswordService.addOnlineTemporaryPasswordOneTime(
addDoorLockOnlineOneTimeDto,
);
return {
statusCode: HttpStatus.CREATED,
data: temporaryPasswords,
};
} 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')
async addOfflineOneTimeTemporaryPassword(
@Body() addDoorLockOfflineOneTimeDto: AddDoorLockOfflineOneTimeDto,
) {
try {
const temporaryPassword =
await this.visitorPasswordService.addOfflineOneTimeTemporaryPassword(
addDoorLockOfflineOneTimeDto,
);
return {
statusCode: HttpStatus.CREATED,
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')
async addOfflineMultipleTimeTemporaryPassword(
@Body()
addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto,
) {
try {
const temporaryPassword =
await this.visitorPasswordService.addOfflineMultipleTimeTemporaryPassword(
addDoorLockOfflineMultipleDto,
);
return {
statusCode: HttpStatus.CREATED,
data: temporaryPassword,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async GetVisitorPassword() {
try {
return await this.visitorPasswordService.getPasswords();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('/devices')
async GetVisitorDevices() {
try {
return await this.visitorPasswordService.getAllPassDevices();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1 @@
export * from './temp-pass.dto';

View File

@ -0,0 +1,213 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsString,
IsArray,
ValidateNested,
IsEnum,
Length,
IsOptional,
IsEmail,
} 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 AddDoorLockOnlineMultipleDto {
@ApiProperty({
description: 'email',
required: true,
})
@IsEmail()
@IsNotEmpty()
public email: string;
@ApiProperty({
description: 'devicesUuid',
required: true,
})
@IsArray()
@IsNotEmpty()
public devicesUuid: [string];
@ApiProperty({
description: 'password name',
required: true,
})
@IsString()
@IsNotEmpty()
public passwordName: 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 })
@IsOptional()
@Type(() => ScheduleDto)
public scheduleList: ScheduleDto[];
}
export class AddDoorLockOnlineOneTimeDto {
@ApiProperty({
description: 'email',
required: true,
})
@IsEmail()
@IsNotEmpty()
public email: string;
@ApiProperty({
description: 'devicesUuid',
required: true,
})
@IsArray()
@IsNotEmpty()
public devicesUuid: [string];
@ApiProperty({
description: 'password name',
required: true,
})
@IsString()
@IsNotEmpty()
public passwordName: 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;
}
export class AddDoorLockOfflineOneTimeDto {
@ApiProperty({
description: 'email',
required: true,
})
@IsEmail()
@IsNotEmpty()
public email: string;
@ApiProperty({
description: 'password name',
required: true,
})
@IsString()
@IsNotEmpty()
public passwordName: string;
@ApiProperty({
description: 'devicesUuid',
required: true,
})
@IsArray()
@IsNotEmpty()
public devicesUuid: [string];
}
export class AddDoorLockOfflineMultipleDto {
@ApiProperty({
description: 'email',
required: true,
})
@IsEmail()
@IsNotEmpty()
public email: string;
@ApiProperty({
description: 'devicesUuid',
required: true,
})
@IsArray()
@IsNotEmpty()
public devicesUuid: [string];
@ApiProperty({
description: 'password name',
required: true,
})
@IsString()
@IsNotEmpty()
public passwordName: string;
@ApiProperty({
description: 'effectiveTime',
required: true,
})
@IsString()
@IsNotEmpty()
public effectiveTime: string;
@ApiProperty({
description: 'invalidTime',
required: true,
})
@IsString()
@IsNotEmpty()
public invalidTime: string;
}

View File

@ -0,0 +1,66 @@
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;
offline_temp_password_id?: string;
offline_temp_password?: string;
};
msg?: string;
}
export interface addDeviceObjectInterface {
passwordName: string;
encryptedPassword: string;
effectiveTime: string;
invalidTime: string;
ticketId: string;
scheduleList?: any[];
}
export interface ScheduleDto {
effectiveTime: string;
invalidTime: string;
workingDay: WorkingDays[];
}
export interface AddDoorLockOnlineInterface {
passwordName: string;
password: string;
effectiveTime: string;
invalidTime: string;
scheduleList?: ScheduleDto[];
}
export interface getPasswordInterface {
success: boolean;
result: [
{
effective_time: number;
id: number;
invalid_time: number;
passwordName: 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;
}
export interface getPasswordOfflineInterface {
success: boolean;
result: {
records: [];
};
msg?: string;
}

View File

@ -0,0 +1 @@
export * from './visitor-password.service';

View File

@ -0,0 +1,815 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import {
addDeviceObjectInterface,
createTickInterface,
} from '../interfaces/visitor-password.interface';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import {
AddDoorLockOfflineMultipleDto,
AddDoorLockOfflineOneTimeDto,
AddDoorLockOnlineMultipleDto,
AddDoorLockOnlineOneTimeDto,
} from '../dtos';
import { EmailService } from '@app/common/util/email.service';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
import { DoorLockService } from 'src/door-lock/services';
import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface';
import { DeviceService } from 'src/device/services';
@Injectable()
export class VisitorPasswordService {
private tuya: TuyaContext;
constructor(
private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository,
private readonly emailService: EmailService,
private readonly doorLockService: DoorLockService,
private readonly deviceService: DeviceService,
private readonly passwordEncryptionService: PasswordEncryptionService,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
const tuyaEuUrl = this.configService.get<string>('tuya-config.TUYA_EU_URL');
this.tuya = new TuyaContext({
baseUrl: tuyaEuUrl,
accessKey,
secretKey,
});
}
async addOfflineMultipleTimeTemporaryPassword(
addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto,
) {
try {
const deviceResults = await Promise.allSettled(
addDoorLockOfflineMultipleDto.devicesUuid.map(async (deviceUuid) => {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails || !deviceDetails.deviceTuyaUuid) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (deviceDetails.productDevice.prodType !== ProductType.DL) {
throw new HttpException(
'This is not a door lock device',
HttpStatus.BAD_REQUEST,
);
}
const createMultipleOfflinePass =
await this.addOfflineTemporaryPasswordTuya(
deviceDetails.deviceTuyaUuid,
'multiple',
addDoorLockOfflineMultipleDto,
addDoorLockOfflineMultipleDto.passwordName,
);
if (!createMultipleOfflinePass.success) {
throw new HttpException(
createMultipleOfflinePass.msg,
HttpStatus.BAD_REQUEST,
);
}
// Send email if the password creation is successful
if (createMultipleOfflinePass.result.offline_temp_password_id) {
const emailSubject = 'One-Time Password Creation Success';
const emailBody = `Your multiple time offline temporary password "${createMultipleOfflinePass.result.offline_temp_password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`;
await this.emailService.sendEmail(
addDoorLockOfflineMultipleDto.email,
emailSubject,
emailBody,
);
}
return {
success: true,
result: createMultipleOfflinePass.result,
deviceUuid,
};
} catch (error) {
return {
success: false,
deviceUuid,
error: error.message || 'Error processing device',
};
}
}),
);
// Process results: separate successful and failed operations
const successfulResults = deviceResults
.filter(
(result) =>
result.status === 'fulfilled' &&
(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) => (result as PromiseFulfilledResult<any>).value);
const failedResults = deviceResults
.filter(
(result) =>
result.status === 'rejected' ||
!(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) =>
result.status === 'rejected'
? {
success: false,
error:
(result as PromiseRejectedResult).reason.message ||
'Error processing device',
}
: (result as PromiseFulfilledResult<any>).value,
);
// Return results if there are successful operations
if (successfulResults.length > 0) {
return {
successOperations: successfulResults,
failedOperations: failedResults,
};
}
// If all failed, throw an error with all failure messages
throw new HttpException(
failedResults.map((res) => res.error).join('; ') ||
'All device operations failed',
HttpStatus.BAD_REQUEST,
);
} catch (error) {
console.error(error);
throw new HttpException(
error.message ||
'Error adding offline multiple-time temporary password from Tuya',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addOfflineOneTimeTemporaryPassword(
addDoorLockOfflineOneTimeDto: AddDoorLockOfflineOneTimeDto,
) {
try {
const deviceResults = await Promise.allSettled(
addDoorLockOfflineOneTimeDto.devicesUuid.map(async (deviceUuid) => {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails || !deviceDetails.deviceTuyaUuid) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (deviceDetails.productDevice.prodType !== ProductType.DL) {
throw new HttpException(
'This is not a door lock device',
HttpStatus.BAD_REQUEST,
);
}
const createOnceOfflinePass =
await this.addOfflineTemporaryPasswordTuya(
deviceDetails.deviceTuyaUuid,
'once',
null,
addDoorLockOfflineOneTimeDto.passwordName,
);
if (!createOnceOfflinePass.success) {
throw new HttpException(
createOnceOfflinePass.msg,
HttpStatus.BAD_REQUEST,
);
}
// Send email if the password creation is successful
if (createOnceOfflinePass.result.offline_temp_password_id) {
const emailSubject = 'One-Time Password Creation Success';
const emailBody = `Your one-time offline temporary password "${createOnceOfflinePass.result.offline_temp_password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`;
await this.emailService.sendEmail(
addDoorLockOfflineOneTimeDto.email,
emailSubject,
emailBody,
);
}
return {
success: true,
result: createOnceOfflinePass.result,
deviceUuid,
};
} catch (error) {
return {
success: false,
deviceUuid,
error: error.message || 'Error processing device',
};
}
}),
);
// Process results: separate successful and failed operations
const successfulResults = deviceResults
.filter(
(result) =>
result.status === 'fulfilled' &&
(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) => (result as PromiseFulfilledResult<any>).value);
const failedResults = deviceResults
.filter(
(result) =>
result.status === 'rejected' ||
!(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) =>
result.status === 'rejected'
? {
success: false,
error:
(result as PromiseRejectedResult).reason.message ||
'Error processing device',
}
: (result as PromiseFulfilledResult<any>).value,
);
// Return results if there are successful operations
if (successfulResults.length > 0) {
return {
successOperations: successfulResults,
failedOperations: failedResults,
};
}
// If all failed, throw an error with all failure messages
throw new HttpException(
failedResults.map((res) => res.error).join('; ') ||
'All device operations failed',
HttpStatus.BAD_REQUEST,
);
} catch (error) {
console.error(error);
throw new HttpException(
error.message ||
'Error adding offline one-time temporary password from Tuya',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addOfflineTemporaryPasswordTuya(
doorLockUuid: string,
type: string,
addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto,
passwordName: string,
): Promise<createTickInterface> {
try {
const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
...(type === 'multiple' && {
effective_time: addDoorLockOfflineMultipleDto.effectiveTime,
invalid_time: addDoorLockOfflineMultipleDto.invalidTime,
}),
name: passwordName,
type,
},
});
return response as createTickInterface;
} catch (error) {
throw new HttpException(
'Error adding offline temporary password from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addOnlineTemporaryPasswordMultipleTime(
addDoorLockOnlineMultipleDto: AddDoorLockOnlineMultipleDto,
) {
try {
const deviceResults = await Promise.allSettled(
addDoorLockOnlineMultipleDto.devicesUuid.map(async (deviceUuid) => {
try {
const passwordData = await this.getTicketAndEncryptedPassword(
deviceUuid,
addDoorLockOnlineMultipleDto.password,
);
if (
!passwordData.ticketKey ||
!passwordData.encryptedPassword ||
!passwordData.deviceTuyaUuid
) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
const addDeviceObj: addDeviceObjectInterface = {
...addDoorLockOnlineMultipleDto,
...passwordData,
};
const createPass =
await this.addOnlineTemporaryPasswordMultipleTuya(
addDeviceObj,
passwordData.deviceTuyaUuid,
);
if (!createPass.success) {
throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST);
}
// Send email if the password creation is successful
if (createPass.result.id) {
const emailSubject = 'Password Creation Success';
const emailBody = `Your temporary password "${addDoorLockOnlineMultipleDto.password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`;
await this.emailService.sendEmail(
addDoorLockOnlineMultipleDto.email,
emailSubject,
emailBody,
);
}
return {
success: true,
id: createPass.result.id,
deviceUuid,
};
} catch (error) {
return {
success: false,
deviceUuid,
error: error.message || 'Error processing device',
};
}
}),
);
// Process results: separate successful and failed operations
const successfulResults = deviceResults
.filter(
(result) =>
result.status === 'fulfilled' &&
(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) => (result as PromiseFulfilledResult<any>).value);
const failedResults = deviceResults
.filter(
(result) =>
result.status === 'rejected' ||
!(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) =>
result.status === 'rejected'
? {
success: false,
error:
(result as PromiseRejectedResult).reason.message ||
'Error processing device',
}
: (result as PromiseFulfilledResult<any>).value,
);
// Return results if there are successful operations
if (successfulResults?.length > 0) {
return {
successOperations: successfulResults,
failedOperations: failedResults,
};
}
// Throw an error if all operations failed
throw new HttpException(
failedResults.map((res) => res.error).join('; ') ||
'All device operations failed',
HttpStatus.BAD_REQUEST,
);
} catch (error) {
console.error(error);
throw new HttpException(
error.message || 'Error adding online temporary password from Tuya',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getPasswords() {
const deviceIds = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
},
});
const data = [];
deviceIds.forEach((deviceId) => {
data.push(
this.doorLockService
.getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswords(deviceId.uuid, true)
.catch(() => {}),
this.doorLockService
.getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true)
.catch(() => {}),
this.doorLockService
.getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true)
.catch(() => {}),
);
});
return (await Promise.all(data)).flat().filter((datum) => {
return datum != null;
});
}
async getAllPassDevices() {
const devices = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
},
relations: ['productDevice'],
});
const devicesData = await Promise.all(
devices?.map(async (device) => {
return {
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
...(await this.deviceService.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
)),
uuid: device.uuid,
} as GetDeviceDetailsInterface;
}),
);
return devicesData;
}
async addOnlineTemporaryPasswordOneTime(
addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto,
) {
try {
const deviceResults = await Promise.allSettled(
addDoorLockOnlineOneTimeDto.devicesUuid.map(async (deviceUuid) => {
try {
const passwordData = await this.getTicketAndEncryptedPassword(
deviceUuid,
addDoorLockOnlineOneTimeDto.password,
);
if (
!passwordData.ticketKey ||
!passwordData.encryptedPassword ||
!passwordData.deviceTuyaUuid
) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
const addDeviceObj: addDeviceObjectInterface = {
...addDoorLockOnlineOneTimeDto,
...passwordData,
};
const createPass = await this.addOnlineTemporaryPasswordOneTimeTuya(
addDeviceObj,
passwordData.deviceTuyaUuid,
);
if (!createPass.success) {
throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST);
}
// Send email if the password creation is successful
if (createPass.result.id) {
const emailSubject = 'Password Creation Success';
const emailBody = `Your temporary password "${addDoorLockOnlineOneTimeDto.password}" has been successfully created for the device with UUID: ${deviceUuid}.\n\nThank you for using our service.\n\nBest Regards,\nSyncrow`;
await this.emailService.sendEmail(
addDoorLockOnlineOneTimeDto.email,
emailSubject,
emailBody,
);
}
return {
success: true,
id: createPass.result.id,
deviceUuid,
};
} catch (error) {
return {
success: false,
deviceUuid,
error: error.message || 'Error processing device',
};
}
}),
);
// Process results: separate successful and failed operations
const successfulResults = deviceResults
.filter(
(result) =>
result.status === 'fulfilled' &&
(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) => (result as PromiseFulfilledResult<any>).value);
const failedResults = deviceResults
.filter(
(result) =>
result.status === 'rejected' ||
!(result as PromiseFulfilledResult<any>).value.success,
)
.map((result) =>
result.status === 'rejected'
? {
success: false,
error:
(result as PromiseRejectedResult).reason.message ||
'Error processing device',
}
: (result as PromiseFulfilledResult<any>).value,
);
// Return results if there are successful operations
if (successfulResults.length > 0) {
return {
successOperations: successfulResults,
failedOperations: failedResults,
};
}
// Throw an error if all operations failed
throw new HttpException(
failedResults.map((res) => res.error).join('; ') ||
'All device operations failed',
HttpStatus.BAD_REQUEST,
);
} catch (error) {
console.error(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<createTickInterface> {
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 addOnlineTemporaryPasswordMultipleTuya(
addDeviceObj: addDeviceObjectInterface,
doorLockUuid: string,
): Promise<createTickInterface> {
try {
const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`;
let scheduleList;
if (addDeviceObj?.scheduleList?.length > 0) {
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.passwordName,
password: addDeviceObj.encryptedPassword,
effective_time: addDeviceObj.effectiveTime,
invalid_time: addDeviceObj.invalidTime,
password_type: 'ticket',
ticket_id: addDeviceObj.ticketId,
...(addDeviceObj?.scheduleList?.length > 0 && {
schedule_list: scheduleList,
}),
type: '0',
},
});
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);
}
}
async addOnlineTemporaryPasswordOneTimeTuya(
addDeviceObj: addDeviceObjectInterface,
doorLockUuid: string,
): Promise<createTickInterface> {
try {
const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
name: addDeviceObj.passwordName,
password: addDeviceObj.encryptedPassword,
effective_time: addDeviceObj.effectiveTime,
invalid_time: addDeviceObj.invalidTime,
password_type: 'ticket',
ticket_id: addDeviceObj.ticketId,
type: '1',
},
});
return response as createTickInterface;
} catch (error) {
throw new HttpException(
error.msg || 'Error adding online temporary password from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { VisitorPasswordService } from './services/visitor-password.service';
import { VisitorPasswordController } from './controllers/visitor-password.controller';
import { ConfigModule } from '@nestjs/config';
import { DeviceRepositoryModule } from '@app/common/modules/device';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { EmailService } from '@app/common/util/email.service';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
import { DoorLockModule } from 'src/door-lock/door.lock.module';
import { DeviceService } from 'src/device/services';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SpaceRepository } from '@app/common/modules/space/repositories';
@Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController],
providers: [
VisitorPasswordService,
EmailService,
PasswordEncryptionService,
DeviceService,
ProductRepository,
DeviceStatusFirebaseService,
SpaceRepository,
DeviceRepository,
],
exports: [VisitorPasswordService],
})
export class VisitorPasswordModule {}