merged dev

This commit is contained in:
yousef-alkhrissat
2024-07-31 01:55:21 +03:00
67 changed files with 2391 additions and 233 deletions

View File

@ -1,59 +0,0 @@
on:
push:
branches: [ "dev" ]
workflow_dispatch:
env:
AZURE_WEBAPP_NAME: backend-dev # set this to your application's name
AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
NODE_VERSION: '20.x' # set this to the node version to use
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: npm install, build, and test
run: |
npm install
npm run build --if-present
npm run test --if-present
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v3
with:
name: node-app
path: .
deploy:
permissions:
contents: none
runs-on: ubuntu-latest
needs: build
environment:
name: 'Development'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v3
with:
name: node-app
- name: 'Deploy to Azure WebApp'
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

View File

@ -52,4 +52,4 @@ jobs:
--name ${{ env.AZURE_WEB_APP_NAME }} \
--resource-group backend \
--docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
--docker-registry-server-url https://${{ env.ACR_REGISTRY }}
--docker-registry-server-url https://${{ env.ACR_REGISTRY }}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,197 @@
export const allCountries = [
'Afghanistan',
'Albania',
'Algeria',
'Andorra',
'Angola',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bhutan',
'Bolivia',
'Bosnia and Herzegovina',
'Botswana',
'Brazil',
'Brunei',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cabo Verde',
'Cambodia',
'Cameroon',
'Canada',
'Central African Republic',
'Chad',
'Chile',
'China',
'Colombia',
'Comoros',
'Congo (Congo-Brazzaville)',
'Costa Rica',
'Croatia',
'Cuba',
'Cyprus',
'Czech Republic (Czechia)',
'Democratic Republic of the Congo',
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Eswatini (fmr. "Swaziland")',
'Ethiopia',
'Fiji',
'Finland',
'France',
'Gabon',
'Gambia',
'Georgia',
'Germany',
'Ghana',
'Greece',
'Grenada',
'Guatemala',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Honduras',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran',
'Iraq',
'Ireland',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
'Kuwait',
'Kyrgyzstan',
'Laos',
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands',
'Mauritania',
'Mauritius',
'Mexico',
'Micronesia',
'Moldova',
'Monaco',
'Mongolia',
'Montenegro',
'Morocco',
'Mozambique',
'Myanmar (formerly Burma)',
'Namibia',
'Nauru',
'Nepal',
'Netherlands',
'New Zealand',
'Nicaragua',
'Niger',
'Nigeria',
'North Korea',
'North Macedonia (formerly Macedonia)',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestine State',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines',
'Poland',
'Portugal',
'Qatar',
'Romania',
'Russia',
'Rwanda',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Korea',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan',
'Suriname',
'Sweden',
'Switzerland',
'Syria',
'Taiwan',
'Tajikistan',
'Tanzania',
'Thailand',
'Timor-Leste',
'Togo',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates',
'United Kingdom',
'United States of America',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Vatican City',
'Venezuela',
'Vietnam',
'Yemen',
'Zambia',
'Zimbabwe',
];

View File

@ -0,0 +1,126 @@
export const allTimeZones = [
{ name: 'International Date Line West', offset: 'GMT-12:00' },
{ name: 'Hawaii', offset: 'GMT-10:00' },
{ name: 'Alaska', offset: 'GMT-09:00' },
{ name: 'Pacific Time (US & Canada)', offset: 'GMT-08:00' },
{ name: 'Arizona', offset: 'GMT-07:00' },
{ name: 'Mountain Time (US & Canada)', offset: 'GMT-07:00' },
{ name: 'Central Time (US & Canada)', offset: 'GMT-06:00' },
{ name: 'Mexico City', offset: 'GMT-06:00' },
{ name: 'Saskatchewan', offset: 'GMT-06:00' },
{ name: 'Eastern Time (US & Canada)', offset: 'GMT-05:00' },
{ name: 'Bogota', offset: 'GMT-05:00' },
{ name: 'Indiana (East)', offset: 'GMT-05:00' },
{ name: 'Atlantic Time (Canada)', offset: 'GMT-04:00' },
{ name: 'Caracas', offset: 'GMT-04:00' },
{ name: 'Santiago', offset: 'GMT-04:00' },
{ name: 'Newfoundland', offset: 'GMT-03:30' },
{ name: 'Brasilia', offset: 'GMT-03:00' },
{ name: 'Buenos Aires', offset: 'GMT-03:00' },
{ name: 'Greenland', offset: 'GMT-03:00' },
{ name: 'Mid-Atlantic', offset: 'GMT-02:00' },
{ name: 'Azores', offset: 'GMT-01:00' },
{ name: 'Cape Verde Is.', offset: 'GMT-01:00' },
{ name: 'Casablanca', offset: 'GMT+00:00' },
{ name: 'Dublin', offset: 'GMT+00:00' },
{ name: 'Lisbon', offset: 'GMT+00:00' },
{ name: 'London', offset: 'GMT+00:00' },
{ name: 'Monrovia', offset: 'GMT+00:00' },
{ name: 'Amsterdam', offset: 'GMT+01:00' },
{ name: 'Belgrade', offset: 'GMT+01:00' },
{ name: 'Berlin', offset: 'GMT+01:00' },
{ name: 'Bratislava', offset: 'GMT+01:00' },
{ name: 'Brussels', offset: 'GMT+01:00' },
{ name: 'Budapest', offset: 'GMT+01:00' },
{ name: 'Copenhagen', offset: 'GMT+01:00' },
{ name: 'Madrid', offset: 'GMT+01:00' },
{ name: 'Paris', offset: 'GMT+01:00' },
{ name: 'Prague', offset: 'GMT+01:00' },
{ name: 'Rome', offset: 'GMT+01:00' },
{ name: 'Sarajevo', offset: 'GMT+01:00' },
{ name: 'Warsaw', offset: 'GMT+01:00' },
{ name: 'West Central Africa', offset: 'GMT+01:00' },
{ name: 'Zagreb', offset: 'GMT+01:00' },
{ name: 'Athens', offset: 'GMT+02:00' },
{ name: 'Bucharest', offset: 'GMT+02:00' },
{ name: 'Cairo', offset: 'GMT+02:00' },
{ name: 'Harare', offset: 'GMT+02:00' },
{ name: 'Helsinki', offset: 'GMT+02:00' },
{ name: 'Jerusalem', offset: 'GMT+02:00' },
{ name: 'Kaliningrad', offset: 'GMT+02:00' },
{ name: 'Kyiv', offset: 'GMT+02:00' },
{ name: 'Pretoria', offset: 'GMT+02:00' },
{ name: 'Riga', offset: 'GMT+02:00' },
{ name: 'Sofia', offset: 'GMT+02:00' },
{ name: 'Tallinn', offset: 'GMT+02:00' },
{ name: 'Vilnius', offset: 'GMT+02:00' },
{ name: 'Baghdad', offset: 'GMT+03:00' },
{ name: 'Istanbul', offset: 'GMT+03:00' },
{ name: 'Kuwait', offset: 'GMT+03:00' },
{ name: 'Minsk', offset: 'GMT+03:00' },
{ name: 'Moscow', offset: 'GMT+03:00' },
{ name: 'Nairobi', offset: 'GMT+03:00' },
{ name: 'Riyadh', offset: 'GMT+03:00' },
{ name: 'St. Petersburg', offset: 'GMT+03:00' },
{ name: 'Tehran', offset: 'GMT+03:30' },
{ name: 'Abu Dhabi', offset: 'GMT+04:00' },
{ name: 'Baku', offset: 'GMT+04:00' },
{ name: 'Muscat', offset: 'GMT+04:00' },
{ name: 'Tbilisi', offset: 'GMT+04:00' },
{ name: 'Yerevan', offset: 'GMT+04:00' },
{ name: 'Kabul', offset: 'GMT+04:30' },
{ name: 'Ekaterinburg', offset: 'GMT+05:00' },
{ name: 'Islamabad', offset: 'GMT+05:00' },
{ name: 'Karachi', offset: 'GMT+05:00' },
{ name: 'Tashkent', offset: 'GMT+05:00' },
{ name: 'Chennai', offset: 'GMT+05:30' },
{ name: 'Kolkata', offset: 'GMT+05:30' },
{ name: 'Mumbai', offset: 'GMT+05:30' },
{ name: 'New Delhi', offset: 'GMT+05:30' },
{ name: 'Sri Jayawardenepura', offset: 'GMT+05:30' },
{ name: 'Kathmandu', offset: 'GMT+05:45' },
{ name: 'Almaty', offset: 'GMT+06:00' },
{ name: 'Astana', offset: 'GMT+06:00' },
{ name: 'Dhaka', offset: 'GMT+06:00' },
{ name: 'Urumqi', offset: 'GMT+06:00' },
{ name: 'Rangoon', offset: 'GMT+06:30' },
{ name: 'Bangkok', offset: 'GMT+07:00' },
{ name: 'Hanoi', offset: 'GMT+07:00' },
{ name: 'Jakarta', offset: 'GMT+07:00' },
{ name: 'Krasnoyarsk', offset: 'GMT+07:00' },
{ name: 'Beijing', offset: 'GMT+08:00' },
{ name: 'Chongqing', offset: 'GMT+08:00' },
{ name: 'Hong Kong', offset: 'GMT+08:00' },
{ name: 'Irkutsk', offset: 'GMT+08:00' },
{ name: 'Kuala Lumpur', offset: 'GMT+08:00' },
{ name: 'Perth', offset: 'GMT+08:00' },
{ name: 'Singapore', offset: 'GMT+08:00' },
{ name: 'Taipei', offset: 'GMT+08:00' },
{ name: 'Ulaan Bataar', offset: 'GMT+08:00' },
{ name: 'Osaka', offset: 'GMT+09:00' },
{ name: 'Sapporo', offset: 'GMT+09:00' },
{ name: 'Seoul', offset: 'GMT+09:00' },
{ name: 'Tokyo', offset: 'GMT+09:00' },
{ name: 'Yakutsk', offset: 'GMT+09:00' },
{ name: 'Adelaide', offset: 'GMT+09:30' },
{ name: 'Darwin', offset: 'GMT+09:30' },
{ name: 'Brisbane', offset: 'GMT+10:00' },
{ name: 'Canberra', offset: 'GMT+10:00' },
{ name: 'Guam', offset: 'GMT+10:00' },
{ name: 'Hobart', offset: 'GMT+10:00' },
{ name: 'Melbourne', offset: 'GMT+10:00' },
{ name: 'Port Moresby', offset: 'GMT+10:00' },
{ name: 'Sydney', offset: 'GMT+10:00' },
{ name: 'Vladivostok', offset: 'GMT+10:00' },
{ name: 'Magadan', offset: 'GMT+11:00' },
{ name: 'New Caledonia', offset: 'GMT+11:00' },
{ name: 'Solomon Is.', offset: 'GMT+11:00' },
{ name: 'Kamchatka', offset: 'GMT+12:00' },
{ name: 'Marshall Is.', offset: 'GMT+12:00' },
{ name: 'Fiji', offset: 'GMT+12:00' },
{ name: 'Auckland', offset: 'GMT+12:00' },
{ name: 'Wellington', offset: 'GMT+12:00' },
{ name: "Nuku'alofa", offset: 'GMT+13:00' },
{ name: 'Samoa', offset: 'GMT+13:00' },
{ name: 'Tokelau Is.', offset: 'GMT+13:00' },
];

View File

@ -16,6 +16,8 @@ import { UserRoleEntity } from '../modules/user-role/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user-notification/entities';
import { DeviceNotificationEntity } from '../modules/device-notification/entities';
import { RegionEntity } from '../modules/region/entities';
import { TimeZoneEntity } from '../modules/timezone/entities';
@Module({
imports: [
@ -46,6 +48,8 @@ import { DeviceNotificationEntity } from '../modules/device-notification/entitie
RoleTypeEntity,
UserNotificationEntity,
DeviceNotificationEntity,
RegionEntity,
TimeZoneEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -0,0 +1,3 @@
export function removeBase64Prefix(dataUrl: string): string {
return dataUrl.replace(/^data:image\/[a-z]+;base64,/, '');
}

View File

@ -4,12 +4,12 @@ function toSnakeCase(str) {
export function convertKeysToSnakeCase(obj) {
if (Array.isArray(obj)) {
return obj.map((v) => convertKeysToSnakeCase(v));
} else if (obj !== null && obj.constructor === Object) {
return Object.keys(obj).reduce((result, key) => {
return obj.map(convertKeysToSnakeCase);
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const snakeKey = toSnakeCase(key);
result[snakeKey] = convertKeysToSnakeCase(obj[key]);
return result;
acc[snakeKey] = convertKeysToSnakeCase(obj[key]);
return acc;
}, {});
}
return obj;

View File

@ -1,4 +1,4 @@
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany, Unique, Index } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto } from '../dtos/device.dto';
import { SpaceEntity } from '../../space/entities';
@ -50,6 +50,11 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
nullable: false,
})
productDevice: ProductEntity;
@Index()
@Column({ nullable: false })
uuid: string;
constructor(partial: Partial<DeviceEntity>) {
super();
Object.assign(this, partial);

View File

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

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class RegionDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public regionName: string;
}

View File

@ -0,0 +1 @@
export * from './region.entity';

View File

@ -0,0 +1,20 @@
import { Column, Entity, OneToMany } from 'typeorm';
import { RegionDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'region' })
export class RegionEntity extends AbstractEntity<RegionDto> {
@Column({
nullable: false,
})
regionName: string;
@OneToMany(() => UserEntity, (user) => user.region)
users: UserEntity[];
constructor(partial: Partial<RegionEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RegionEntity } from './entities/region.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([RegionEntity])],
})
export class RegionRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './region.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { RegionEntity } from '../entities';
@Injectable()
export class RegionRepository extends Repository<RegionEntity> {
constructor(private dataSource: DataSource) {
super(RegionEntity, dataSource.createEntityManager());
}
}

View File

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

View File

@ -0,0 +1,15 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class TimeZoneDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public cityName: string;
@IsString()
@IsNotEmpty()
public timeZoneOffset: string;
}

View File

@ -0,0 +1 @@
export * from './timezone.entity';

View File

@ -0,0 +1,23 @@
import { Column, Entity, OneToMany } from 'typeorm';
import { TimeZoneDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'timezone' })
export class TimeZoneEntity extends AbstractEntity<TimeZoneDto> {
@Column({
nullable: false,
})
cityName: string;
@Column({
nullable: false,
})
timeZoneOffset: string;
@OneToMany(() => UserEntity, (user) => user.timezone)
users: UserEntity[];
constructor(partial: Partial<TimeZoneEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './timezone.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { TimeZoneEntity } from '../entities';
@Injectable()
export class TimeZoneRepository extends Repository<TimeZoneEntity> {
constructor(private dataSource: DataSource) {
super(TimeZoneEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TimeZoneEntity } from './entities/timezone.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([TimeZoneEntity])],
})
export class TimeZoneRepositoryModule {}

View File

@ -1,5 +1,5 @@
import { DeviceUserPermissionEntity } from '../../device-user-permission/entities/device.user.permission.entity';
import { Column, Entity, OneToMany } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { UserDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserSpaceEntity } from '../../user-space/entities';
@ -7,6 +7,9 @@ import { UserRoleEntity } from '../../user-role/entities';
import { DeviceNotificationEntity } from '../../device-notification/entities';
import { UserNotificationEntity } from '../../user-notification/entities';
import { DeviceEntity } from '../../device/entities';
import { defaultProfilePicture } from '@app/common/constants/default.profile.picture';
import { RegionEntity } from '../../region/entities';
import { TimeZoneEntity } from '../../timezone/entities';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {
@ -17,6 +20,13 @@ export class UserEntity extends AbstractEntity<UserDto> {
})
public uuid: string;
@Column({
nullable: true,
type: 'text',
default: defaultProfilePicture,
})
public profilePicture: string;
@Column({
nullable: false,
unique: true,
@ -78,6 +88,12 @@ export class UserEntity extends AbstractEntity<UserDto> {
nullable: true,
})
roles: UserRoleEntity[];
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
region: RegionEntity;
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
nullable: true,
})
timezone: TimeZoneEntity;
constructor(partial: Partial<UserEntity>) {
super();
Object.assign(this, partial);

View File

@ -15,6 +15,10 @@ import { UserRepository } from '../modules/user/repositories';
import { UserRoleRepository } from '../modules/user-role/repositories';
import { UserRoleRepositoryModule } from '../modules/user-role/user.role.repository.module';
import { UserRepositoryModule } from '../modules/user/user.repository.module';
import { RegionSeeder } from './services/regions.seeder';
import { RegionRepository } from '../modules/region/repositories';
import { TimeZoneSeeder } from './services/timezone.seeder';
import { TimeZoneRepository } from '../modules/timezone/repositories';
@Global()
@Module({
providers: [
@ -28,6 +32,10 @@ import { UserRepositoryModule } from '../modules/user/user.repository.module';
SuperAdminSeeder,
UserRepository,
UserRoleRepository,
RegionSeeder,
RegionRepository,
TimeZoneSeeder,
TimeZoneRepository,
],
exports: [SeederService],
controllers: [],

View File

@ -0,0 +1,40 @@
import { RegionRepository } from '@app/common/modules/region/repositories';
import { allCountries } from '@app/common/constants/regions';
import { Injectable } from '@nestjs/common';
@Injectable()
export class RegionSeeder {
constructor(private readonly regionRepository: RegionRepository) {}
async addRegionDataIfNotFound(): Promise<void> {
try {
const existingRegions = await this.regionRepository.find();
const regionNames = existingRegions.map((region) => region.regionName);
const missingRegions = allCountries.filter(
(country) => !regionNames.includes(country),
);
if (missingRegions.length > 0) {
await this.addRegionData(missingRegions);
}
} catch (err) {
console.error('Error while checking region data:', err);
throw err;
}
}
private async addRegionData(regions: string[]): Promise<void> {
try {
const regionEntities = regions.map((regionName) => ({
regionName,
}));
await this.regionRepository.save(regionEntities);
} catch (err) {
console.error('Error while adding region data:', err);
throw err;
}
}
}

View File

@ -3,12 +3,16 @@ import { PermissionTypeSeeder } from './permission.type.seeder';
import { RoleTypeSeeder } from './role.type.seeder';
import { SpaceTypeSeeder } from './space.type.seeder';
import { SuperAdminSeeder } from './supper.admin.seeder';
import { RegionSeeder } from './regions.seeder';
import { TimeZoneSeeder } from './timezone.seeder';
@Injectable()
export class SeederService {
constructor(
private readonly permissionTypeSeeder: PermissionTypeSeeder,
private readonly roleTypeSeeder: RoleTypeSeeder,
private readonly spaceTypeSeeder: SpaceTypeSeeder,
private readonly regionSeeder: RegionSeeder,
private readonly timeZoneSeeder: TimeZoneSeeder,
private readonly superAdminSeeder: SuperAdminSeeder,
) {}
@ -16,6 +20,8 @@ export class SeederService {
await this.permissionTypeSeeder.addPermissionTypeDataIfNotFound();
await this.roleTypeSeeder.addRoleTypeDataIfNotFound();
await this.spaceTypeSeeder.addSpaceTypeDataIfNotFound();
await this.regionSeeder.addRegionDataIfNotFound();
await this.timeZoneSeeder.addTimeZoneDataIfNotFound();
await this.superAdminSeeder.createSuperAdminIfNotFound();
}
}

View File

@ -0,0 +1,42 @@
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { allTimeZones } from '@app/common/constants/timezones';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TimeZoneSeeder {
constructor(private readonly timeZoneRepository: TimeZoneRepository) {}
async addTimeZoneDataIfNotFound(): Promise<void> {
try {
const existingTimeZones = await this.timeZoneRepository.find();
const timeZoneNames = existingTimeZones.map((tz) => tz.cityName);
const missingTimeZones = allTimeZones.filter(
(tz) => !timeZoneNames.includes(tz.name),
);
if (missingTimeZones.length > 0) {
await this.addTimeZoneData(missingTimeZones);
}
} catch (err) {
console.error('Error while checking time zone data:', err);
throw err;
}
}
private async addTimeZoneData(
timeZones: { name: string; offset: string }[],
): Promise<void> {
try {
const timeZoneEntities = timeZones.map((tz) => ({
cityName: tz.name,
timeZoneOffset: tz.offset,
}));
await this.timeZoneRepository.save(timeZoneEntities);
} catch (err) {
console.error('Error while adding time zone data:', err);
throw err;
}
}
}

View File

@ -20,6 +20,9 @@ import { SceneModule } from './scene/scene.module';
import { DoorLockModule } from './door-lock/door.lock.module';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { AutomationModule } from './automation/automation.module';
import { RegionModule } from './region/region.module';
import { TimeZoneModule } from './timezone/timezone.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -41,8 +44,10 @@ import { LoggingInterceptor } from './interceptors/logging.interceptor';
UserNotificationModule,
SeederModule,
SceneModule,
AutomationModule,
DoorLockModule,
//
RegionModule,
TimeZoneModule,
],
controllers: [AuthenticationController],
providers: [

View File

@ -9,7 +9,7 @@ import { ApiTags } from '@nestjs/swagger';
@ApiTags('Tuya Auth')
export class AuthenticationController {
constructor(private readonly authenticationService: AuthenticationService) {}
@Post('auth')
@Post('auth2')
async Authentication() {
return await this.authenticationService.main();
}

View File

@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { IsPasswordStrong } from 'src/validators/password.validator';
export class UserSignUpDto {
@ApiProperty({
@ -16,6 +17,10 @@ export class UserSignUpDto {
})
@IsString()
@IsNotEmpty()
@IsPasswordStrong({
message:
'password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character.',
})
public password: string;
@ApiProperty({

View File

@ -1,14 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { IsPasswordStrong } from 'src/validators/password.validator';
export class ForgetPasswordDto {
@ApiProperty()
@ApiProperty({
description: 'email',
required: true,
})
@IsEmail()
@IsNotEmpty()
email: string;
public email: string;
@ApiProperty()
@ApiProperty({
description: 'password',
required: true,
})
@IsString()
@IsNotEmpty()
password: string;
@IsPasswordStrong({
message:
'password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character.',
})
public password: string;
}

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { AutomationService } from './services/automation.service';
import { AutomationController } from './controllers/automation.controller';
import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { DeviceService } from 'src/device/services';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductRepository } from '@app/common/modules/product/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
controllers: [AutomationController],
providers: [
AutomationService,
SpaceRepository,
DeviceService,
DeviceRepository,
ProductRepository,
],
exports: [AutomationService],
})
export class AutomationModule {}

View File

@ -0,0 +1,150 @@
import { AutomationService } from '../services/automation.service';
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import {
AddAutomationDto,
UpdateAutomationDto,
UpdateAutomationStatusDto,
} from '../dtos/automation.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
@ApiTags('Automation Module')
@Controller({
version: '1',
path: 'automation',
})
export class AutomationController {
constructor(private readonly automationService: AutomationService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
async addAutomation(@Body() addAutomationDto: AddAutomationDto) {
try {
const automation =
await this.automationService.addAutomation(addAutomationDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Automation added successfully',
data: automation,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':unitUuid')
async getAutomationByUnit(@Param('unitUuid') unitUuid: string) {
try {
const automation =
await this.automationService.getAutomationByUnit(unitUuid);
return automation;
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('details/:automationId')
async getAutomationDetails(@Param('automationId') automationId: string) {
try {
const automation =
await this.automationService.getAutomationDetails(automationId);
return automation;
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
``;
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete(':unitUuid/:automationId')
async deleteAutomation(
@Param('unitUuid') unitUuid: string,
@Param('automationId') automationId: string,
) {
try {
await this.automationService.deleteAutomation(unitUuid, automationId);
return {
statusCode: HttpStatus.OK,
message: 'Automation Deleted Successfully',
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':automationId')
async updateAutomation(
@Body() updateAutomationDto: UpdateAutomationDto,
@Param('automationId') automationId: string,
) {
try {
const automation = await this.automationService.updateAutomation(
updateAutomationDto,
automationId,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Automation updated successfully',
data: automation,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('status/:automationId')
async updateAutomationStatus(
@Body() updateAutomationStatusDto: UpdateAutomationStatusDto,
@Param('automationId') automationId: string,
) {
try {
await this.automationService.updateAutomationStatus(
updateAutomationStatusDto,
automationId,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Automation status updated successfully',
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

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

View File

@ -0,0 +1,213 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsString,
IsArray,
ValidateNested,
IsOptional,
IsNumber,
IsBoolean,
} from 'class-validator';
import { Type } from 'class-transformer';
class EffectiveTime {
@ApiProperty({ description: 'Start time', required: true })
@IsString()
@IsNotEmpty()
public start: string;
@ApiProperty({ description: 'End time', required: true })
@IsString()
@IsNotEmpty()
public end: string;
@ApiProperty({ description: 'Loops', required: true })
@IsString()
@IsNotEmpty()
public loops: string;
}
class Expr {
@ApiProperty({ description: 'Status code', required: true })
@IsString()
@IsNotEmpty()
public statusCode: string;
@ApiProperty({ description: 'Comparator', required: true })
@IsString()
@IsNotEmpty()
public comparator: string;
@ApiProperty({ description: 'Status value', required: true })
@IsNotEmpty()
public statusValue: any;
}
class Condition {
@ApiProperty({ description: 'Condition code', required: true })
@IsNumber()
@IsNotEmpty()
public code: number;
@ApiProperty({ description: 'Entity ID', required: true })
@IsString()
@IsNotEmpty()
public entityId: string;
@ApiProperty({ description: 'Entity type', required: true })
@IsString()
@IsNotEmpty()
public entityType: string;
@ApiProperty({ description: 'Expression', required: true, type: Expr })
@ValidateNested()
@Type(() => Expr)
@IsNotEmpty()
public expr: Expr;
}
class ExecutorProperty {
@ApiProperty({
description: 'Function code (for device issue action)',
required: false,
})
@IsString()
@IsOptional()
public functionCode?: string;
@ApiProperty({
description: 'Function value (for device issue action)',
required: false,
})
@IsOptional()
public functionValue?: any;
@ApiProperty({
description: 'Delay in seconds (for delay action)',
required: false,
})
@IsNumber()
@IsOptional()
public delaySeconds?: number;
}
class Action {
@ApiProperty({ description: 'Entity ID', required: true })
@IsString()
@IsNotEmpty()
public entityId: string;
@ApiProperty({ description: 'Action executor', required: true })
@IsString()
@IsNotEmpty()
public actionExecutor: string;
@ApiProperty({
description: 'Executor property',
required: false,
type: ExecutorProperty,
})
@ValidateNested()
@Type(() => ExecutorProperty)
@IsOptional()
public executorProperty?: ExecutorProperty;
}
export class AddAutomationDto {
@ApiProperty({ description: 'Unit ID', required: true })
@IsString()
@IsNotEmpty()
public unitUuid: string;
@ApiProperty({ description: 'Automation name', required: true })
@IsString()
@IsNotEmpty()
public automationName: string;
@ApiProperty({ description: 'Decision expression', required: true })
@IsString()
@IsNotEmpty()
public decisionExpr: string;
@ApiProperty({
description: 'Effective time',
required: true,
type: EffectiveTime,
})
@ValidateNested()
@Type(() => EffectiveTime)
@IsNotEmpty()
public effectiveTime: EffectiveTime;
@ApiProperty({ description: 'Conditions', required: true, type: [Condition] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => Condition)
@IsNotEmpty()
public conditions: Condition[];
@ApiProperty({ description: 'Actions', required: true, type: [Action] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => Action)
@IsNotEmpty()
public actions: Action[];
constructor(dto: Partial<AddAutomationDto>) {
Object.assign(this, dto);
}
}
export class UpdateAutomationDto {
@ApiProperty({ description: 'Automation name', required: true })
@IsString()
@IsNotEmpty()
public automationName: string;
@ApiProperty({ description: 'Decision expression', required: true })
@IsString()
@IsNotEmpty()
public decisionExpr: string;
@ApiProperty({
description: 'Effective time',
required: true,
type: EffectiveTime,
})
@ValidateNested()
@Type(() => EffectiveTime)
@IsNotEmpty()
public effectiveTime: EffectiveTime;
@ApiProperty({ description: 'Conditions', required: true, type: [Condition] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => Condition)
@IsNotEmpty()
public conditions: Condition[];
@ApiProperty({ description: 'Actions', required: true, type: [Action] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => Action)
@IsNotEmpty()
public actions: Action[];
constructor(dto: Partial<UpdateAutomationDto>) {
Object.assign(this, dto);
}
}
export class UpdateAutomationStatusDto {
@ApiProperty({ description: 'Unit uuid', required: true })
@IsString()
@IsNotEmpty()
public unitUuid: string;
@ApiProperty({ description: 'Is enable', required: true })
@IsBoolean()
@IsNotEmpty()
public isEnable: boolean;
constructor(dto: Partial<UpdateAutomationStatusDto>) {
Object.assign(this, dto);
}
}

View File

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

View File

@ -0,0 +1,50 @@
export interface AddAutomationInterface {
success: boolean;
msg?: string;
result: {
id: string;
};
}
export interface GetAutomationByUnitInterface {
success: boolean;
msg?: string;
result: {
list: Array<{
id: string;
name: string;
status: string;
}>;
};
}
export interface DeleteAutomationInterface {
success: boolean;
msg?: string;
result: boolean;
}
export interface Action {
actionExecutor: string;
entityId: string;
[key: string]: any; // Allow additional properties
}
export interface Condition {
entityType: string;
entityId: string;
[key: string]: any; // Allow additional properties
}
export interface AutomationResponseData {
id: string;
name: string;
status: string;
spaceId?: string;
runningMode?: string;
actions: Action[];
conditions: Condition[];
[key: string]: any; // Allow additional properties
}
export interface AutomationDetailsResult {
id: string;
name: string;
type: string;
}

View File

@ -0,0 +1,423 @@
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
AddAutomationDto,
UpdateAutomationDto,
UpdateAutomationStatusDto,
} from '../dtos';
import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services';
import {
AddAutomationInterface,
AutomationDetailsResult,
AutomationResponseData,
DeleteAutomationInterface,
GetAutomationByUnitInterface,
} from '../interface/automation.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
@Injectable()
export class AutomationService {
private tuya: TuyaContext;
constructor(
private readonly configService: ConfigService,
private readonly spaceRepository: SpaceRepository,
private readonly deviceService: DeviceService,
) {
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 addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) {
try {
let unitSpaceTuyaId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(addAutomationDto.unitUuid);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitDetails) {
throw new BadRequestException('Invalid unit UUID');
}
} else {
unitSpaceTuyaId = spaceTuyaId;
}
const actions = addAutomationDto.actions.map((action) =>
convertKeysToSnakeCase(action),
);
const conditions = addAutomationDto.conditions.map((condition) =>
convertKeysToSnakeCase(condition),
);
for (const action of actions) {
if (action.action_executor === 'device_issue') {
const device = await this.deviceService.getDeviceByDeviceUuid(
action.entity_id,
false,
);
if (device) {
action.entity_id = device.deviceTuyaUuid;
}
}
}
for (const condition of conditions) {
if (condition.entity_type === 'device_report') {
const device = await this.deviceService.getDeviceByDeviceUuid(
condition.entity_id,
false,
);
if (device) {
condition.entity_id = device.deviceTuyaUuid;
}
}
}
const path = `/v2.0/cloud/scene/rule`;
const response: AddAutomationInterface = await this.tuya.request({
method: 'POST',
path,
body: {
space_id: unitSpaceTuyaId,
name: addAutomationDto.automationName,
effective_time: {
...addAutomationDto.effectiveTime,
timezone_id: 'Asia/Dubai',
},
type: 'automation',
decision_expr: addAutomationDto.decisionExpr,
conditions: conditions,
actions: actions,
},
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
return {
id: response.result.id,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async getUnitByUuid(unitUuid: string): Promise<GetUnitByUuidInterface> {
try {
const unit = await this.spaceRepository.findOne({
where: {
uuid: unitUuid,
spaceType: {
type: 'unit',
},
},
relations: ['spaceType'],
});
if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') {
throw new BadRequestException('Invalid unit UUID');
}
return {
uuid: unit.uuid,
createdAt: unit.createdAt,
updatedAt: unit.updatedAt,
name: unit.spaceName,
type: unit.spaceType.type,
spaceTuyaUuid: unit.spaceTuyaUuid,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Unit not found', HttpStatus.NOT_FOUND);
}
}
}
async getAutomationByUnit(unitUuid: string) {
try {
const unit = await this.getUnitByUuid(unitUuid);
if (!unit.spaceTuyaUuid) {
throw new BadRequestException('Invalid unit UUID');
}
const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=automation`;
const response: GetAutomationByUnitInterface = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
return response.result.list.map((item) => {
return {
id: item.id,
name: item.name,
status: item.status,
type: 'automation',
};
});
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async getTapToRunSceneDetailsTuya(
sceneId: string,
): Promise<AutomationDetailsResult> {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const camelCaseResponse = convertKeysToCamelCase(response);
const { id, name, type } = camelCaseResponse.result;
return {
id,
name,
type,
} as AutomationDetailsResult;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
'Scene not found for Tuya',
HttpStatus.NOT_FOUND,
);
}
}
}
async getAutomationDetails(automationId: string, withSpaceId = false) {
try {
const path = `/v2.0/cloud/scene/rule/${automationId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const responseData: AutomationResponseData = convertKeysToCamelCase(
response.result,
);
const actions = responseData.actions.map((action) => ({
...action,
}));
for (const action of actions) {
if (action.actionExecutor === 'device_issue') {
const device = await this.deviceService.getDeviceByDeviceTuyaUuid(
action.entityId,
);
if (device) {
action.entityId = device.uuid;
}
} else if (
action.actionExecutor !== 'device_issue' &&
action.actionExecutor !== 'delay'
) {
const sceneDetails = await this.getTapToRunSceneDetailsTuya(
action.entityId,
);
if (sceneDetails.id) {
action.name = sceneDetails.name;
action.type = sceneDetails.type;
}
}
}
const conditions = responseData.conditions.map((condition) => ({
...condition,
}));
for (const condition of conditions) {
if (condition.entityType === 'device_report') {
const device = await this.deviceService.getDeviceByDeviceTuyaUuid(
condition.entityId,
);
if (device) {
condition.entityId = device.uuid;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { timeZoneId, ...effectiveTimeWithoutTimeZoneId } =
responseData.effectiveTime || {};
return {
id: responseData.id,
name: responseData.name,
status: responseData.status,
type: 'automation',
...(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { spaceId, runningMode, ...rest } = responseData;
return rest;
})(),
actions,
conditions,
effectiveTime: effectiveTimeWithoutTimeZoneId, // Use modified effectiveTime
...(withSpaceId && { spaceId: responseData.spaceId }),
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Automation not found', HttpStatus.NOT_FOUND);
}
}
}
async deleteAutomation(
unitUuid: string,
automationId: string,
spaceTuyaId = null,
) {
try {
let unitSpaceTuyaId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(unitUuid);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitSpaceTuyaId) {
throw new BadRequestException('Invalid unit UUID');
}
} else {
unitSpaceTuyaId = spaceTuyaId;
}
const path = `/v2.0/cloud/scene/rule?ids=${automationId}&space_id=${unitSpaceTuyaId}`;
const response: DeleteAutomationInterface = await this.tuya.request({
method: 'DELETE',
path,
});
if (!response.success) {
throw new HttpException('Automation not found', HttpStatus.NOT_FOUND);
}
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async updateAutomation(
updateAutomationDto: UpdateAutomationDto,
automationId: string,
) {
try {
const spaceTuyaId = await this.getAutomationDetails(automationId, true);
if (!spaceTuyaId.spaceId) {
throw new HttpException(
"Automation doesn't exist",
HttpStatus.NOT_FOUND,
);
}
const addAutomation = {
...updateAutomationDto,
unitUuid: null,
};
const newAutomation = await this.addAutomation(
addAutomation,
spaceTuyaId.spaceId,
);
if (newAutomation.id) {
await this.deleteAutomation(null, automationId, spaceTuyaId.spaceId);
return newAutomation;
}
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async updateAutomationStatus(
updateAutomationStatusDto: UpdateAutomationStatusDto,
automationId: string,
) {
try {
const unitDetails = await this.getUnitByUuid(
updateAutomationStatusDto.unitUuid,
);
if (!unitDetails.spaceTuyaUuid) {
throw new BadRequestException('Invalid unit UUID');
}
const path = `/v2.0/cloud/scene/rule/state?space_id=${unitDetails.spaceTuyaUuid}`;
const response: DeleteAutomationInterface = await this.tuya.request({
method: 'PUT',
path,
body: {
ids: automationId,
is_enable: updateAutomationStatusDto.isEnable,
},
});
if (!response.success) {
throw new HttpException('Automation not found', HttpStatus.NOT_FOUND);
}
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Automation not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
}

View File

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

View File

@ -197,8 +197,15 @@ export class DeviceService {
where: {
uuid: updateDeviceInRoomDto.deviceUuid,
},
relations: ['spaceDevice'],
relations: ['spaceDevice', 'spaceDevice.parent'],
});
if (device.spaceDevice.parent.spaceTuyaUuid) {
await this.transferDeviceInSpacesTuya(
device.deviceTuyaUuid,
device.spaceDevice.parent.spaceTuyaUuid,
);
}
return {
uuid: device.uuid,
roomUuid: device.spaceDevice.uuid,
@ -210,7 +217,26 @@ export class DeviceService {
);
}
}
async transferDeviceInSpacesTuya(
deviceId: string,
spaceId: string,
): Promise<controlDeviceInterface> {
try {
const path = `/v2.0/cloud/thing/${deviceId}/transfer`;
const response = await this.tuya.request({
method: 'POST',
path,
body: { space_id: spaceId },
});
return response as controlDeviceInterface;
} catch (error) {
throw new HttpException(
'Error transferring device in spaces from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false);

View File

@ -9,11 +9,13 @@ import {
Get,
Delete,
UseGuards,
Put,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddDoorLockOnlineDto } from '../dtos/add.online-temp.dto';
import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto';
import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto';
@ApiTags('Door Lock Module')
@Controller({
@ -55,13 +57,11 @@ export class DoorLockController {
@UseGuards(JwtAuthGuard)
@Post('temporary-password/offline/one-time/:doorLockUuid')
async addOfflineOneTimeTemporaryPassword(
@Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
@Param('doorLockUuid') doorLockUuid: string,
) {
try {
const temporaryPassword =
await this.doorLockService.addOfflineOneTimeTemporaryPassword(
addDoorLockOfflineTempDto,
doorLockUuid,
);
@ -82,13 +82,14 @@ export class DoorLockController {
@UseGuards(JwtAuthGuard)
@Post('temporary-password/offline/multiple-time/:doorLockUuid')
async addOfflineMultipleTimeTemporaryPassword(
@Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
@Body()
addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto,
@Param('doorLockUuid') doorLockUuid: string,
) {
try {
const temporaryPassword =
await this.doorLockService.addOfflineMultipleTimeTemporaryPassword(
addDoorLockOfflineTempDto,
addDoorLockOfflineTempMultipleTimeDto,
doorLockUuid,
);
@ -124,6 +125,29 @@ export class DoorLockController {
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('temporary-password/online/:doorLockUuid/:passwordId')
async deleteDoorLockPassword(
@Param('doorLockUuid') doorLockUuid: string,
@Param('passwordId') passwordId: string,
) {
try {
await this.doorLockService.deleteDoorLockPassword(
doorLockUuid,
passwordId,
);
return {
statusCode: HttpStatus.OK,
message: 'Temporary Password deleted Successfully',
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('temporary-password/offline/one-time/:doorLockUuid')
async getOfflineOneTimeTemporaryPasswords(
@Param('doorLockUuid') doorLockUuid: string,
@ -156,21 +180,29 @@ export class DoorLockController {
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('temporary-password/:doorLockUuid/:passwordId')
async deleteDoorLockPassword(
@Put('temporary-password/:doorLockUuid/offline/:passwordId')
async updateOfflineTemporaryPassword(
@Body()
updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto,
@Param('doorLockUuid') doorLockUuid: string,
@Param('passwordId') passwordId: string,
) {
try {
await this.doorLockService.deleteDoorLockPassword(
doorLockUuid,
passwordId,
);
const temporaryPassword =
await this.doorLockService.updateOfflineTemporaryPassword(
updateDoorLockOfflineTempDto,
doorLockUuid,
passwordId,
);
return {
statusCode: HttpStatus.OK,
message: 'Temporary Password deleted Successfully',
statusCode: HttpStatus.CREATED,
success: true,
message: 'offline temporary password updated successfully',
data: temporaryPassword,
};
} catch (error) {
throw new HttpException(

View File

@ -1,23 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Length } from 'class-validator';
export class AddDoorLockOfflineTempDto {
@ApiProperty({
description: 'name',
required: true,
})
@IsString()
@IsNotEmpty()
public name: string;
@ApiProperty({
description: 'password',
required: true,
})
@IsString()
@IsNotEmpty()
@Length(7, 7)
public password: string;
import { IsNotEmpty, IsString } from 'class-validator';
export class AddDoorLockOfflineTempMultipleTimeDto {
@ApiProperty({
description: 'effectiveTime',
required: true,

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateDoorLockOfflineTempDto {
@ApiProperty({
description: 'name',
required: true,
})
@IsString()
@IsNotEmpty()
public name: string;
}

View File

@ -55,3 +55,10 @@ export interface deleteTemporaryPasswordInterface {
result: boolean;
msg?: string;
}
export interface getPasswordOfflineInterface {
success: boolean;
result: {
records: [];
};
msg?: string;
}

View File

@ -7,13 +7,15 @@ import {
createTickInterface,
deleteTemporaryPasswordInterface,
getPasswordInterface,
getPasswordOfflineInterface,
} from '../interfaces/door.lock.interface';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import { PasswordEncryptionService } from './encryption.services';
import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto';
import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto';
@Injectable()
export class DoorLockService {
@ -93,18 +95,13 @@ export class DoorLockService {
HttpStatus.BAD_REQUEST,
);
}
const passwords = await this.getTemporaryPasswordsTuya(
const passwords = await this.getTemporaryOfflinePasswordsTuya(
deviceDetails.deviceTuyaUuid,
'multiple',
);
if (passwords.result.length > 0) {
const passwordFiltered = passwords.result.filter(
(item) =>
(!item.schedule_list || item.schedule_list.length === 0) &&
item.type === 0,
);
return convertKeysToCamelCase(passwordFiltered);
if (passwords.result.records.length > 0) {
return convertKeysToCamelCase(passwords.result.records);
}
return passwords;
@ -128,18 +125,13 @@ export class DoorLockService {
HttpStatus.BAD_REQUEST,
);
}
const passwords = await this.getTemporaryPasswordsTuya(
const passwords = await this.getTemporaryOfflinePasswordsTuya(
deviceDetails.deviceTuyaUuid,
'once',
);
if (passwords.result.length > 0) {
const passwordFiltered = passwords.result.filter(
(item) =>
(!item.schedule_list || item.schedule_list.length === 0) &&
item.type === 0, //temp solution
);
return convertKeysToCamelCase(passwordFiltered);
if (passwords.result.records.length > 0) {
return convertKeysToCamelCase(passwords.result.records);
}
return passwords;
@ -162,13 +154,13 @@ export class DoorLockService {
HttpStatus.BAD_REQUEST,
);
}
const passwords = await this.getTemporaryPasswordsTuya(
const passwords = await this.getOnlineTemporaryPasswordsTuya(
deviceDetails.deviceTuyaUuid,
);
if (passwords.result.length > 0) {
const passwordFiltered = passwords.result
.filter((item) => item.type === 0) //temp solution
.filter((item) => item.type === 0)
.map((password: any) => {
if (password.schedule_list?.length > 0) {
password.schedule_list = password.schedule_list.map(
@ -200,7 +192,7 @@ export class DoorLockService {
);
}
}
async getTemporaryPasswordsTuya(
async getOnlineTemporaryPasswordsTuya(
doorLockUuid: string,
): Promise<getPasswordInterface> {
try {
@ -219,25 +211,45 @@ export class DoorLockService {
);
}
}
async getTemporaryOfflinePasswordsTuya(
doorLockUuid: string,
type: string,
): Promise<getPasswordOfflineInterface> {
try {
const path = `/v1.0/devices/${doorLockUuid}/door-lock/offline-temp-password?pwd_type_codes=${type}&target_status=EFFECTIVE&page_no=1&page_size=100`;
const response = await this.tuya.request({
method: 'GET',
path,
});
return response as getPasswordOfflineInterface;
} catch (error) {
throw new HttpException(
'Error getting offline temporary passwords from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addOfflineMultipleTimeTemporaryPassword(
addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto,
doorLockUuid: string,
) {
try {
const createOnlinePass = await this.addOnlineTemporaryPassword(
addDoorLockOfflineTempDto,
doorLockUuid,
'multiple',
false,
);
if (!createOnlinePass) {
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 createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya(
addDoorLockOfflineTempDto,
createOnlinePass.id,
createOnlinePass.deviceTuyaUuid,
deviceDetails.deviceTuyaUuid,
'multiple',
addDoorLockOfflineTempMultipleTimeDto,
);
if (!createOnceOfflinePass.success) {
throw new HttpException(
@ -255,25 +267,22 @@ export class DoorLockService {
);
}
}
async addOfflineOneTimeTemporaryPassword(
addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
doorLockUuid: string,
) {
async addOfflineOneTimeTemporaryPassword(doorLockUuid: string) {
try {
const createOnlinePass = await this.addOnlineTemporaryPassword(
addDoorLockOfflineTempDto,
doorLockUuid,
'once',
false,
);
if (!createOnlinePass) {
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 createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya(
addDoorLockOfflineTempDto,
createOnlinePass.id,
createOnlinePass.deviceTuyaUuid,
deviceDetails.deviceTuyaUuid,
'once',
null,
);
if (!createOnceOfflinePass.success) {
throw new HttpException(
@ -292,10 +301,9 @@ export class DoorLockService {
}
}
async addOfflineTemporaryPasswordTuya(
addDoorLockDto: AddDoorLockOnlineInterface,
onlinePassId: number,
doorLockUuid: string,
type: string,
addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto,
): Promise<createTickInterface> {
try {
const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`;
@ -304,14 +312,12 @@ export class DoorLockService {
method: 'POST',
path,
body: {
name: addDoorLockDto.name,
...(type === 'multiple' && {
effective_time: addDoorLockDto.effectiveTime,
invalid_time: addDoorLockDto.invalidTime,
effective_time: addDoorLockOfflineTempMultipleTimeDto.effectiveTime,
invalid_time: addDoorLockOfflineTempMultipleTimeDto.invalidTime,
}),
type,
password_id: onlinePassId,
},
});
@ -326,8 +332,6 @@ export class DoorLockService {
async addOnlineTemporaryPassword(
addDoorLockDto: AddDoorLockOnlineInterface,
doorLockUuid: string,
type: string = 'once',
isOnline: boolean = true,
) {
try {
const passwordData = await this.getTicketAndEncryptedPassword(
@ -348,8 +352,6 @@ export class DoorLockService {
const createPass = await this.addOnlineTemporaryPasswordTuya(
addDeviceObj,
passwordData.deviceTuyaUuid,
type,
addDeviceObj.scheduleList ? isOnline : false,
);
if (!createPass.success) {
@ -429,13 +431,11 @@ export class DoorLockService {
async addOnlineTemporaryPasswordTuya(
addDeviceObj: addDeviceObjectInterface,
doorLockUuid: string,
type: string,
isOnline: boolean = true,
): Promise<createTickInterface> {
try {
const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`;
let scheduleList;
if (isOnline) {
if (addDeviceObj.scheduleList.length > 0) {
scheduleList = addDeviceObj.scheduleList.map((schedule) => ({
effective_time: this.timeToMinutes(schedule.effectiveTime),
invalid_time: this.timeToMinutes(schedule.invalidTime),
@ -453,11 +453,11 @@ export class DoorLockService {
invalid_time: addDeviceObj.invalidTime,
password_type: 'ticket',
ticket_id: addDeviceObj.ticketId,
...(isOnline && {
...(addDeviceObj.scheduleList.length > 0 && {
schedule_list: scheduleList,
}),
type: '0', //temporary solution,
type: '0',
},
});
@ -579,4 +579,64 @@ export class DoorLockService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
}
async updateOfflineTemporaryPassword(
updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto,
doorLockUuid: string,
passwordId: 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 updateOfflinePass = await this.updateOfflineTemporaryPasswordTuya(
deviceDetails.deviceTuyaUuid,
updateDoorLockOfflineTempDto,
passwordId,
);
if (!updateOfflinePass.success) {
throw new HttpException(updateOfflinePass.msg, HttpStatus.BAD_REQUEST);
}
return {
result: updateOfflinePass.result,
};
} catch (error) {
throw new HttpException(
error.message || 'Error updating offline temporary password from Tuya',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateOfflineTemporaryPasswordTuya(
doorLockUuid: string,
updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto,
passwordId: string,
): Promise<createTickInterface> {
try {
const path = `/v1.0/cloud/lock/${doorLockUuid}/door-lock/offline-temp-password/${passwordId}`;
const response = await this.tuya.request({
method: 'PUT',
path,
body: {
password_name: updateDoorLockOfflineTempDto.name,
},
});
return response as createTickInterface;
} catch (error) {
throw new HttpException(
'Error updating offline temporary password from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,45 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
@Injectable()
export class CheckProfilePictureGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
if (req.body) {
const { profilePicture } = req.body;
if (profilePicture) {
const isBase64 = /^data:image\/[a-z]+;base64,/.test(profilePicture);
if (!isBase64) {
throw new BadRequestException(
'Profile picture must be in base64 format.',
);
}
// Get the size of the base64 string (in bytes)
const base64StringLength =
profilePicture.length - 'data:image/[a-z]+;base64,'.length;
const base64ImageSizeInBytes = base64StringLength * 0.75; // Base64 encoding expands data by 33%
const maxSizeInBytes = 1 * 1024 * 1024; // 1 MB
// Check if the size exceeds the limit
if (base64ImageSizeInBytes > maxSizeInBytes) {
throw new BadRequestException(
'Profile picture size exceeds the allowed limit.',
);
}
}
// Check if profilePicture is a base64 string
} else {
throw new BadRequestException('Invalid request parameters');
}
return true;
} catch (error) {
console.log('Profile picture guard error: ', error);
throw error;
}
}
}

View File

@ -4,6 +4,7 @@ import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils';
import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded } from 'body-parser';
import { SeederService } from '@app/common/seed/services/seeder.service';
async function bootstrap() {
@ -11,6 +12,10 @@ async function bootstrap() {
app.enableCors();
// Set the body parser limit to 1 MB
app.use(json({ limit: '1mb' }));
app.use(urlencoded({ limit: '1mb', extended: true }));
app.use(
rateLimit({
windowMs: 5 * 60 * 1000,
@ -42,7 +47,8 @@ async function bootstrap() {
} catch (error) {
console.error('Seeding failed!', error);
}
console.log('Starting auth at port ...', process.env.PORT || 4000);
await app.listen(process.env.PORT || 4000);
}
console.log('Starting auth at port ...', process.env.PORT || 4000);
bootstrap();

View File

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

View File

@ -0,0 +1,33 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { RegionService } from '../services/region.service';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
@ApiTags('Region Module')
@Controller({
version: '1',
path: 'region',
})
export class RegionController {
constructor(private readonly regionService: RegionService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async getAllRegions() {
try {
return await this.regionService.getAllRegions();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { RegionService } from './services/region.service';
import { RegionController } from './controllers/region.controller';
import { ConfigModule } from '@nestjs/config';
import { RegionRepository } from '@app/common/modules/region/repositories';
@Module({
imports: [ConfigModule],
controllers: [RegionController],
providers: [RegionService, RegionRepository],
exports: [RegionService],
})
export class RegionModule {}

View File

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

View File

@ -0,0 +1,25 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { RegionRepository } from '@app/common/modules/region/repositories';
@Injectable()
export class RegionService {
constructor(private readonly regionRepository: RegionRepository) {}
async getAllRegions() {
try {
const regions = await this.regionRepository.find();
return regions;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Regions found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -21,3 +21,8 @@ export interface DeleteTapToRunSceneInterface {
msg?: string;
result: boolean;
}
export interface SceneDetailsResult {
id: string;
name: string;
type: string;
}

View File

@ -15,6 +15,7 @@ import {
AddTapToRunSceneInterface,
DeleteTapToRunSceneInterface,
GetTapToRunSceneByUnitInterface,
SceneDetailsResult,
} from '../interface/scene.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
@ -256,6 +257,18 @@ export class SceneService {
if (device) {
action.entityId = device.uuid;
}
} else if (
action.actionExecutor !== 'device_issue' &&
action.actionExecutor !== 'delay'
) {
const sceneDetails = await this.getTapToRunSceneDetailsTuya(
action.entityId,
);
if (sceneDetails.id) {
action.name = sceneDetails.name;
action.type = sceneDetails.type;
}
}
}
@ -275,6 +288,38 @@ export class SceneService {
}
}
}
async getTapToRunSceneDetailsTuya(
sceneId: string,
): Promise<SceneDetailsResult> {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const camelCaseResponse = convertKeysToCamelCase(response);
const { id, name, type } = camelCaseResponse.result;
return {
id,
name,
type,
} as SceneDetailsResult;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
'Scene not found for Tuya',
HttpStatus.NOT_FOUND,
);
}
}
}
async updateTapToRunScene(
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
sceneId: string,

View File

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

View File

@ -0,0 +1,33 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { TimeZoneService } from '../services/timezone.service';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
@ApiTags('TimeZone Module')
@Controller({
version: '1',
path: 'timezone',
})
export class TimeZoneController {
constructor(private readonly timeZoneService: TimeZoneService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async getAllTimeZones() {
try {
return await this.timeZoneService.getAllTimeZones();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

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

View File

@ -0,0 +1,25 @@
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
@Injectable()
export class TimeZoneService {
constructor(private readonly timeZoneRepository: TimeZoneRepository) {}
async getAllTimeZones() {
try {
const timeZones = await this.timeZoneRepository.find();
return timeZones;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('TimeZones found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TimeZoneService } from './services/timezone.service';
import { TimeZoneController } from './controllers/timezone.controller';
import { ConfigModule } from '@nestjs/config';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
@Module({
imports: [ConfigModule],
controllers: [TimeZoneController],
providers: [TimeZoneService, TimeZoneRepository],
exports: [TimeZoneService],
})
export class TimeZoneModule {}

View File

@ -1,8 +1,23 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Param,
Put,
UseGuards,
} from '@nestjs/common';
import { UserService } from '../services/user.service';
import { UserListDto } from '../dtos/user.list.dto';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import {
UpdateNameDto,
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
@ApiTags('User Module')
@Controller({
@ -13,13 +28,116 @@ export class UserController {
constructor(private readonly userService: UserService) {}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard)
@Get('list')
async userList(@Query() userListDto: UserListDto) {
@UseGuards(JwtAuthGuard)
@Get(':userUuid')
async getUserDetailsByUserUuid(@Param('userUuid') userUuid: string) {
try {
return await this.userService.userDetails(userListDto);
} catch (err) {
throw new Error(err);
return await this.userService.getUserDetailsByUserUuid(userUuid);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckProfilePictureGuard)
@Put('/profile-picture/:userUuid')
async updateProfilePictureByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateProfilePictureDataDto: UpdateProfilePictureDataDto,
) {
try {
const userData = await this.userService.updateProfilePictureByUserUuid(
userUuid,
updateProfilePictureDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'profile picture updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/region/:userUuid')
async updateRegionByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateRegionDataDto: UpdateRegionDataDto,
) {
try {
const userData = await this.userService.updateRegionByUserUuid(
userUuid,
updateRegionDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'region updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/timezone/:userUuid')
async updateNameByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateTimezoneDataDto: UpdateTimezoneDataDto,
) {
try {
const userData = await this.userService.updateTimezoneByUserUuid(
userUuid,
updateTimezoneDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'timezone updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/name/:userUuid')
async updateTimezoneByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateNameDto: UpdateNameDto,
) {
try {
const userData = await this.userService.updateNameByUserUuid(
userUuid,
updateNameDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'name updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1 +1 @@
export * from './user.list.dto';
export * from './update.user.dto';

View File

@ -0,0 +1,61 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateProfilePictureDataDto {
@ApiProperty({
description: 'profilePicture',
required: true,
})
@IsString()
@IsNotEmpty()
public profilePicture: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}
export class UpdateRegionDataDto {
@ApiProperty({
description: 'regionUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public regionUuid: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}
export class UpdateTimezoneDataDto {
@ApiProperty({
description: 'timezoneUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public timezoneUuid: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}
export class UpdateNameDto {
@ApiProperty({
description: 'firstName',
required: true,
})
@IsString()
@IsNotEmpty()
public firstName: string;
@ApiProperty({
description: 'lastName',
required: true,
})
@IsString()
@IsNotEmpty()
public lastName: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,32 +0,0 @@
import {
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
} from 'class-validator';
export class UserListDto {
@IsString()
@IsOptional()
schema: string;
@IsNumberString()
@IsNotEmpty()
page_no: number;
@IsNumberString()
@IsNotEmpty()
page_size: number;
@IsString()
@IsOptional()
username: string;
@IsNumberString()
@IsOptional()
start_time: number;
@IsNumberString()
@IsOptional()
end_time: number;
}

View File

@ -1,28 +1,240 @@
import { Injectable } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { UserListDto } from '../dtos/user.list.dto';
import { ConfigService } from '@nestjs/config';
import {
UpdateNameDto,
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from './../dtos/update.user.dto';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
@Injectable()
export class UserService {
private tuya: TuyaContext;
constructor(private readonly configService: ConfigService) {
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,
});
constructor(
private readonly userRepository: UserRepository,
private readonly regionRepository: RegionRepository,
private readonly timeZoneRepository: TimeZoneRepository,
) {}
async getUserDetailsByUserUuid(userUuid: string) {
try {
const user = await this.userRepository.findOne({
where: {
uuid: userUuid,
},
relations: ['region', 'timezone'],
});
if (!user) {
throw new BadRequestException('Invalid room UUID');
}
// Use the utility function to remove the base64 prefix
const cleanedProfilePicture = removeBase64Prefix(user.profilePicture);
return {
uuid: user.uuid,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
profilePicture: cleanedProfilePicture,
region: user.region,
timeZone: user.timezone,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
}
}
async userDetails(userListDto: UserListDto) {
const path = `/v2.0/apps/${userListDto.schema}/users`;
const data = await this.tuya.request({
method: 'GET',
path,
query: userListDto,
});
return data;
async updateProfilePictureByUserUuid(
userUuid: string,
updateProfilePictureDataDto: UpdateProfilePictureDataDto,
) {
try {
await this.userRepository.update(
{ uuid: userUuid },
{ ...updateProfilePictureDataDto },
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
// Use the utility function to remove the base64 prefix
const cleanedProfilePicture = removeBase64Prefix(
updatedUser.profilePicture,
);
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: cleanedProfilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
}
}
async updateRegionByUserUuid(
userUuid: string,
updateRegionDataDto: UpdateRegionDataDto,
) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Ensure the region UUID is provided
if (!updateRegionDataDto.regionUuid) {
throw new BadRequestException('Region UUID is required');
}
// Ensure the region exists
const region = await this.regionRepository.findOne({
where: {
uuid: updateRegionDataDto.regionUuid,
},
});
if (!region) {
throw new BadRequestException('Invalid region UUID');
}
await this.userRepository.update(
{ uuid: userUuid },
{
region: region,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.region) {
throw new BadRequestException('Region update failed');
}
// Use the utility function to remove the base64 prefix
const cleanedProfilePicture = removeBase64Prefix(
updatedUser.profilePicture,
);
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: cleanedProfilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
async updateTimezoneByUserUuid(
userUuid: string,
updateTimezoneDataDto: UpdateTimezoneDataDto,
) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Ensure the region UUID is provided
if (!updateTimezoneDataDto.timezoneUuid) {
throw new BadRequestException('Timezone UUID is required');
}
// Ensure the region exists
const timezone = await this.timeZoneRepository.findOne({
where: {
uuid: updateTimezoneDataDto.timezoneUuid,
},
});
if (!timezone) {
throw new BadRequestException('Invalid timezone UUID');
}
await this.userRepository.update(
{ uuid: userUuid },
{
timezone: timezone,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.timeZone) {
throw new BadRequestException('Timezone update failed');
}
// Use the utility function to remove the base64 prefix
const cleanedProfilePicture = removeBase64Prefix(
updatedUser.profilePicture,
);
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: cleanedProfilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
async updateNameByUserUuid(userUuid: string, updateNameDto: UpdateNameDto) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (!updateNameDto.firstName || !updateNameDto.lastName) {
throw new BadRequestException('First Name and Last Name is required');
}
await this.userRepository.update(
{ uuid: userUuid },
{
firstName: updateNameDto.firstName,
lastName: updateNameDto.lastName,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.firstName || !updatedUser.lastName) {
throw new BadRequestException('First Name and Last Name update failed');
}
// Use the utility function to remove the base64 prefix
const cleanedProfilePicture = removeBase64Prefix(
updatedUser.profilePicture,
);
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: cleanedProfilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
}

View File

@ -2,11 +2,19 @@ import { Module } from '@nestjs/common';
import { UserService } from './services/user.service';
import { UserController } from './controllers/user.controller';
import { ConfigModule } from '@nestjs/config';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
@Module({
imports: [ConfigModule],
controllers: [UserController],
providers: [UserService],
providers: [
UserService,
UserRepository,
RegionRepository,
TimeZoneRepository,
],
exports: [UserService],
})
export class UserModule {}

View File

@ -0,0 +1,33 @@
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsPasswordStrongConstraint
implements ValidatorConstraintInterface
{
validate(password: string) {
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])[A-Za-z\d!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{8,}$/;
return regex.test(password);
}
defaultMessage() {
return 'Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character from the set !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~.';
}
}
export function IsPasswordStrong(validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsPasswordStrongConstraint,
});
};
}