From 37e0effb07dc4254011f5cfd1950892df83d611f Mon Sep 17 00:00:00 2001 From: Viraj Parmar Date: Fri, 16 Feb 2024 17:34:29 +0530 Subject: [PATCH 001/259] user list api done --- apps/auth/src/auth.module.ts | 14 +- apps/auth/src/config/auth.config.ts | 10 + apps/auth/src/config/index.ts | 3 + .../authentication/authentication.module.ts | 12 + .../controllers/authentication.controller.ts | 14 ++ .../services/authentication.service.ts | 121 ++++++++++ apps/backend/src/app.controller.spec.ts | 22 -- apps/backend/src/app.module.ts | 10 - ...pp.controller.ts => backend.controller.ts} | 6 +- apps/backend/src/backend.module.ts | 18 ++ .../{app.service.ts => backend.service.ts} | 4 +- apps/backend/src/config/auth.config.ts | 10 + apps/backend/src/config/index.ts | 3 + apps/backend/src/main.ts | 4 +- .../user/controllers/user.controller.ts | 21 ++ .../src/modules/user/dtos/user.list.dto.ts | 27 +++ .../src/modules/user/services/user.service.ts | 33 +++ apps/backend/src/modules/user/user.module.ts | 12 + apps/backend/test/app.e2e-spec.ts | 24 -- apps/backend/test/jest-e2e.json | 9 - package-lock.json | 227 +++++++++++++++++- package.json | 10 +- 22 files changed, 528 insertions(+), 86 deletions(-) create mode 100644 apps/auth/src/config/auth.config.ts create mode 100644 apps/auth/src/config/index.ts create mode 100644 apps/auth/src/modules/authentication/authentication.module.ts create mode 100644 apps/auth/src/modules/authentication/controllers/authentication.controller.ts create mode 100644 apps/auth/src/modules/authentication/services/authentication.service.ts delete mode 100644 apps/backend/src/app.controller.spec.ts delete mode 100644 apps/backend/src/app.module.ts rename apps/backend/src/{app.controller.ts => backend.controller.ts} (61%) create mode 100644 apps/backend/src/backend.module.ts rename apps/backend/src/{app.service.ts => backend.service.ts} (63%) create mode 100644 apps/backend/src/config/auth.config.ts create mode 100644 apps/backend/src/config/index.ts create mode 100644 apps/backend/src/modules/user/controllers/user.controller.ts create mode 100644 apps/backend/src/modules/user/dtos/user.list.dto.ts create mode 100644 apps/backend/src/modules/user/services/user.service.ts create mode 100644 apps/backend/src/modules/user/user.module.ts delete mode 100644 apps/backend/test/app.e2e-spec.ts delete mode 100644 apps/backend/test/jest-e2e.json diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts index 03810e7..1701fc9 100644 --- a/apps/auth/src/auth.module.ts +++ b/apps/auth/src/auth.module.ts @@ -1,10 +1,18 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; - +import { ConfigModule } from '@nestjs/config'; +import config from './config'; +import { AuthenticationModule } from './modules/authentication/authentication.module'; +import { AuthenticationController } from './modules/authentication/controllers/authentication.controller'; @Module({ - imports: [], - controllers: [AuthController], + imports: [ + ConfigModule.forRoot({ + load: config, + }), + AuthenticationModule, + ], + controllers: [AuthController,AuthenticationController], providers: [AuthService], }) export class AuthModule {} diff --git a/apps/auth/src/config/auth.config.ts b/apps/auth/src/config/auth.config.ts new file mode 100644 index 0000000..bb2157b --- /dev/null +++ b/apps/auth/src/config/auth.config.ts @@ -0,0 +1,10 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'auth-config', + (): Record => ({ + DEVICE_ID: process.env.DEVICE_ID, + ACCESS_KEY: process.env.ACCESS_KEY, + SECRET_KEY: process.env.SECRET_KEY, + }), +); diff --git a/apps/auth/src/config/index.ts b/apps/auth/src/config/index.ts new file mode 100644 index 0000000..adf5667 --- /dev/null +++ b/apps/auth/src/config/index.ts @@ -0,0 +1,3 @@ +import AuthConfig from './auth.config'; + +export default [AuthConfig]; diff --git a/apps/auth/src/modules/authentication/authentication.module.ts b/apps/auth/src/modules/authentication/authentication.module.ts new file mode 100644 index 0000000..eb5c84c --- /dev/null +++ b/apps/auth/src/modules/authentication/authentication.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthenticationController } from './controllers/authentication.controller'; +import { AuthenticationService } from './services/authentication.service'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ConfigModule], + controllers: [AuthenticationController], + providers: [AuthenticationService], + exports: [AuthenticationService], +}) +export class AuthenticationModule {} diff --git a/apps/auth/src/modules/authentication/controllers/authentication.controller.ts b/apps/auth/src/modules/authentication/controllers/authentication.controller.ts new file mode 100644 index 0000000..931a522 --- /dev/null +++ b/apps/auth/src/modules/authentication/controllers/authentication.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Post } from '@nestjs/common'; +import { AuthenticationService } from '../services/authentication.service'; + +@Controller({ + version: '1', + path: 'authentication', +}) +export class AuthenticationController { + constructor(private readonly authenticationService: AuthenticationService) {} + @Post('auth') + async Authentication() { + return await this.authenticationService.main(); + } +} diff --git a/apps/auth/src/modules/authentication/services/authentication.service.ts b/apps/auth/src/modules/authentication/services/authentication.service.ts new file mode 100644 index 0000000..f7e002c --- /dev/null +++ b/apps/auth/src/modules/authentication/services/authentication.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; +import * as qs from 'qs'; +import * as crypto from 'crypto'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +@Injectable() +export class AuthenticationService { + private token: string; + private deviceId: string; + private accessKey: string; + private secretKey: string; + + constructor(private readonly configService: ConfigService) { + (this.deviceId = this.configService.get('auth-config.DEVICE_ID')), + (this.accessKey = this.configService.get( + 'auth-config.ACCESS_KEY', + )), + (this.secretKey = this.configService.get( + 'auth-config.SECRET_KEY', + )); + } + + async main() { + await this.getToken(); + const data = await this.getDeviceInfo(this.deviceId); + console.log('fetch success: ', JSON.stringify(data)); + return JSON.stringify(data); + } + + async getToken() { + const method = 'GET'; + const timestamp = Date.now().toString(); + const signUrl = 'https://openapi.tuyaeu.com/v1.0/token?grant_type=1'; + const contentHash = crypto.createHash('sha256').update('').digest('hex'); + const stringToSign = [method, contentHash, '', signUrl].join('\n'); + const signStr = this.accessKey + timestamp + stringToSign; + + const headers = { + t: timestamp, + sign_method: 'HMAC-SHA256', + client_id: this.accessKey, + sign: await this.encryptStr(signStr, this.secretKey), + }; + + const { data: login } = await axios.get( + 'https://openapi.tuyaeu.com/v1.0/token', + { + params: { + grant_type: 1, + }, + headers, + }, + ); + + if (!login || !login.success) { + throw new Error(`fetch failed: ${login.msg}`); + } + this.token = login.result.access_token; + } + + async getDeviceInfo(deviceId: string) { + const query = {}; + const method = 'POST'; + const url = `https://openapi.tuyaeu.com/v1.0/devices/${deviceId}/commands`; + const reqHeaders: { [k: string]: string } = await this.getRequestSign( + url, + method, + {}, + query, + ); + + const { data } = await axios.post(url, {}, reqHeaders); + + if (!data || !data.success) { + throw new Error(`request api failed: ${data.msg}`); + } + + return data; + } + + async encryptStr(str: string, secret: string): Promise { + return crypto + .createHmac('sha256', secret) + .update(str, 'utf8') + .digest('hex') + .toUpperCase(); + } + + async getRequestSign( + path: string, + method: string, + headers: { [k: string]: string } = {}, + query: { [k: string]: any } = {}, + body: { [k: string]: any } = {}, + ) { + const t = Date.now().toString(); + const [uri, pathQuery] = path.split('?'); + const queryMerged = Object.assign(query, qs.parse(pathQuery)); + const sortedQuery: { [k: string]: string } = {}; + Object.keys(queryMerged) + .sort() + .forEach((i) => (sortedQuery[i] = query[i])); + + const querystring = decodeURIComponent(qs.stringify(sortedQuery)); + const url = querystring ? `${uri}?${querystring}` : uri; + const contentHash = crypto + .createHash('sha256') + .update(JSON.stringify(body)) + .digest('hex'); + const stringToSign = [method, contentHash, '', url].join('\n'); + const signStr = this.accessKey + this.token + t + stringToSign; + return { + t, + path: url, + client_id: this.accessKey, + sign: await this.encryptStr(signStr, this.secretKey), + sign_method: 'HMAC-SHA256', + access_token: this.token, + }; + } +} diff --git a/apps/backend/src/app.controller.spec.ts b/apps/backend/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/apps/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts deleted file mode 100644 index 8662803..0000000 --- a/apps/backend/src/app.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/backend.controller.ts similarity index 61% rename from apps/backend/src/app.controller.ts rename to apps/backend/src/backend.controller.ts index cce879e..6fc2a0e 100644 --- a/apps/backend/src/app.controller.ts +++ b/apps/backend/src/backend.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; +import { AppService } from './backend.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - @Get() + @Get('healthcheck') getHello(): string { - return this.appService.getHello(); + return this.appService.healthcheck(); } } diff --git a/apps/backend/src/backend.module.ts b/apps/backend/src/backend.module.ts new file mode 100644 index 0000000..c41a9b8 --- /dev/null +++ b/apps/backend/src/backend.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './backend.controller'; +import { AppService } from './backend.service'; +import { UserModule } from './modules/user/user.module'; +import { ConfigModule } from '@nestjs/config'; +import config from './config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + load: config, + }), + UserModule, + ], + controllers: [AppController], + providers: [AppService], +}) +export class BackendModule {} diff --git a/apps/backend/src/app.service.ts b/apps/backend/src/backend.service.ts similarity index 63% rename from apps/backend/src/app.service.ts rename to apps/backend/src/backend.service.ts index 927d7cc..4f15a30 100644 --- a/apps/backend/src/app.service.ts +++ b/apps/backend/src/backend.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; + healthcheck(): string { + return 'Healthcheck!'; } } diff --git a/apps/backend/src/config/auth.config.ts b/apps/backend/src/config/auth.config.ts new file mode 100644 index 0000000..3a3f870 --- /dev/null +++ b/apps/backend/src/config/auth.config.ts @@ -0,0 +1,10 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'auth-config', + (): Record => ({ + //DEVICE_ID: process.env.DEVICE_ID, + ACCESS_KEY: process.env.ACCESS_KEY, + SECRET_KEY: process.env.SECRET_KEY, + }), +); diff --git a/apps/backend/src/config/index.ts b/apps/backend/src/config/index.ts new file mode 100644 index 0000000..adf5667 --- /dev/null +++ b/apps/backend/src/config/index.ts @@ -0,0 +1,3 @@ +import AuthConfig from './auth.config'; + +export default [AuthConfig]; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index add741b..9343e74 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,8 +1,8 @@ import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import { BackendModule } from './backend.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(BackendModule); await app.listen(6000); } bootstrap(); diff --git a/apps/backend/src/modules/user/controllers/user.controller.ts b/apps/backend/src/modules/user/controllers/user.controller.ts new file mode 100644 index 0000000..4b3c5bb --- /dev/null +++ b/apps/backend/src/modules/user/controllers/user.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { UserService } from '../services/user.service'; +import { UserListDto } from '../dtos/user.list.dto'; + +//@ApiTags('User Module') +@Controller({ + version: '1', + path: 'user', +}) +export class UserController { + constructor(private readonly userService: UserService) {} + + @Get('list') + async userList(@Query() userListDto: UserListDto) { + try { + return await this.userService.userDetails(userListDto); + } catch (err) { + throw new Error(err); + } + } +} diff --git a/apps/backend/src/modules/user/dtos/user.list.dto.ts b/apps/backend/src/modules/user/dtos/user.list.dto.ts new file mode 100644 index 0000000..da7d79a --- /dev/null +++ b/apps/backend/src/modules/user/dtos/user.list.dto.ts @@ -0,0 +1,27 @@ +import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class UserListDto { + @IsString() + @IsOptional() + schema: string; + + @IsNumber() + @IsNotEmpty() + page_no: number; + + @IsNumber() + @IsNotEmpty() + page_size: number; + + @IsString() + @IsOptional() + username: string; + + @IsNumber() + @IsOptional() + start_time: number; + + @IsNumber() + @IsOptional() + end_time: number; +} diff --git a/apps/backend/src/modules/user/services/user.service.ts b/apps/backend/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..e21e2b8 --- /dev/null +++ b/apps/backend/src/modules/user/services/user.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { UserListDto } from '../dtos/user.list.dto'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class UserService { + private accessKey: string; + private secretKey: string; + constructor(private readonly configService: ConfigService) { + (this.accessKey = this.configService.get('auth-config.ACCESS_KEY')), + (this.secretKey = this.configService.get( + 'auth-config.SECRET_KEY', + )); + } + async userDetails(userListDto: UserListDto) { + const url = `https://openapi.tuyaeu.com/v2.0/apps/${userListDto.schema}/users`; + + const tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyacn.com', + accessKey:this.secretKey, + secretKey:this.accessKey, + }); + + const data = await tuya.request({ + method: 'GET', + path: url, + query: userListDto, + }); + + return data; + } +} diff --git a/apps/backend/src/modules/user/user.module.ts b/apps/backend/src/modules/user/user.module.ts new file mode 100644 index 0000000..be9e33d --- /dev/null +++ b/apps/backend/src/modules/user/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './services/user.service'; +import { UserController } from './controllers/user.controller'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ConfigModule], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) +export class UserModule {} diff --git a/apps/backend/test/app.e2e-spec.ts b/apps/backend/test/app.e2e-spec.ts deleted file mode 100644 index 50cda62..0000000 --- a/apps/backend/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/apps/backend/test/jest-e2e.json b/apps/backend/test/jest-e2e.json deleted file mode 100644 index e9d912f..0000000 --- a/apps/backend/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/package-lock.json b/package-lock.json index 5e884ee..591269f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,14 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@tuya/tuya-connector-nodejs": "^2.1.2", + "axios": "^1.6.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "ioredis": "^5.3.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -1069,6 +1075,11 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1747,6 +1758,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", + "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "dependencies": { + "dotenv": "16.4.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", @@ -1975,6 +2001,23 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@tuya/tuya-connector-nodejs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tuya/tuya-connector-nodejs/-/tuya-connector-nodejs-2.1.2.tgz", + "integrity": "sha512-8tM7QlOF1QQrT3iQgcHp4JDNRUdOyi06h8F5ZL7antQZYP67TRQ2/puisoo2uhdXo+n+GT0B605pWaDqr9nPrA==", + "dependencies": { + "axios": "^0.21.1", + "qs": "^6.10.1" + } + }, + "node_modules/@tuya/tuya-connector-nodejs/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2233,6 +2276,11 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2816,8 +2864,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3292,6 +3349,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -3380,6 +3452,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3416,7 +3496,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3615,7 +3694,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3690,11 +3768,18 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3773,6 +3858,25 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4546,6 +4650,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4616,7 +4739,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5094,6 +5216,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6128,6 +6273,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.56", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.56.tgz", + "integrity": "sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6161,8 +6311,17 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6401,8 +6560,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -6955,6 +7113,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7106,6 +7269,25 @@ "node": ">= 0.10" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", @@ -7693,6 +7875,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8447,6 +8634,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8467,6 +8666,14 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 497acf1..1ce8852 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", + "backend:dev": "nest start backend --watch", + "auth:dev": "nest start auth --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/apps/backend/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -21,8 +23,14 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@tuya/tuya-connector-nodejs": "^2.1.2", + "axios": "^1.6.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "ioredis": "^5.3.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -69,4 +77,4 @@ "/apps/" ] } -} \ No newline at end of file +} From 4ffd94ec780b94a1035c7132046c154732971dfd Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 18 Feb 2024 17:01:43 +0300 Subject: [PATCH 002/259] functioning API Call --- .../services/authentication.service.ts | 2 +- .../user/controllers/user.controller.ts | 2 +- .../src/modules/user/services/user.service.ts | 29 ++++++++----------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/apps/auth/src/modules/authentication/services/authentication.service.ts b/apps/auth/src/modules/authentication/services/authentication.service.ts index f7e002c..dc9e0b2 100644 --- a/apps/auth/src/modules/authentication/services/authentication.service.ts +++ b/apps/auth/src/modules/authentication/services/authentication.service.ts @@ -112,7 +112,7 @@ export class AuthenticationService { return { t, path: url, - client_id: this.accessKey, + client_id: 'this.accessKey', sign: await this.encryptStr(signStr, this.secretKey), sign_method: 'HMAC-SHA256', access_token: this.token, diff --git a/apps/backend/src/modules/user/controllers/user.controller.ts b/apps/backend/src/modules/user/controllers/user.controller.ts index 4b3c5bb..901506a 100644 --- a/apps/backend/src/modules/user/controllers/user.controller.ts +++ b/apps/backend/src/modules/user/controllers/user.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { UserService } from '../services/user.service'; import { UserListDto } from '../dtos/user.list.dto'; diff --git a/apps/backend/src/modules/user/services/user.service.ts b/apps/backend/src/modules/user/services/user.service.ts index e21e2b8..d6b3610 100644 --- a/apps/backend/src/modules/user/services/user.service.ts +++ b/apps/backend/src/modules/user/services/user.service.ts @@ -5,29 +5,24 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class UserService { - private accessKey: string; - private secretKey: string; + private tuya: TuyaContext; constructor(private readonly configService: ConfigService) { - (this.accessKey = this.configService.get('auth-config.ACCESS_KEY')), - (this.secretKey = this.configService.get( - 'auth-config.SECRET_KEY', - )); + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); } async userDetails(userListDto: UserListDto) { - const url = `https://openapi.tuyaeu.com/v2.0/apps/${userListDto.schema}/users`; - - const tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyacn.com', - accessKey:this.secretKey, - secretKey:this.accessKey, - }); - - const data = await tuya.request({ + const path = `/v2.0/apps/${userListDto.schema}/users`; + const data = await this.tuya.request({ method: 'GET', - path: url, + path, query: userListDto, }); - return data; } } From 3758aaf8f9fd8f725c12d7f1236c32a238deeb8e Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 16:41:47 -0500 Subject: [PATCH 003/259] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_syncrow-dev.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/dev_syncrow-dev.yml diff --git a/.github/workflows/dev_syncrow-dev.yml b/.github/workflows/dev_syncrow-dev.yml new file mode 100644 index 0000000..134b4cd --- /dev/null +++ b/.github/workflows/dev_syncrow-dev.yml @@ -0,0 +1,71 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - syncrow-dev + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v3 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_F5089EEA95DF450E90E990B230B63FEA }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_6A9379B9B88748918EE02EE725C051AD }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_8041F0A416EB4B24ADE667B446A2BD0D }} + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'syncrow-dev' + slot-name: 'Production' + package: . + \ No newline at end of file From 482a71f53c64dbf3a406293ac01acbfd8d509f42 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 19 Feb 2024 23:18:22 +0300 Subject: [PATCH 004/259] Create azure-webapps-node.yml --- .github/workflows/azure-webapps-node.yml | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/azure-webapps-node.yml diff --git a/.github/workflows/azure-webapps-node.yml b/.github/workflows/azure-webapps-node.yml new file mode 100644 index 0000000..c648b24 --- /dev/null +++ b/.github/workflows/azure-webapps-node.yml @@ -0,0 +1,59 @@ +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 }} From d34bc13d90b39a06704977f91cf700b0614c24dd Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 20 Feb 2024 14:04:18 +0300 Subject: [PATCH 005/259] change ports --- apps/auth/src/main.ts | 2 +- apps/backend/src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index 9572bdb..d1e14e8 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -3,6 +3,6 @@ import { AuthModule } from './auth.module'; async function bootstrap() { const app = await NestFactory.create(AuthModule); - await app.listen(6001); + await app.listen(7001); } bootstrap(); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index add741b..7af4000 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -3,6 +3,6 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(6000); + await app.listen(7000); } bootstrap(); From ff1bb4cc418dda7402ce4ebd3b52030c78d0414a Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 16:56:41 -0500 Subject: [PATCH 006/259] Docerizing and proxying the microservices --- Dockerfile | 25 +++++++++++ apps/auth/src/main.ts | 1 + apps/backend/src/main.ts | 2 + bun.lockb | Bin 0 -> 286429 bytes nginx.conf | 29 ++++++++++++ package-lock.json | 92 +++++++++++++++++++++++++++++++++++++++ package.json | 8 +++- 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100755 bun.lockb create mode 100644 nginx.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f218aeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:20 as builder + +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build auth +RUN npm run build backend + +FROM nginx:alpine + +RUN rm /etc/nginx/conf.d/default.conf + +COPY nginx.conf /etc/nginx/conf.d + +COPY --from=builder /usr/src/app/dist/apps/auth /usr/share/nginx/html/auth +COPY --from=builder /usr/src/app/dist/apps/backend /usr/share/nginx/html/backend + +EXPOSE 443 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index d1e14e8..48eee43 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -5,4 +5,5 @@ async function bootstrap() { const app = await NestFactory.create(AuthModule); await app.listen(7001); } +console.log('Starting auth at port 7001...'); bootstrap(); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 7af4000..c327e3a 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -5,4 +5,6 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(7000); } + +console.log('Starting backend at port 7000...'); bootstrap(); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..aafac7209b6f7d998a51ac765adaacfba39ac5c2 GIT binary patch literal 286429 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.9.0" + } + }, "node_modules/@babel/template": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", @@ -3478,6 +3491,48 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -3611,6 +3666,22 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7111,6 +7182,12 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -7535,6 +7612,15 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", @@ -7666,6 +7752,12 @@ "node": ">=0.10.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 497acf1..211f952 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" + "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json", + "start:auth": "nest start auth", + "start:backend": "nest start backend", + "start:all": "concurrently \"npm run start:auth\" \"npm run start:backend\"" }, "dependencies": { "@nestjs/common": "^10.0.0", @@ -36,6 +39,7 @@ "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "concurrently": "^8.2.2", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -69,4 +73,4 @@ "/apps/" ] } -} \ No newline at end of file +} From cfb293843e4cef754bf507fa702ed5a2c4a09f62 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 01:36:33 +0300 Subject: [PATCH 007/259] working dockerized environment --- Dockerfile | 24 ++++++++++++++---------- bun.lockb | Bin 286429 -> 286429 bytes nginx.conf | 31 ++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index f218aeb..109471d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:20 as builder -WORKDIR /usr/src/app +WORKDIR /src/app COPY package*.json ./ @@ -8,18 +8,22 @@ RUN npm install COPY . . -RUN npm run build auth -RUN npm run build backend +RUN npm run build -FROM nginx:alpine +# Runtime stage +FROM node:20-alpine -RUN rm /etc/nginx/conf.d/default.conf +RUN apk add --no-cache nginx +COPY nginx.conf /etc/nginx/nginx.conf -COPY nginx.conf /etc/nginx/conf.d +WORKDIR /app -COPY --from=builder /usr/src/app/dist/apps/auth /usr/share/nginx/html/auth -COPY --from=builder /usr/src/app/dist/apps/backend /usr/share/nginx/html/backend +COPY --from=builder /src/app/dist/apps/auth ./auth +COPY --from=builder /src/app/dist/apps/backend ./backend +COPY package*.json ./ -EXPOSE 443 +RUN npm install -CMD ["nginx", "-g", "daemon off;"] +EXPOSE 80 + +CMD ["sh", "-c", "nginx -g 'daemon off;' & node auth/main.js & node backend/main.js"] diff --git a/bun.lockb b/bun.lockb index aafac7209b6f7d998a51ac765adaacfba39ac5c2..d8412183864ff8303a75e7d80bcbd80d3c59380a 100755 GIT binary patch delta 48375 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 Tl(JF-)daY7a_sbX8Pezf`wY*H delta 48232 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 diff --git a/nginx.conf b/nginx.conf index 6c402b4..6e330da 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,4 +1,8 @@ -events {} +worker_processes 1; + +events { + worker_connections 1024; +} http { include mime.types; @@ -7,18 +11,23 @@ http { keepalive_timeout 65; server { - listen 443; - server_name localhost; + listen 80; - location /auth { - alias /usr/share/nginx/html/auth; - try_files $uri $uri/ /auth/index.html; - } + location /auth { + proxy_pass http://localhost:7001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } - location /backend { - alias /usr/share/nginx/html/backend; - try_files $uri $uri/ /backend/index.html; - } + location / { + proxy_pass http://localhost:7000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } # error pages error_page 500 502 503 504 /50x.html; From 03c2dbf1d3a2bef54354b4cc0c0ad020d439a487 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 17:38:46 -0500 Subject: [PATCH 008/259] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_syncrow(dev).yml | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/dev_syncrow(dev).yml diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml new file mode 100644 index 0000000..8c30361 --- /dev/null +++ b/.github/workflows/dev_syncrow(dev).yml @@ -0,0 +1,71 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - syncrow + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v3 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'dev' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_B0E5D07AA5C94CCABC05A696CFDFA185 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D2A87194F5BF4F2CA24CE8C4D3EDDF24 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_4E6606241F0B4089BAFF050BC609DDE0 }} + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'syncrow' + slot-name: 'dev' + package: . + \ No newline at end of file From 2f4364abb3eb8787d7aa77beedc3618146e1e13f Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:04:06 +0300 Subject: [PATCH 009/259] updating the pipeline --- .github/workflows/azure-webapps-node.yml | 59 -------------------- .github/workflows/dev_syncrow(dev).yml | 42 +++++++------- .github/workflows/dev_syncrow-dev.yml | 71 ------------------------ 3 files changed, 21 insertions(+), 151 deletions(-) delete mode 100644 .github/workflows/azure-webapps-node.yml delete mode 100644 .github/workflows/dev_syncrow-dev.yml diff --git a/.github/workflows/azure-webapps-node.yml b/.github/workflows/azure-webapps-node.yml deleted file mode 100644 index c648b24..0000000 --- a/.github/workflows/azure-webapps-node.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 8c30361..6ebdc01 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,6 +1,3 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - name: Build and deploy Node.js app to Azure Web App - syncrow on: @@ -9,6 +6,11 @@ on: - dev workflow_dispatch: +env: + NODE_VERSION: '20.x' + AZURE_WEB_APP_NAME: 'syncrow' + AZURE_SLOT_NAME: 'dev' + jobs: build: runs-on: ubuntu-latest @@ -16,10 +18,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Node.js version + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20.x' + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' - name: npm install, build, and test run: | @@ -28,7 +31,7 @@ jobs: npm run test --if-present - name: Zip artifact for deployment - run: zip release.zip ./* -r + run: zip -r release.zip . -x node_modules\* \*.git\* \*.github\* \*tests\* - name: Upload artifact for deployment job uses: actions/upload-artifact@v3 @@ -41,9 +44,10 @@ jobs: needs: build environment: name: 'dev' + # Dynamically set or adjust as needed url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT + permissions: + id-token: write # For federated credentials, if applicable steps: - name: Download artifact from build job @@ -53,19 +57,15 @@ jobs: - name: Unzip artifact for deployment run: unzip release.zip - - - name: Login to Azure - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_B0E5D07AA5C94CCABC05A696CFDFA185 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D2A87194F5BF4F2CA24CE8C4D3EDDF24 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_4E6606241F0B4089BAFF050BC609DDE0 }} + + - name: Login to Azure + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp + - name: Deploy to Azure Web App uses: azure/webapps-deploy@v2 with: - app-name: 'syncrow' - slot-name: 'dev' - package: . - \ No newline at end of file + app-name: ${{ env.AZURE_WEB_APP_NAME }} + slot-name: ${{ env.AZURE_SLOT_NAME }} + package: release.zip diff --git a/.github/workflows/dev_syncrow-dev.yml b/.github/workflows/dev_syncrow-dev.yml deleted file mode 100644 index 134b4cd..0000000 --- a/.github/workflows/dev_syncrow-dev.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Node.js app to Azure Web App - syncrow-dev - -on: - push: - branches: - - dev - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js version - uses: actions/setup-node@v3 - with: - node-version: '20.x' - - - name: npm install, build, and test - run: | - npm install - npm run build --if-present - npm run test --if-present - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v3 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: Login to Azure - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_F5089EEA95DF450E90E990B230B63FEA }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_6A9379B9B88748918EE02EE725C051AD }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_8041F0A416EB4B24ADE667B446A2BD0D }} - - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp - uses: azure/webapps-deploy@v2 - with: - app-name: 'syncrow-dev' - slot-name: 'Production' - package: . - \ No newline at end of file From 21a41deddb518811b276ea21a06161c287aa5b22 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:11:13 +0300 Subject: [PATCH 010/259] downdgrade azure loging to v1 --- .github/workflows/dev_syncrow(dev).yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 6ebdc01..65b9294 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -59,7 +59,7 @@ jobs: run: unzip release.zip - name: Login to Azure - uses: azure/login@v2 + uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret From 86769de18bcff916b4306cfc04742e15ca744e8f Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:26:25 +0300 Subject: [PATCH 011/259] use azure container registry --- .github/workflows/dev_syncrow(dev).yml | 75 ++++++++------------------ 1 file changed, 23 insertions(+), 52 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 65b9294..00ed555 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,4 +1,4 @@ -name: Build and deploy Node.js app to Azure Web App - syncrow +name: Build and deploy Docker image to Azure Web App for Containers on: push: @@ -7,65 +7,36 @@ on: workflow_dispatch: env: - NODE_VERSION: '20.x' AZURE_WEB_APP_NAME: 'syncrow' - AZURE_SLOT_NAME: 'dev' + AZURE_WEB_APP_SLOT_NAME: 'dev' + ACR_REGISTRY: 'syncrow.azurecr.io' # Replace with your ACR name + IMAGE_NAME: 'backend' # Replace with your image name + IMAGE_TAG: 'latest' # Consider using dynamic tags, e.g., GitHub SHA jobs: - build: + build_and_deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - 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: Zip artifact for deployment - run: zip -r release.zip . -x node_modules\* \*.git\* \*.github\* \*tests\* - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v3 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'dev' - # Dynamically set or adjust as needed - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write # For federated credentials, if applicable - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: Login to Azure + - name: Log in to Azure uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret + creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Deploy to Azure Web App - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEB_APP_NAME }} - slot-name: ${{ env.AZURE_SLOT_NAME }} - package: release.zip + - name: Log in to Azure Container Registry + run: az acr login --name ${{ env.ACR_REGISTRY }} + + - name: Build and push Docker image + run: | + docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + - name: Set Web App with Docker container + run: | + az webapp config container set \ + --name ${{ env.AZURE_WEB_APP_NAME }} \ + --resource-group YourResourceGroupName \ # Replace with your resource group name + --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From ea7ad0fafda51bd475dc857e26e53ccd4dd7d323 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:27:26 +0300 Subject: [PATCH 012/259] use azure container registry --- .github/workflows/dev_syncrow(dev).yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 00ed555..c779f3c 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -9,9 +9,9 @@ on: env: AZURE_WEB_APP_NAME: 'syncrow' AZURE_WEB_APP_SLOT_NAME: 'dev' - ACR_REGISTRY: 'syncrow.azurecr.io' # Replace with your ACR name - IMAGE_NAME: 'backend' # Replace with your image name - IMAGE_TAG: 'latest' # Consider using dynamic tags, e.g., GitHub SHA + ACR_REGISTRY: 'syncrow.azurecr.io' + IMAGE_NAME: 'backend' + IMAGE_TAG: 'latest' jobs: build_and_deploy: @@ -37,6 +37,6 @@ jobs: run: | az webapp config container set \ --name ${{ env.AZURE_WEB_APP_NAME }} \ - --resource-group YourResourceGroupName \ # Replace with your resource group name + --resource-group syncrow \ --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From b4504a03fdd23cef56d4206a19c27449d34b2d49 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:36:37 +0300 Subject: [PATCH 013/259] troubleshooting pipeline --- .github/workflows/dev_syncrow(dev).yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index c779f3c..4250a37 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -28,6 +28,9 @@ jobs: - name: Log in to Azure Container Registry run: az acr login --name ${{ env.ACR_REGISTRY }} + - name: List build output + run: ls -R dist/apps/ + - name: Build and push Docker image run: | docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} From fee8c94c427dd632c81c0def60853e10d317fc47 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:40:43 +0300 Subject: [PATCH 014/259] troubleshooting pipeline --- .github/workflows/dev_syncrow(dev).yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 4250a37..9c58403 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -20,6 +20,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies and build project + run: | + npm install + npm run build + + - name: List build output + run: ls -R dist/apps/ + - name: Log in to Azure uses: azure/login@v1 with: From 8a5f6597d7707def42f07d0756d65258337f8cbf Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 03:02:39 +0300 Subject: [PATCH 015/259] fix dockerfile --- Dockerfile | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 109471d..25be02c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ -FROM node:20 as builder +FROM node:20-alpine -WORKDIR /src/app +RUN apk add --no-cache nginx + +COPY nginx.conf /etc/nginx/nginx.conf + +WORKDIR /app COPY package*.json ./ @@ -10,20 +14,6 @@ COPY . . RUN npm run build -# Runtime stage -FROM node:20-alpine - -RUN apk add --no-cache nginx -COPY nginx.conf /etc/nginx/nginx.conf - -WORKDIR /app - -COPY --from=builder /src/app/dist/apps/auth ./auth -COPY --from=builder /src/app/dist/apps/backend ./backend -COPY package*.json ./ - -RUN npm install - EXPOSE 80 -CMD ["sh", "-c", "nginx -g 'daemon off;' & node auth/main.js & node backend/main.js"] +CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] \ No newline at end of file From 7589038172bc14c2d56be83a762bcfa317f7c128 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 03:10:20 +0300 Subject: [PATCH 016/259] fix resource group --- .github/workflows/dev_syncrow(dev).yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 9c58403..fe53da8 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -53,6 +53,6 @@ jobs: run: | az webapp config container set \ --name ${{ env.AZURE_WEB_APP_NAME }} \ - --resource-group syncrow \ + --resource-group backend \ --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From 0764793874f924de39f88d286c88431fcb463f7c Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Mon, 26 Feb 2024 11:23:44 +0530 Subject: [PATCH 017/259] authentication module done --- apps/auth/src/config/app.config.ts | 8 + apps/auth/src/config/index.ts | 3 +- apps/auth/src/config/jwt.config.ts | 11 + apps/auth/src/main.ts | 24 + .../authentication/authentication.module.ts | 21 +- .../constants/login.response.constant.ts | 3 + .../controllers/authentication.controller.ts | 2 + .../authentication/controllers/index.ts | 2 + .../controllers/user-auth.controller.ts | 95 ++ .../src/modules/authentication/dtos/index.ts | 4 + .../authentication/dtos/user-auth.dto.ts | 36 + .../authentication/dtos/user-login.dto.ts | 14 + .../authentication/dtos/user-otp.dto.ts | 22 + .../authentication/dtos/user-password.dto.ts | 14 + .../modules/authentication/services/index.ts | 2 + .../services/user-auth.service.ts | 151 +++ apps/backend/src/backend.module.ts | 2 + apps/backend/src/config/app.config.ts | 8 + apps/backend/src/config/index.ts | 5 +- apps/backend/src/config/jwt.config.ts | 11 + apps/backend/src/main.ts | 18 + .../src/modules/user/controllers/index.ts | 1 + .../user/controllers/user.controller.ts | 3 +- apps/backend/src/modules/user/dtos/index.ts | 1 + .../src/modules/user/services/index.ts | 1 + libs/common/src/auth/auth.module.ts | 28 + .../src/auth/interfaces/auth.interface.ts | 7 + libs/common/src/auth/services/auth.service.ts | 55 ++ .../src/auth/strategies/jwt.strategy.ts | 39 + libs/common/src/common.module.ts | 22 + libs/common/src/common.service.spec.ts | 18 + libs/common/src/common.service.ts | 4 + libs/common/src/config/email.config.ts | 9 + libs/common/src/config/index.ts | 2 + libs/common/src/constants/otp-type.enum.ts | 4 + libs/common/src/database/database.module.ts | 38 + libs/common/src/database/strategies/index.ts | 1 + .../strategies/snack-naming.strategy.ts | 62 ++ libs/common/src/guards/jwt.auth.guard.ts | 11 + libs/common/src/helper/helper.module.ts | 11 + .../helper/services/helper.hash.service.ts | 63 ++ libs/common/src/helper/services/index.ts | 1 + libs/common/src/index.ts | 2 + .../src/modules/abstract/dtos/abstract.dto.ts | 13 + .../common/src/modules/abstract/dtos/index.ts | 1 + .../abstract/entities/abstract.entity.ts | 47 + .../src/modules/session/dtos/session.dto.ts | 26 + .../src/modules/session/entities/index.ts | 1 + .../session/entities/session.entity.ts | 31 + .../repositories/session.repository.ts | 10 + .../session/session.repository.module.ts | 11 + .../common/src/modules/user-otp/dtos/index.ts | 1 + .../src/modules/user-otp/dtos/user-otp.dto.ts | 19 + .../src/modules/user-otp/entities/index.ts | 1 + .../user-otp/entities/user-otp.entity.ts | 32 + .../modules/user-otp/repositories/index.ts | 0 .../repositories/user-otp.repository.ts | 10 + .../user-otp/user-otp.repository.module.ts | 11 + libs/common/src/modules/user/dtos/index.ts | 1 + libs/common/src/modules/user/dtos/user.dto.ts | 23 + .../common/src/modules/user/entities/index.ts | 0 .../src/modules/user/entities/user.entity.ts | 41 + .../src/modules/user/repositories/index.ts | 1 + .../user/repositories/user.repository.ts | 10 + .../modules/user/user.repository.module.ts | 11 + .../common/src/response/response.decorator.ts | 4 + .../src/response/response.interceptor.ts | 5 + libs/common/src/util/email.service.ts | 38 + libs/common/src/util/types.ts | 46 + .../src/util/user-auth.swagger.utils.ts | 21 + libs/common/tsconfig.lib.json | 9 + nest-cli.json | 9 + package-lock.json | 896 ++++++++++++++++-- package.json | 23 +- tsconfig.json | 9 +- 75 files changed, 2111 insertions(+), 89 deletions(-) create mode 100644 apps/auth/src/config/app.config.ts create mode 100644 apps/auth/src/config/jwt.config.ts create mode 100644 apps/auth/src/modules/authentication/constants/login.response.constant.ts create mode 100644 apps/auth/src/modules/authentication/controllers/index.ts create mode 100644 apps/auth/src/modules/authentication/controllers/user-auth.controller.ts create mode 100644 apps/auth/src/modules/authentication/dtos/index.ts create mode 100644 apps/auth/src/modules/authentication/dtos/user-auth.dto.ts create mode 100644 apps/auth/src/modules/authentication/dtos/user-login.dto.ts create mode 100644 apps/auth/src/modules/authentication/dtos/user-otp.dto.ts create mode 100644 apps/auth/src/modules/authentication/dtos/user-password.dto.ts create mode 100644 apps/auth/src/modules/authentication/services/index.ts create mode 100644 apps/auth/src/modules/authentication/services/user-auth.service.ts create mode 100644 apps/backend/src/config/app.config.ts create mode 100644 apps/backend/src/config/jwt.config.ts create mode 100644 apps/backend/src/modules/user/controllers/index.ts create mode 100644 apps/backend/src/modules/user/dtos/index.ts create mode 100644 apps/backend/src/modules/user/services/index.ts create mode 100644 libs/common/src/auth/auth.module.ts create mode 100644 libs/common/src/auth/interfaces/auth.interface.ts create mode 100644 libs/common/src/auth/services/auth.service.ts create mode 100644 libs/common/src/auth/strategies/jwt.strategy.ts create mode 100644 libs/common/src/common.module.ts create mode 100644 libs/common/src/common.service.spec.ts create mode 100644 libs/common/src/common.service.ts create mode 100644 libs/common/src/config/email.config.ts create mode 100644 libs/common/src/config/index.ts create mode 100644 libs/common/src/constants/otp-type.enum.ts create mode 100644 libs/common/src/database/database.module.ts create mode 100644 libs/common/src/database/strategies/index.ts create mode 100644 libs/common/src/database/strategies/snack-naming.strategy.ts create mode 100644 libs/common/src/guards/jwt.auth.guard.ts create mode 100644 libs/common/src/helper/helper.module.ts create mode 100644 libs/common/src/helper/services/helper.hash.service.ts create mode 100644 libs/common/src/helper/services/index.ts create mode 100644 libs/common/src/index.ts create mode 100644 libs/common/src/modules/abstract/dtos/abstract.dto.ts create mode 100644 libs/common/src/modules/abstract/dtos/index.ts create mode 100644 libs/common/src/modules/abstract/entities/abstract.entity.ts create mode 100644 libs/common/src/modules/session/dtos/session.dto.ts create mode 100644 libs/common/src/modules/session/entities/index.ts create mode 100644 libs/common/src/modules/session/entities/session.entity.ts create mode 100644 libs/common/src/modules/session/repositories/session.repository.ts create mode 100644 libs/common/src/modules/session/session.repository.module.ts create mode 100644 libs/common/src/modules/user-otp/dtos/index.ts create mode 100644 libs/common/src/modules/user-otp/dtos/user-otp.dto.ts create mode 100644 libs/common/src/modules/user-otp/entities/index.ts create mode 100644 libs/common/src/modules/user-otp/entities/user-otp.entity.ts create mode 100644 libs/common/src/modules/user-otp/repositories/index.ts create mode 100644 libs/common/src/modules/user-otp/repositories/user-otp.repository.ts create mode 100644 libs/common/src/modules/user-otp/user-otp.repository.module.ts create mode 100644 libs/common/src/modules/user/dtos/index.ts create mode 100644 libs/common/src/modules/user/dtos/user.dto.ts create mode 100644 libs/common/src/modules/user/entities/index.ts create mode 100644 libs/common/src/modules/user/entities/user.entity.ts create mode 100644 libs/common/src/modules/user/repositories/index.ts create mode 100644 libs/common/src/modules/user/repositories/user.repository.ts create mode 100644 libs/common/src/modules/user/user.repository.module.ts create mode 100644 libs/common/src/response/response.decorator.ts create mode 100644 libs/common/src/response/response.interceptor.ts create mode 100644 libs/common/src/util/email.service.ts create mode 100644 libs/common/src/util/types.ts create mode 100644 libs/common/src/util/user-auth.swagger.utils.ts create mode 100644 libs/common/tsconfig.lib.json diff --git a/apps/auth/src/config/app.config.ts b/apps/auth/src/config/app.config.ts new file mode 100644 index 0000000..ef1ad58 --- /dev/null +++ b/apps/auth/src/config/app.config.ts @@ -0,0 +1,8 @@ +export default () => ({ + DB_HOST: process.env.DB_HOST, + DB_PORT: process.env.DB_PORT, + DB_USER: process.env.DB_USER, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_NAME: process.env.DB_NAME, + DB_SYNC: process.env.DB_SYNC, +}); diff --git a/apps/auth/src/config/index.ts b/apps/auth/src/config/index.ts index adf5667..f1ccc51 100644 --- a/apps/auth/src/config/index.ts +++ b/apps/auth/src/config/index.ts @@ -1,3 +1,4 @@ import AuthConfig from './auth.config'; +import AppConfig from './app.config' -export default [AuthConfig]; +export default [AuthConfig,AppConfig]; diff --git a/apps/auth/src/config/jwt.config.ts b/apps/auth/src/config/jwt.config.ts new file mode 100644 index 0000000..17a1e92 --- /dev/null +++ b/apps/auth/src/config/jwt.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'jwt', + (): Record => ({ + secret: process.env.JWT_SECRET, + expire_time: process.env.JWT_EXPIRE_TIME, + secret_refresh: process.env.JWT_SECRET_REFRESH, + expire_refresh: process.env.JWT_EXPIRE_TIME_REFRESH, + }) +); \ No newline at end of file diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index 9572bdb..7d68bcc 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -1,8 +1,32 @@ import { NestFactory } from '@nestjs/core'; import { AuthModule } from './auth.module'; +import rateLimit from 'express-rate-limit'; +import helmet from 'helmet'; +import { setupSwaggerAuthentication } from '@app/common/util/user-auth.swagger.utils'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AuthModule); + + app.enableCors(); + + app.use( + rateLimit({ + windowMs: 5 * 60 * 1000, + max: 500, + }), + ); + + app.use( + helmet({ + contentSecurityPolicy: false, + }), + ); + + setupSwaggerAuthentication(app); + + app.useGlobalPipes(new ValidationPipe()); + await app.listen(6001); } bootstrap(); diff --git a/apps/auth/src/modules/authentication/authentication.module.ts b/apps/auth/src/modules/authentication/authentication.module.ts index eb5c84c..11069eb 100644 --- a/apps/auth/src/modules/authentication/authentication.module.ts +++ b/apps/auth/src/modules/authentication/authentication.module.ts @@ -2,11 +2,24 @@ import { Module } from '@nestjs/common'; import { AuthenticationController } from './controllers/authentication.controller'; import { AuthenticationService } from './services/authentication.service'; import { ConfigModule } from '@nestjs/config'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; +import { CommonModule } from '@app/common'; +import { UserAuthController } from './controllers'; +import { UserAuthService } from './services'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; +import { UserOtpRepository } from '@app/common/modules/user-otp/repositories/user-otp.repository'; @Module({ - imports: [ConfigModule], - controllers: [AuthenticationController], - providers: [AuthenticationService], - exports: [AuthenticationService], + imports: [ConfigModule, UserRepositoryModule, CommonModule], + controllers: [AuthenticationController, UserAuthController], + providers: [ + AuthenticationService, + UserAuthService, + UserRepository, + UserSessionRepository, + UserOtpRepository, + ], + exports: [AuthenticationService, UserAuthService], }) export class AuthenticationModule {} diff --git a/apps/auth/src/modules/authentication/constants/login.response.constant.ts b/apps/auth/src/modules/authentication/constants/login.response.constant.ts new file mode 100644 index 0000000..0be9517 --- /dev/null +++ b/apps/auth/src/modules/authentication/constants/login.response.constant.ts @@ -0,0 +1,3 @@ +export interface ILoginResponse { + access_token: string; +} diff --git a/apps/auth/src/modules/authentication/controllers/authentication.controller.ts b/apps/auth/src/modules/authentication/controllers/authentication.controller.ts index 931a522..070620b 100644 --- a/apps/auth/src/modules/authentication/controllers/authentication.controller.ts +++ b/apps/auth/src/modules/authentication/controllers/authentication.controller.ts @@ -1,10 +1,12 @@ import { Controller, Post } from '@nestjs/common'; import { AuthenticationService } from '../services/authentication.service'; +import { ApiTags } from '@nestjs/swagger'; @Controller({ version: '1', path: 'authentication', }) +@ApiTags('Tuya Auth') export class AuthenticationController { constructor(private readonly authenticationService: AuthenticationService) {} @Post('auth') diff --git a/apps/auth/src/modules/authentication/controllers/index.ts b/apps/auth/src/modules/authentication/controllers/index.ts new file mode 100644 index 0000000..2ce466d --- /dev/null +++ b/apps/auth/src/modules/authentication/controllers/index.ts @@ -0,0 +1,2 @@ +export * from './authentication.controller'; +export * from './user-auth.controller'; diff --git a/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts b/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts new file mode 100644 index 0000000..21f9f66 --- /dev/null +++ b/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts @@ -0,0 +1,95 @@ +import { + Body, + Controller, + Delete, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { UserAuthService } from '../services/user-auth.service'; +import { UserSignUpDto } from '../dtos/user-auth.dto'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ResponseMessage } from '@app/common/response/response.decorator'; +import { UserLoginDto } from '../dtos/user-login.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; + +@Controller({ + version: '1', + path: 'authentication', +}) +@ApiTags('Auth') +export class UserAuthController { + constructor(private readonly userAuthService: UserAuthService) {} + + @ResponseMessage('User Registered Successfully') + @Post('user/signup') + async signUp(@Body() userSignUpDto: UserSignUpDto) { + const signupUser = await this.userAuthService.signUp(userSignUpDto); + return { + statusCode: HttpStatus.CREATED, + data: { + id: signupUser.uuid, + }, + message: 'User Registered Successfully', + }; + } + + @ResponseMessage('user logged in successfully') + @Post('user/login') + async userLogin(@Body() data: UserLoginDto) { + const accessToken = await this.userAuthService.userLogin(data); + return { + statusCode: HttpStatus.CREATED, + data: accessToken, + message: 'User Loggedin Successfully', + }; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('user/delete/:id') + async userDelete(@Param('id') id: string) { + await this.userAuthService.deleteUser(id); + return { + statusCode: HttpStatus.OK, + data: { + id, + }, + message: 'User Deleted Successfully', + }; + } + + @Post('user/send-otp') + async sendOtp(@Body() otpDto: UserOtpDto) { + const otpCode = await this.userAuthService.generateOTP(otpDto); + return { + statusCode: HttpStatus.OK, + data: { + otp: otpCode, + }, + message: 'Otp Send Successfully', + }; + } + + @Post('user/verify-otp') + async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { + await this.userAuthService.verifyOTP(verifyOtpDto); + return { + statusCode: HttpStatus.OK, + data: {}, + message: 'Otp Verified Successfully', + }; + } + + @Post('user/forget-password') + async forgetPassword(@Body() forgetPasswordDto: ForgetPasswordDto) { + await this.userAuthService.forgetPassword(forgetPasswordDto); + return { + statusCode: HttpStatus.OK, + data: {}, + message: 'Password changed successfully', + }; + } +} diff --git a/apps/auth/src/modules/authentication/dtos/index.ts b/apps/auth/src/modules/authentication/dtos/index.ts new file mode 100644 index 0000000..069bcc1 --- /dev/null +++ b/apps/auth/src/modules/authentication/dtos/index.ts @@ -0,0 +1,4 @@ +export * from './user-auth.dto' +export * from './user-login.dto' +export * from './user-otp.dto' +export * from './user-password.dto' \ No newline at end of file diff --git a/apps/auth/src/modules/authentication/dtos/user-auth.dto.ts b/apps/auth/src/modules/authentication/dtos/user-auth.dto.ts new file mode 100644 index 0000000..8e77057 --- /dev/null +++ b/apps/auth/src/modules/authentication/dtos/user-auth.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class UserSignUpDto { + @ApiProperty({ + description:'email', + required:true + }) + @IsEmail() + @IsNotEmpty() + public email: string; + + @ApiProperty({ + description:'password', + required:true + }) + @IsString() + @IsNotEmpty() + public password: string; + + @ApiProperty({ + description:'first name', + required:true + }) + @IsString() + @IsNotEmpty() + public firstName: string; + + @ApiProperty({ + description:'last name', + required:true + }) + @IsString() + @IsNotEmpty() + public lastName: string; +} diff --git a/apps/auth/src/modules/authentication/dtos/user-login.dto.ts b/apps/auth/src/modules/authentication/dtos/user-login.dto.ts new file mode 100644 index 0000000..6a14047 --- /dev/null +++ b/apps/auth/src/modules/authentication/dtos/user-login.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class UserLoginDto { + @ApiProperty() + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty() + @IsString() + @IsOptional() + password: string; +} diff --git a/apps/auth/src/modules/authentication/dtos/user-otp.dto.ts b/apps/auth/src/modules/authentication/dtos/user-otp.dto.ts new file mode 100644 index 0000000..d4fad12 --- /dev/null +++ b/apps/auth/src/modules/authentication/dtos/user-otp.dto.ts @@ -0,0 +1,22 @@ +import { OtpType } from '@app/common/constants/otp-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class UserOtpDto { + @ApiProperty() + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty() + @IsEnum(OtpType) + @IsNotEmpty() + type: OtpType; +} + +export class VerifyOtpDto extends UserOtpDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + otpCode: string; +} diff --git a/apps/auth/src/modules/authentication/dtos/user-password.dto.ts b/apps/auth/src/modules/authentication/dtos/user-password.dto.ts new file mode 100644 index 0000000..fe2118c --- /dev/null +++ b/apps/auth/src/modules/authentication/dtos/user-password.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class ForgetPasswordDto { + @ApiProperty() + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + password: string; +} diff --git a/apps/auth/src/modules/authentication/services/index.ts b/apps/auth/src/modules/authentication/services/index.ts new file mode 100644 index 0000000..ac532d6 --- /dev/null +++ b/apps/auth/src/modules/authentication/services/index.ts @@ -0,0 +1,2 @@ +export * from './authentication.service'; +export * from './user-auth.service'; diff --git a/apps/auth/src/modules/authentication/services/user-auth.service.ts b/apps/auth/src/modules/authentication/services/user-auth.service.ts new file mode 100644 index 0000000..4f75152 --- /dev/null +++ b/apps/auth/src/modules/authentication/services/user-auth.service.ts @@ -0,0 +1,151 @@ +import { UserRepository } from '@app/common/modules/user/repositories'; +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { UserSignUpDto } from '../dtos/user-auth.dto'; +import { HelperHashService } from '@app/common/helper/services'; +import { UserLoginDto } from '../dtos/user-login.dto'; +import { AuthService } from '@app/common/auth/services/auth.service'; +import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; +import { UserOtpRepository } from '@app/common/modules/user-otp/repositories/user-otp.repository'; +import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; +import { EmailService } from '@app/common/util/email.service'; +import { OtpType } from '@app/common/constants/otp-type.enum'; +import { UserEntity } from '@app/common/modules/user/entities/user.entity'; +import { ILoginResponse } from '../constants/login.response.constant'; + +@Injectable() +export class UserAuthService { + constructor( + private readonly userRepository: UserRepository, + private readonly sessionRepository: UserSessionRepository, + private readonly otpRepository: UserOtpRepository, + private readonly helperHashService: HelperHashService, + private readonly authService: AuthService, + private readonly emailService: EmailService, + ) {} + + async signUp(userSignUpDto: UserSignUpDto): Promise { + const findUser = await this.findUser(userSignUpDto.email); + if (findUser) { + throw new BadRequestException('User already registered with given email'); + } + const salt = this.helperHashService.randomSalt(10); + const password = this.helperHashService.bcrypt( + userSignUpDto.password, + salt, + ); + return await this.userRepository.save({ ...userSignUpDto, password }); + } + + async findUser(email: string) { + return await this.userRepository.findOne({ + where: { + email, + }, + }); + } + + async forgetPassword(forgetPasswordDto: ForgetPasswordDto) { + const findUser = await this.findUser(forgetPasswordDto.email); + if (!findUser) { + throw new BadRequestException('User not found'); + } + const salt = this.helperHashService.randomSalt(10); + const password = this.helperHashService.bcrypt( + forgetPasswordDto.password, + salt, + ); + return await this.userRepository.update( + { uuid: findUser.uuid }, + { password }, + ); + } + + async userLogin(data: UserLoginDto): Promise { + const user = await this.authService.validateUser(data.email, data.password); + if (!user) { + throw new UnauthorizedException('Invalid login credentials.'); + } + + const session = await Promise.all([ + await this.sessionRepository.update( + { userId: user.id }, + { + isLoggedOut: true, + }, + ), + await this.authService.createSession({ + userId: user.uuid, + loginTime: new Date(), + isLoggedOut: false, + }), + ]); + + return await this.authService.login({ + email: user.email, + userId: user.id, + uuid: user.uuid, + sessionId: session[1].uuid, + }); + } + + async deleteUser(uuid: string) { + const user = await this.findOneById(uuid); + if (!user) { + throw new BadRequestException('User does not found'); + } + return await this.userRepository.delete({ uuid }); + } + + async findOneById(id: string): Promise { + return await this.userRepository.findOne({ where: { uuid: id } }); + } + + async generateOTP(data: UserOtpDto): Promise { + await this.otpRepository.delete({ email: data.email, type: data.type }); + const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); + const expiryTime = new Date(); + expiryTime.setMinutes(expiryTime.getMinutes() + 1); + await this.otpRepository.save({ + email: data.email, + otpCode, + expiryTime, + type: data.type, + }); + const subject = 'OTP send successfully'; + const message = `Your OTP code is ${otpCode}`; + this.emailService.sendOTPEmail(data.email, subject, message); + return otpCode; + } + + async verifyOTP(data: VerifyOtpDto): Promise { + const otp = await this.otpRepository.findOne({ + where: { email: data.email, type: data.type }, + }); + + if (!otp) { + throw new BadRequestException('this email is not registered'); + } + + if (otp.otpCode !== data.otpCode) { + throw new BadRequestException('You entered wrong otp'); + } + + if (otp.expiryTime < new Date()) { + await this.otpRepository.delete(otp.id); + throw new BadRequestException('OTP expired'); + } + + if (data.type == OtpType.VERIFICATION) { + await this.userRepository.update( + { email: data.email }, + { isUserVerified: true }, + ); + } + + return true; + } +} diff --git a/apps/backend/src/backend.module.ts b/apps/backend/src/backend.module.ts index c41a9b8..9afbd2f 100644 --- a/apps/backend/src/backend.module.ts +++ b/apps/backend/src/backend.module.ts @@ -4,6 +4,7 @@ import { AppService } from './backend.service'; import { UserModule } from './modules/user/user.module'; import { ConfigModule } from '@nestjs/config'; import config from './config'; +import { CommonModule } from '@app/common'; @Module({ imports: [ @@ -11,6 +12,7 @@ import config from './config'; load: config, }), UserModule, + CommonModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts new file mode 100644 index 0000000..ef1ad58 --- /dev/null +++ b/apps/backend/src/config/app.config.ts @@ -0,0 +1,8 @@ +export default () => ({ + DB_HOST: process.env.DB_HOST, + DB_PORT: process.env.DB_PORT, + DB_USER: process.env.DB_USER, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_NAME: process.env.DB_NAME, + DB_SYNC: process.env.DB_SYNC, +}); diff --git a/apps/backend/src/config/index.ts b/apps/backend/src/config/index.ts index adf5667..f8720ad 100644 --- a/apps/backend/src/config/index.ts +++ b/apps/backend/src/config/index.ts @@ -1,3 +1,4 @@ import AuthConfig from './auth.config'; - -export default [AuthConfig]; +import AppConfig from './app.config' +import JwtConfig from './jwt.config' +export default [AuthConfig,AppConfig,JwtConfig]; diff --git a/apps/backend/src/config/jwt.config.ts b/apps/backend/src/config/jwt.config.ts new file mode 100644 index 0000000..a4ff896 --- /dev/null +++ b/apps/backend/src/config/jwt.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'jwt', + (): Record => ({ + secret: process.env.JWT_SECRET, + expire_time: process.env.JWT_EXPIRE_TIME, + secret_refresh: process.env.JWT_SECRET_REFRESH, + expire_refresh: process.env.JWT_EXPIRE_TIME_REFRESH, + }) +); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 9343e74..c062492 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,8 +1,26 @@ import { NestFactory } from '@nestjs/core'; import { BackendModule } from './backend.module'; +import rateLimit from 'express-rate-limit'; +import helmet from 'helmet'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(BackendModule); + app.enableCors(); + + app.use( + rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + max: 500, // limit each IP to 500 requests per windowMs + }), + ); + + app.use( + helmet({ + contentSecurityPolicy: false, + }), + ); + app.useGlobalPipes(new ValidationPipe()); await app.listen(6000); } bootstrap(); diff --git a/apps/backend/src/modules/user/controllers/index.ts b/apps/backend/src/modules/user/controllers/index.ts new file mode 100644 index 0000000..bb9c2b0 --- /dev/null +++ b/apps/backend/src/modules/user/controllers/index.ts @@ -0,0 +1 @@ +export * from './user.controller' \ No newline at end of file diff --git a/apps/backend/src/modules/user/controllers/user.controller.ts b/apps/backend/src/modules/user/controllers/user.controller.ts index 901506a..705a748 100644 --- a/apps/backend/src/modules/user/controllers/user.controller.ts +++ b/apps/backend/src/modules/user/controllers/user.controller.ts @@ -1,8 +1,9 @@ import { Controller, Get, Query } from '@nestjs/common'; import { UserService } from '../services/user.service'; import { UserListDto } from '../dtos/user.list.dto'; +import { ApiTags } from '@nestjs/swagger'; -//@ApiTags('User Module') +@ApiTags('User Module') @Controller({ version: '1', path: 'user', diff --git a/apps/backend/src/modules/user/dtos/index.ts b/apps/backend/src/modules/user/dtos/index.ts new file mode 100644 index 0000000..ad0550f --- /dev/null +++ b/apps/backend/src/modules/user/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.list.dto' \ No newline at end of file diff --git a/apps/backend/src/modules/user/services/index.ts b/apps/backend/src/modules/user/services/index.ts new file mode 100644 index 0000000..5a6a9db --- /dev/null +++ b/apps/backend/src/modules/user/services/index.ts @@ -0,0 +1 @@ +export * from './user.service' \ No newline at end of file diff --git a/libs/common/src/auth/auth.module.ts b/libs/common/src/auth/auth.module.ts new file mode 100644 index 0000000..64de838 --- /dev/null +++ b/libs/common/src/auth/auth.module.ts @@ -0,0 +1,28 @@ +import { PassportModule } from '@nestjs/passport'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Module } from '@nestjs/common'; +import { HelperModule } from '../helper/helper.module'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { UserSessionRepository } from '../modules/session/repositories/session.repository'; +import { AuthService } from './services/auth.service'; +import { UserRepository } from '../modules/user/repositories'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { expiresIn: configService.get('JWT_EXPIRE_TIME') }, + }), + }), + HelperModule, + ], + providers: [JwtStrategy, UserSessionRepository,AuthService,UserRepository], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/libs/common/src/auth/interfaces/auth.interface.ts b/libs/common/src/auth/interfaces/auth.interface.ts new file mode 100644 index 0000000..ae85fe1 --- /dev/null +++ b/libs/common/src/auth/interfaces/auth.interface.ts @@ -0,0 +1,7 @@ +export class AuthInterface { + email: string; + userId: number; + uuid: string; + sessionId: string; + id: number; +} diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts new file mode 100644 index 0000000..2d3c0ee --- /dev/null +++ b/libs/common/src/auth/services/auth.service.ts @@ -0,0 +1,55 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { HelperHashService } from '../../helper/services'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; +import { UserSessionEntity } from '@app/common/modules/session/entities'; + +@Injectable() +export class AuthService { + constructor( + private jwtService: JwtService, + private readonly userRepository: UserRepository, + private readonly sessionRepository: UserSessionRepository, + private readonly helperHashService: HelperHashService, + ) {} + + async validateUser(email: string, pass: string): Promise { + const user = await this.userRepository.findOne({ + where: { + email, + }, + }); + if (!user.isUserVerified) { + throw new BadRequestException('User is not verified'); + } + if (user) { + const passwordMatch = this.helperHashService.bcryptCompare( + pass, + user.password, + ); + if (passwordMatch) { + const { ...result } = user; + return result; + } + } + return null; + } + + async createSession(data): Promise { + return await this.sessionRepository.save(data); + } + + async login(user: any) { + const payload = { + email: user.email, + userId: user.userId, + uuid: user.uuid, + type: user.type, + sessionId: user.sessionId, + }; + return { + access_token: this.jwtService.sign(payload), + }; + } +} diff --git a/libs/common/src/auth/strategies/jwt.strategy.ts b/libs/common/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..833f42c --- /dev/null +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,39 @@ +import { ConfigService } from '@nestjs/config'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; +import { AuthInterface } from '../interfaces/auth.interface'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly sessionRepository: UserSessionRepository, + private readonly configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + }); + } + + async validate(payload: AuthInterface) { + const validateUser = await this.sessionRepository.findOne({ + where: { + uuid: payload.sessionId, + isLoggedOut: false, + }, + }); + if (validateUser) { + return { + email: payload.email, + userId: payload.id, + uuid: payload.uuid, + sessionId: payload.sessionId, + }; + } else { + throw new BadRequestException('Unauthorized'); + } + } +} diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts new file mode 100644 index 0000000..62ea3a4 --- /dev/null +++ b/libs/common/src/common.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { CommonService } from './common.service'; +import { DatabaseModule } from './database/database.module'; +import { HelperModule } from './helper/helper.module'; +import { AuthModule } from './auth/auth.module'; +import { ConfigModule } from '@nestjs/config'; +import config from './config'; +import { EmailService } from './util/email.service'; + +@Module({ + providers: [CommonService,EmailService], + exports: [CommonService, HelperModule, AuthModule,EmailService], + imports: [ + ConfigModule.forRoot({ + load: config, + }), + DatabaseModule, + HelperModule, + AuthModule, + ], +}) +export class CommonModule {} diff --git a/libs/common/src/common.service.spec.ts b/libs/common/src/common.service.spec.ts new file mode 100644 index 0000000..6ea5a77 --- /dev/null +++ b/libs/common/src/common.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CommonService } from './common.service'; + +describe('CommonService', () => { + let service: CommonService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CommonService], + }).compile(); + + service = module.get(CommonService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts new file mode 100644 index 0000000..f0369a5 --- /dev/null +++ b/libs/common/src/common.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class CommonService {} diff --git a/libs/common/src/config/email.config.ts b/libs/common/src/config/email.config.ts new file mode 100644 index 0000000..4050433 --- /dev/null +++ b/libs/common/src/config/email.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'email-config', + (): Record => ({ + EMAIL_ID: process.env.EMAIL_USER, + PASSWORD: process.env.EMAIL_PASSWORD, + }), +); diff --git a/libs/common/src/config/index.ts b/libs/common/src/config/index.ts new file mode 100644 index 0000000..94380ce --- /dev/null +++ b/libs/common/src/config/index.ts @@ -0,0 +1,2 @@ +import emailConfig from './email.config'; +export default [emailConfig]; diff --git a/libs/common/src/constants/otp-type.enum.ts b/libs/common/src/constants/otp-type.enum.ts new file mode 100644 index 0000000..918dd63 --- /dev/null +++ b/libs/common/src/constants/otp-type.enum.ts @@ -0,0 +1,4 @@ +export enum OtpType { + VERIFICATION = 'VERIFICATION', + PASSWORD = 'PASSWORD', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts new file mode 100644 index 0000000..0302f0f --- /dev/null +++ b/libs/common/src/database/database.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SnakeNamingStrategy } from './strategies'; +import { UserEntity } from '../modules/user/entities/user.entity'; +import { UserSessionEntity } from '../modules/session/entities/session.entity'; +import { UserOtpEntity } from '../modules/user-otp/entities'; + +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + name: 'default', + type: 'postgres', + host: configService.get('DB_HOST'), + port: configService.get('DB_PORT'), + username: configService.get('DB_USER'), + password: configService.get('DB_PASSWORD'), + database: configService.get('DB_NAME'), + entities: [UserEntity, UserSessionEntity,UserOtpEntity], + namingStrategy: new SnakeNamingStrategy(), + synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), + logging: true, + extra: { + charset: 'utf8mb4', + max: 20, // set pool max size + idleTimeoutMillis: 5000, // close idle clients after 5 second + connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established + maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) + }, + continuationLocalStorage: true, + }), + }), + ], +}) +export class DatabaseModule {} diff --git a/libs/common/src/database/strategies/index.ts b/libs/common/src/database/strategies/index.ts new file mode 100644 index 0000000..0dabf18 --- /dev/null +++ b/libs/common/src/database/strategies/index.ts @@ -0,0 +1 @@ +export * from './snack-naming.strategy'; diff --git a/libs/common/src/database/strategies/snack-naming.strategy.ts b/libs/common/src/database/strategies/snack-naming.strategy.ts new file mode 100644 index 0000000..fed7e37 --- /dev/null +++ b/libs/common/src/database/strategies/snack-naming.strategy.ts @@ -0,0 +1,62 @@ +import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm'; +import { snakeCase } from 'typeorm/util/StringUtils'; + +export class SnakeNamingStrategy + extends DefaultNamingStrategy + implements NamingStrategyInterface +{ + tableName(className: string, customName: string): string { + return customName ? customName : snakeCase(className); + } + + columnName( + propertyName: string, + customName: string, + embeddedPrefixes: string[], + ): string { + return ( + snakeCase(embeddedPrefixes.join('_')) + + (customName ? customName : snakeCase(propertyName)) + ); + } + + relationName(propertyName: string): string { + return snakeCase(propertyName); + } + + joinColumnName(relationName: string, referencedColumnName: string): string { + return snakeCase(relationName + '_' + referencedColumnName); + } + + joinTableName( + firstTableName: string, + secondTableName: string, + firstPropertyName: any, + _secondPropertyName: string, + ): string { + return snakeCase( + firstTableName + + '_' + + firstPropertyName.replaceAll(/\./gi, '_') + + '_' + + secondTableName, + ); + } + + joinTableColumnName( + tableName: string, + propertyName: string, + columnName?: string, + ): string { + return snakeCase( + tableName + '_' + (columnName ? columnName : propertyName), + ); + } + + classTableInheritanceParentColumnName( + parentTableName: string, + parentTableIdPropertyName: string, + ): string { + return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`); + } +} diff --git a/libs/common/src/guards/jwt.auth.guard.ts b/libs/common/src/guards/jwt.auth.guard.ts new file mode 100644 index 0000000..1b57b77 --- /dev/null +++ b/libs/common/src/guards/jwt.auth.guard.ts @@ -0,0 +1,11 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +export class JwtAuthGuard extends AuthGuard('jwt') { + handleRequest(err, user, info) { + if (err || !user) { + throw err || new UnauthorizedException(); + } + return user; + } +} diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts new file mode 100644 index 0000000..826883a --- /dev/null +++ b/libs/common/src/helper/helper.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { HelperHashService } from './services'; + +@Global() +@Module({ + providers: [HelperHashService], + exports: [HelperHashService], + controllers: [], + imports: [], +}) +export class HelperModule {} diff --git a/libs/common/src/helper/services/helper.hash.service.ts b/libs/common/src/helper/services/helper.hash.service.ts new file mode 100644 index 0000000..b4406b9 --- /dev/null +++ b/libs/common/src/helper/services/helper.hash.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { compareSync, genSaltSync, hashSync } from 'bcryptjs'; +import { AES, enc, mode, pad, SHA256 } from 'crypto-js'; + +@Injectable() +export class HelperHashService { + randomSalt(length: number): string { + return genSaltSync(length); + } + + bcrypt(passwordString: string, salt: string): string { + return hashSync(passwordString, salt); + } + + bcryptCompare(passwordString: string, passwordHashed: string): boolean { + return compareSync(passwordString, passwordHashed); + } + + sha256(string: string): string { + return SHA256(string).toString(enc.Hex); + } + + sha256Compare(hashOne: string, hashTwo: string): boolean { + return hashOne === hashTwo; + } + + // Encryption function + encryptPassword(password, secretKey) { + return AES.encrypt('trx8g6gi', secretKey).toString(); + } + + // Decryption function + decryptPassword(encryptedPassword, secretKey) { + const bytes = AES.decrypt(encryptedPassword, secretKey); + return bytes.toString(enc.Utf8); + } + + aes256Encrypt( + data: string | Record | Record[], + key: string, + iv: string, + ): string { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.encrypt(JSON.stringify(data), enc.Utf8.parse(key), { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return cipher.toString(); + } + + aes256Decrypt(encrypted: string, key: string, iv: string) { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.decrypt(encrypted, enc.Utf8.parse(key), { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return cipher.toString(enc.Utf8); + } +} diff --git a/libs/common/src/helper/services/index.ts b/libs/common/src/helper/services/index.ts new file mode 100644 index 0000000..f3f4682 --- /dev/null +++ b/libs/common/src/helper/services/index.ts @@ -0,0 +1 @@ +export * from './helper.hash.service' \ No newline at end of file diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts new file mode 100644 index 0000000..10e9ae0 --- /dev/null +++ b/libs/common/src/index.ts @@ -0,0 +1,2 @@ +export * from './common.module'; +export * from './common.service'; diff --git a/libs/common/src/modules/abstract/dtos/abstract.dto.ts b/libs/common/src/modules/abstract/dtos/abstract.dto.ts new file mode 100644 index 0000000..c6934b0 --- /dev/null +++ b/libs/common/src/modules/abstract/dtos/abstract.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AbstractEntity } from '../entities/abstract.entity'; + +export class AbstractDto { + @ApiProperty({ format: 'uuid' }) + readonly uuid: string; + + constructor(abstract: AbstractEntity, options?: { excludeFields?: boolean }) { + if (!options?.excludeFields) { + this.uuid = abstract.uuid; + } + } +} diff --git a/libs/common/src/modules/abstract/dtos/index.ts b/libs/common/src/modules/abstract/dtos/index.ts new file mode 100644 index 0000000..fd297f8 --- /dev/null +++ b/libs/common/src/modules/abstract/dtos/index.ts @@ -0,0 +1 @@ +export * from './abstract.dto' \ No newline at end of file diff --git a/libs/common/src/modules/abstract/entities/abstract.entity.ts b/libs/common/src/modules/abstract/entities/abstract.entity.ts new file mode 100644 index 0000000..c065c9b --- /dev/null +++ b/libs/common/src/modules/abstract/entities/abstract.entity.ts @@ -0,0 +1,47 @@ +import { Exclude } from 'class-transformer'; +import { + Column, + CreateDateColumn, + Generated, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +import { AbstractDto } from '../dtos'; +import { Constructor } from '@app/common/util/types'; + +export abstract class AbstractEntity< + T extends AbstractDto = AbstractDto, + O = never +> { + @PrimaryGeneratedColumn('increment') + @Exclude() + public id: number; + + @Column() + @Generated('uuid') + public uuid: string; + + @CreateDateColumn({ type: 'timestamp' }) + @Exclude() + public createdAt: Date; + + @UpdateDateColumn({ type: 'timestamp' }) + @Exclude() + public updatedAt: Date; + + private dtoClass: Constructor; + + toDto(options?: O): T { + const dtoClass = this.dtoClass; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!dtoClass) { + throw new Error( + `You need to use @UseDto on class (${this.constructor.name}) be able to call toDto function` + ); + } + + return new this.dtoClass(this, options); + } +} diff --git a/libs/common/src/modules/session/dtos/session.dto.ts b/libs/common/src/modules/session/dtos/session.dto.ts new file mode 100644 index 0000000..7005249 --- /dev/null +++ b/libs/common/src/modules/session/dtos/session.dto.ts @@ -0,0 +1,26 @@ +import { + IsBoolean, + IsDate, + IsNotEmpty, + IsNumber, + IsString, +} from 'class-validator'; + +export class SessionDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsNumber() + @IsNotEmpty() + userId: number; + + @IsDate() + @IsNotEmpty() + public loginTime: Date; + + @IsBoolean() + @IsNotEmpty() + public isLoggedOut: boolean; + +} diff --git a/libs/common/src/modules/session/entities/index.ts b/libs/common/src/modules/session/entities/index.ts new file mode 100644 index 0000000..737110c --- /dev/null +++ b/libs/common/src/modules/session/entities/index.ts @@ -0,0 +1 @@ +export * from './session.entity' \ No newline at end of file diff --git a/libs/common/src/modules/session/entities/session.entity.ts b/libs/common/src/modules/session/entities/session.entity.ts new file mode 100644 index 0000000..e536763 --- /dev/null +++ b/libs/common/src/modules/session/entities/session.entity.ts @@ -0,0 +1,31 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SessionDto } from '../dtos/session.dto'; + +@Entity({ name: 'userSession' }) +export class UserSessionEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + userId: string; + + @Column({ + nullable: false, + }) + public loginTime: Date; + + @Column({ + nullable: false, + }) + public isLoggedOut: boolean; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/session/repositories/session.repository.ts b/libs/common/src/modules/session/repositories/session.repository.ts new file mode 100644 index 0000000..cf1ce64 --- /dev/null +++ b/libs/common/src/modules/session/repositories/session.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserSessionEntity } from '../entities'; + +@Injectable() +export class UserSessionRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserSessionEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/session/session.repository.module.ts b/libs/common/src/modules/session/session.repository.module.ts new file mode 100644 index 0000000..d963533 --- /dev/null +++ b/libs/common/src/modules/session/session.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserSessionEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserSessionEntity])], +}) +export class UserSessionRepositoryModule {} diff --git a/libs/common/src/modules/user-otp/dtos/index.ts b/libs/common/src/modules/user-otp/dtos/index.ts new file mode 100644 index 0000000..2848db5 --- /dev/null +++ b/libs/common/src/modules/user-otp/dtos/index.ts @@ -0,0 +1 @@ +export * from './user-otp.dto' \ No newline at end of file diff --git a/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts b/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts new file mode 100644 index 0000000..e98c700 --- /dev/null +++ b/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserOtpDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public otpCode: string; + + @IsString() + @IsNotEmpty() + public expiryTime: string; +} diff --git a/libs/common/src/modules/user-otp/entities/index.ts b/libs/common/src/modules/user-otp/entities/index.ts new file mode 100644 index 0000000..f1c339e --- /dev/null +++ b/libs/common/src/modules/user-otp/entities/index.ts @@ -0,0 +1 @@ +export * from './user-otp.entity' \ No newline at end of file diff --git a/libs/common/src/modules/user-otp/entities/user-otp.entity.ts b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts new file mode 100644 index 0000000..207ef8c --- /dev/null +++ b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserOtpDto } from '../dtos'; +import { OtpType } from '@app/common/constants/otp-type.enum'; + +@Entity({ name: 'user-otp' }) +export class UserOtpEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public uuid: string; + + @Column({ nullable: false }) + email: string; + + @Column({ nullable: false }) + otpCode: string; + + @Column({ nullable: false }) + expiryTime: Date; + + @Column({ + type: 'enum', + enum: Object.values(OtpType), + }) + type: OtpType; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user-otp/repositories/index.ts b/libs/common/src/modules/user-otp/repositories/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/common/src/modules/user-otp/repositories/user-otp.repository.ts b/libs/common/src/modules/user-otp/repositories/user-otp.repository.ts new file mode 100644 index 0000000..75cff43 --- /dev/null +++ b/libs/common/src/modules/user-otp/repositories/user-otp.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserOtpEntity } from '../entities'; + +@Injectable() +export class UserOtpRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserOtpEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user-otp/user-otp.repository.module.ts b/libs/common/src/modules/user-otp/user-otp.repository.module.ts new file mode 100644 index 0000000..9286d8b --- /dev/null +++ b/libs/common/src/modules/user-otp/user-otp.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserOtpEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserOtpEntity])], +}) +export class UserOtpRepositoryModule {} diff --git a/libs/common/src/modules/user/dtos/index.ts b/libs/common/src/modules/user/dtos/index.ts new file mode 100644 index 0000000..04e381d --- /dev/null +++ b/libs/common/src/modules/user/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.dto'; diff --git a/libs/common/src/modules/user/dtos/user.dto.ts b/libs/common/src/modules/user/dtos/user.dto.ts new file mode 100644 index 0000000..706ca31 --- /dev/null +++ b/libs/common/src/modules/user/dtos/user.dto.ts @@ -0,0 +1,23 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsString() + @IsNotEmpty() + public firstName: string; + + @IsString() + @IsNotEmpty() + public lastName: string; +} diff --git a/libs/common/src/modules/user/entities/index.ts b/libs/common/src/modules/user/entities/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts new file mode 100644 index 0000000..3fd6748 --- /dev/null +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -0,0 +1,41 @@ +import { Column, Entity } from 'typeorm'; +import { UserDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; + +@Entity({ name: 'user' }) +export class UserEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + unique: true, + }) + email: string; + + @Column({ + nullable: false, + }) + public password: string; + + @Column() + public firstName: string; + + @Column({ + nullable: false, + }) + public lastName: string; + + @Column({ + nullable: true, + default: false, + }) + public isUserVerified: boolean; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user/repositories/index.ts b/libs/common/src/modules/user/repositories/index.ts new file mode 100644 index 0000000..9fb5d34 --- /dev/null +++ b/libs/common/src/modules/user/repositories/index.ts @@ -0,0 +1 @@ +export * from './user.repository'; diff --git a/libs/common/src/modules/user/repositories/user.repository.ts b/libs/common/src/modules/user/repositories/user.repository.ts new file mode 100644 index 0000000..f83f14f --- /dev/null +++ b/libs/common/src/modules/user/repositories/user.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserEntity } from '../entities/user.entity'; + +@Injectable() +export class UserRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user/user.repository.module.ts b/libs/common/src/modules/user/user.repository.module.ts new file mode 100644 index 0000000..1b40c6a --- /dev/null +++ b/libs/common/src/modules/user/user.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserEntity } from './entities/user.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserEntity])], +}) +export class UserRepositoryModule {} diff --git a/libs/common/src/response/response.decorator.ts b/libs/common/src/response/response.decorator.ts new file mode 100644 index 0000000..407f4e9 --- /dev/null +++ b/libs/common/src/response/response.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ResponseMessage = (message: string) => + SetMetadata('response_message', message); \ No newline at end of file diff --git a/libs/common/src/response/response.interceptor.ts b/libs/common/src/response/response.interceptor.ts new file mode 100644 index 0000000..7186e9c --- /dev/null +++ b/libs/common/src/response/response.interceptor.ts @@ -0,0 +1,5 @@ +export interface Response { + statusCode: number; + message: string; + data?: T; + } \ No newline at end of file diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts new file mode 100644 index 0000000..f181de8 --- /dev/null +++ b/libs/common/src/util/email.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; + +@Injectable() +export class EmailService { + private user: string; + private pass: string; + + constructor(private readonly configService: ConfigService) { + this.user = this.configService.get('email-config.EMAIL_ID'); + this.pass = this.configService.get('email-config.PASSWORD'); + } + + async sendOTPEmail( + email: string, + subject: string, + message: string, + ): Promise { + + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: this.user, + pass: this.pass, + }, + }); + + const mailOptions = { + from: this.user, + to: email, + subject, + text: message, + }; + + await transporter.sendMail(mailOptions); + } +} diff --git a/libs/common/src/util/types.ts b/libs/common/src/util/types.ts new file mode 100644 index 0000000..c04b857 --- /dev/null +++ b/libs/common/src/util/types.ts @@ -0,0 +1,46 @@ +export type Constructor = new ( + ...arguments_: Arguments + ) => T; + + export type Plain = T; + export type Optional = T | undefined; + export type Nullable = T | null; + + export type PathImpl = Key extends string + ? T[Key] extends Record + ? + | `${Key}.${PathImpl> & + string}` + | `${Key}.${Exclude & string}` + : never + : never; + + export type PathImpl2 = PathImpl | keyof T; + + export type Path = keyof T extends string + ? PathImpl2 extends string | keyof T + ? PathImpl2 + : keyof T + : never; + + export type PathValue< + T, + P extends Path + > = P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends Path + ? PathValue + : never + : never + : P extends keyof T + ? T[P] + : never; + + export type KeyOfType = { + [P in keyof Required]: Required[P] extends U + ? P + : Required[P] extends U[] + ? P + : never; + }[keyof Entity]; + \ No newline at end of file diff --git a/libs/common/src/util/user-auth.swagger.utils.ts b/libs/common/src/util/user-auth.swagger.utils.ts new file mode 100644 index 0000000..faf1798 --- /dev/null +++ b/libs/common/src/util/user-auth.swagger.utils.ts @@ -0,0 +1,21 @@ +import type { INestApplication } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export function setupSwaggerAuthentication(app: INestApplication): void { + const options = new DocumentBuilder() + .setTitle('Authentication-Service') + .addBearerAuth({ + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + in: 'header', + }) + .build(); + + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup('api/authentication/documentation', app, document, { + swaggerOptions: { + persistAuthorization: true, + }, + }); +} diff --git a/libs/common/tsconfig.lib.json b/libs/common/tsconfig.lib.json new file mode 100644 index 0000000..8fdbf52 --- /dev/null +++ b/libs/common/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/common" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json index 61952b0..e6cd962 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -27,6 +27,15 @@ "compilerOptions": { "tsConfigPath": "apps/auth/tsconfig.app.json" } + }, + "common": { + "type": "library", + "root": "libs/common", + "entryFile": "index", + "sourceRoot": "libs/common/src", + "compilerOptions": { + "tsConfigPath": "libs/common/tsconfig.lib.json" + } } } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 591269f..4ee53d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,27 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.0", + "@nestjs/typeorm": "^10.0.2", "@tuya/tuya-connector-nodejs": "^2.1.2", "axios": "^1.6.7", + "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "crypto-js": "^4.2.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", "ioredis": "^5.3.2", + "morgan": "^1.10.0", + "nodemailer": "^6.9.10", + "passport-jwt": "^4.0.1", + "pg": "^8.11.3", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -902,7 +915,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -914,7 +927,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1084,7 +1097,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1101,7 +1113,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -1113,7 +1124,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -1124,14 +1134,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1148,7 +1156,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1163,7 +1170,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1622,7 +1628,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -1650,7 +1656,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.22", @@ -1682,6 +1688,11 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -1810,6 +1821,46 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", @@ -1852,6 +1903,38 @@ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, + "node_modules/@nestjs/swagger": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", + "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "dependencies": { + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.11.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz", @@ -1879,6 +1962,21 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", + "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", + "dependencies": { + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1935,7 +2033,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1977,29 +2074,34 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@tuya/tuya-connector-nodejs": { "version": "2.1.2", @@ -2189,6 +2291,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2205,7 +2315,6 @@ "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2666,7 +2775,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -2696,7 +2805,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -2774,7 +2883,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2793,6 +2901,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2818,6 +2931,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -2827,13 +2948,12 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -2995,14 +3115,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -3018,6 +3136,27 @@ } ] }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3092,7 +3231,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3186,11 +3324,24 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3376,6 +3527,77 @@ "node": ">=8" } }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -3416,7 +3638,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3430,7 +3651,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3674,13 +3894,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3690,6 +3909,16 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3820,7 +4049,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -3880,8 +4109,15 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } }, "node_modules/ee-first": { "version": "1.1.1", @@ -3909,8 +4145,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -3960,7 +4195,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, "engines": { "node": ">=6" } @@ -4347,6 +4581,20 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", + "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -4673,7 +4921,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -4840,7 +5087,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -4888,7 +5134,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -5043,6 +5288,14 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -5052,6 +5305,14 @@ "node": ">=8" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5097,7 +5358,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -5290,7 +5550,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -5375,8 +5634,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -5465,7 +5723,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6159,7 +6416,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -6233,6 +6489,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6318,11 +6614,41 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6335,6 +6661,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6391,7 +6722,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -6518,7 +6849,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6541,7 +6871,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -6557,6 +6886,45 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6585,6 +6953,16 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6651,6 +7029,14 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.10.tgz", + "integrity": "sha512-qtoKfGFhvIFW5kLfrkw2R6Nm6Ur4LNUMykyqu6n9BRKJuyQrqEGwdXXUAbwWEKt33dlWUGXb7rzmJP/p4+O+CA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6699,6 +7085,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6811,6 +7205,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6841,6 +7240,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6849,6 +7266,41 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6871,7 +7323,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -6886,7 +7337,6 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -6902,7 +7352,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -6921,6 +7370,95 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7021,6 +7559,41 @@ "node": ">=4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7306,7 +7879,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7582,7 +8154,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7597,7 +8168,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7608,8 +8178,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -7696,11 +8265,22 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7712,7 +8292,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -7797,7 +8376,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -7848,6 +8426,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7926,7 +8512,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7941,7 +8526,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7955,7 +8539,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7968,7 +8551,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -8075,6 +8657,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -8258,6 +8845,25 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -8404,7 +9010,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8535,11 +9141,153 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typeorm": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "chalk": "^4.1.2", + "cli-highlight": "^2.1.11", + "dayjs": "^1.11.9", + "debug": "^4.3.4", + "dotenv": "^16.0.3", + "glob": "^10.3.10", + "mkdirp": "^2.1.3", + "reflect-metadata": "^0.2.1", + "sha.js": "^2.4.11", + "tslib": "^2.5.0", + "uuid": "^9.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0", + "mssql": "^9.1.1 || ^10.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8562,8 +9310,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universalify": { "version": "2.0.1", @@ -8650,7 +9397,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -8818,7 +9565,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8848,7 +9594,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8898,7 +9643,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -8913,7 +9657,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -8931,7 +9674,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -8940,7 +9682,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index 1ce8852..1cfc0a4 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,27 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.0", + "@nestjs/typeorm": "^10.0.2", "@tuya/tuya-connector-nodejs": "^2.1.2", "axios": "^1.6.7", + "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "crypto-js": "^4.2.0", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", "ioredis": "^5.3.2", + "morgan": "^1.10.0", + "nodemailer": "^6.9.10", + "passport-jwt": "^4.0.1", + "pg": "^8.11.3", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -74,7 +87,11 @@ "coverageDirectory": "./coverage", "testEnvironment": "node", "roots": [ - "/apps/" - ] + "/apps/", + "/libs/" + ], + "moduleNameMapper": { + "^@app/common(|/.*)$": "/libs/common/src/$1" + } } } diff --git a/tsconfig.json b/tsconfig.json index 0828aa1..4d730b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,13 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "paths": {} + "paths": { + "@app/common": [ + "libs/common/src" + ], + "@app/common/*": [ + "libs/common/src/*" + ] + } } } \ No newline at end of file From 1bfcbf09043d8edecb53043d6921ac5279d2b515 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 26 Feb 2024 14:13:58 +0300 Subject: [PATCH 018/259] use pgcrypto --- apps/auth/src/config/app.config.ts | 13 +++++++------ .../controllers/user-auth.controller.ts | 1 + apps/backend/src/config/app.config.ts | 13 +++++++------ libs/common/src/database/database.module.ts | 2 ++ .../src/modules/session/entities/session.entity.ts | 2 ++ .../modules/user-otp/entities/user-otp.entity.ts | 2 ++ .../common/src/modules/user/entities/user.entity.ts | 2 ++ 7 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/auth/src/config/app.config.ts b/apps/auth/src/config/app.config.ts index ef1ad58..0ea13da 100644 --- a/apps/auth/src/config/app.config.ts +++ b/apps/auth/src/config/app.config.ts @@ -1,8 +1,9 @@ export default () => ({ - DB_HOST: process.env.DB_HOST, - DB_PORT: process.env.DB_PORT, - DB_USER: process.env.DB_USER, - DB_PASSWORD: process.env.DB_PASSWORD, - DB_NAME: process.env.DB_NAME, - DB_SYNC: process.env.DB_SYNC, + DB_HOST: process.env.AZURE_POSTGRESQL_HOST, + DB_PORT: process.env.AZURE_POSTGRESQL_PORT, + DB_USER: process.env.AZURE_POSTGRESQL_USER, + DB_PASSWORD: process.env.AZURE_POSTGRESQL_PASSWORD, + DB_NAME: process.env.AZURE_POSTGRESQL_DATABASE, + DB_SYNC: process.env.AZURE_POSTGRESQL_SYNC, + DB_SSL: process.env.AZURE_POSTGRESQL_SSL, }); diff --git a/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts b/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts index 21f9f66..085dd2a 100644 --- a/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts +++ b/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts @@ -31,6 +31,7 @@ export class UserAuthController { statusCode: HttpStatus.CREATED, data: { id: signupUser.uuid, + default: () => 'gen_random_uuid()', // this is a default value for the uuid column }, message: 'User Registered Successfully', }; diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts index ef1ad58..0ea13da 100644 --- a/apps/backend/src/config/app.config.ts +++ b/apps/backend/src/config/app.config.ts @@ -1,8 +1,9 @@ export default () => ({ - DB_HOST: process.env.DB_HOST, - DB_PORT: process.env.DB_PORT, - DB_USER: process.env.DB_USER, - DB_PASSWORD: process.env.DB_PASSWORD, - DB_NAME: process.env.DB_NAME, - DB_SYNC: process.env.DB_SYNC, + DB_HOST: process.env.AZURE_POSTGRESQL_HOST, + DB_PORT: process.env.AZURE_POSTGRESQL_PORT, + DB_USER: process.env.AZURE_POSTGRESQL_USER, + DB_PASSWORD: process.env.AZURE_POSTGRESQL_PASSWORD, + DB_NAME: process.env.AZURE_POSTGRESQL_DATABASE, + DB_SYNC: process.env.AZURE_POSTGRESQL_SYNC, + DB_SSL: process.env.AZURE_POSTGRESQL_SSL, }); diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 0302f0f..a80d929 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -31,6 +31,8 @@ import { UserOtpEntity } from '../modules/user-otp/entities'; maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) }, continuationLocalStorage: true, + ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), + }), }), ], diff --git a/libs/common/src/modules/session/entities/session.entity.ts b/libs/common/src/modules/session/entities/session.entity.ts index e536763..a4518f2 100644 --- a/libs/common/src/modules/session/entities/session.entity.ts +++ b/libs/common/src/modules/session/entities/session.entity.ts @@ -5,6 +5,8 @@ import { SessionDto } from '../dtos/session.dto'; @Entity({ name: 'userSession' }) export class UserSessionEntity extends AbstractEntity { @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', nullable: false, }) public uuid: string; diff --git a/libs/common/src/modules/user-otp/entities/user-otp.entity.ts b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts index 207ef8c..6bd1540 100644 --- a/libs/common/src/modules/user-otp/entities/user-otp.entity.ts +++ b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts @@ -6,6 +6,8 @@ import { OtpType } from '@app/common/constants/otp-type.enum'; @Entity({ name: 'user-otp' }) export class UserOtpEntity extends AbstractEntity { @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', nullable: false, }) public uuid: string; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 3fd6748..c168c11 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -5,6 +5,8 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value nullable: false, }) public uuid: string; From 563278170c9c40134855e987ee3d48ebf7a6b5ad Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 27 Feb 2024 04:26:03 +0300 Subject: [PATCH 019/259] change to smtp mailer --- apps/auth/src/main.ts | 2 +- apps/backend/src/main.ts | 2 +- libs/common/src/config/email.config.ts | 7 +++++-- libs/common/src/util/email.service.ts | 25 ++++++++++++------------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index 7d68bcc..2303cb7 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -27,6 +27,6 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - await app.listen(6001); + await app.listen(7001); } bootstrap(); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index c062492..394a30f 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -21,6 +21,6 @@ async function bootstrap() { }), ); app.useGlobalPipes(new ValidationPipe()); - await app.listen(6000); + await app.listen(7000); } bootstrap(); diff --git a/libs/common/src/config/email.config.ts b/libs/common/src/config/email.config.ts index 4050433..199d084 100644 --- a/libs/common/src/config/email.config.ts +++ b/libs/common/src/config/email.config.ts @@ -3,7 +3,10 @@ import { registerAs } from '@nestjs/config'; export default registerAs( 'email-config', (): Record => ({ - EMAIL_ID: process.env.EMAIL_USER, - PASSWORD: process.env.EMAIL_PASSWORD, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: parseInt(process.env.SMTP_PORT), + SMTP_SECURE: process.env.SMTP_SECURE === 'true', + SMTP_USER: process.env.SMTP_USER, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, }), ); diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts index f181de8..8636dea 100644 --- a/libs/common/src/util/email.service.ts +++ b/libs/common/src/util/email.service.ts @@ -4,12 +4,18 @@ import * as nodemailer from 'nodemailer'; @Injectable() export class EmailService { - private user: string; - private pass: string; + private smtpConfig: any; constructor(private readonly configService: ConfigService) { - this.user = this.configService.get('email-config.EMAIL_ID'); - this.pass = this.configService.get('email-config.PASSWORD'); + this.smtpConfig = { + host: this.configService.get('email-config.SMTP_HOST'), + port: this.configService.get('email-config.SMTP_PORT'), + secure: this.configService.get('email-config.SMTP_SECURE'), // true for 465, false for other ports + auth: { + user: this.configService.get('email-config.SMTP_USER'), + pass: this.configService.get('email-config.SMTP_PASSWORD'), + }, + }; } async sendOTPEmail( @@ -17,17 +23,10 @@ export class EmailService { subject: string, message: string, ): Promise { - - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: this.user, - pass: this.pass, - }, - }); + const transporter = nodemailer.createTransport(this.smtpConfig); const mailOptions = { - from: this.user, + from: this.smtpConfig.auth.user, to: email, subject, text: message, From 619100a7c2769ca1541cacf6b8c9bf8cc03f9cb7 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 16:41:47 -0500 Subject: [PATCH 020/259] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_syncrow-dev.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/dev_syncrow-dev.yml diff --git a/.github/workflows/dev_syncrow-dev.yml b/.github/workflows/dev_syncrow-dev.yml new file mode 100644 index 0000000..134b4cd --- /dev/null +++ b/.github/workflows/dev_syncrow-dev.yml @@ -0,0 +1,71 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - syncrow-dev + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v3 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_F5089EEA95DF450E90E990B230B63FEA }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_6A9379B9B88748918EE02EE725C051AD }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_8041F0A416EB4B24ADE667B446A2BD0D }} + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'syncrow-dev' + slot-name: 'Production' + package: . + \ No newline at end of file From a596cce04cde4eec805bd394265e6a8e427a636e Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 19 Feb 2024 23:18:22 +0300 Subject: [PATCH 021/259] Create azure-webapps-node.yml --- .github/workflows/azure-webapps-node.yml | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/azure-webapps-node.yml diff --git a/.github/workflows/azure-webapps-node.yml b/.github/workflows/azure-webapps-node.yml new file mode 100644 index 0000000..c648b24 --- /dev/null +++ b/.github/workflows/azure-webapps-node.yml @@ -0,0 +1,59 @@ +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 }} From 5036120e2f9121fdf06df41cf1c6501482d435ab Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 16:56:41 -0500 Subject: [PATCH 022/259] Docerizing and proxying the microservices --- Dockerfile | 25 ++++++++++++++ apps/auth/src/main.ts | 1 + apps/backend/src/main.ts | 2 ++ bun.lockb | Bin 0 -> 286429 bytes nginx.conf | 29 ++++++++++++++++ package-lock.json | 70 +++++++++++++++++++++++++++++++++++++++ package.json | 6 +++- 7 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100755 bun.lockb create mode 100644 nginx.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f218aeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:20 as builder + +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build auth +RUN npm run build backend + +FROM nginx:alpine + +RUN rm /etc/nginx/conf.d/default.conf + +COPY nginx.conf /etc/nginx/conf.d + +COPY --from=builder /usr/src/app/dist/apps/auth /usr/share/nginx/html/auth +COPY --from=builder /usr/src/app/dist/apps/backend /usr/share/nginx/html/backend + +EXPOSE 443 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index 2303cb7..e7d4b53 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -29,4 +29,5 @@ async function bootstrap() { await app.listen(7001); } +console.log('Starting auth at port 7001...'); bootstrap(); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 394a30f..d97e1d4 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -23,4 +23,6 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); await app.listen(7000); } + +console.log('Starting backend at port 7000...'); bootstrap(); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..aafac7209b6f7d998a51ac765adaacfba39ac5c2 GIT binary patch literal 286429 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.9.0" + } + }, "node_modules/@babel/template": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", @@ -3777,6 +3790,48 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -7866,6 +7921,12 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -8296,6 +8357,15 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", diff --git a/package.json b/package.json index 1cfc0a4..c4d8f6a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,10 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" + "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json", + "start:auth": "nest start auth", + "start:backend": "nest start backend", + "start:all": "concurrently \"npm run start:auth\" \"npm run start:backend\"" }, "dependencies": { "@nestjs/common": "^10.0.0", @@ -57,6 +60,7 @@ "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "concurrently": "^8.2.2", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", From 4975be197dd46f20de24747d9bb8dac8ceed047e Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 01:36:33 +0300 Subject: [PATCH 023/259] working dockerized environment --- Dockerfile | 24 ++++++++++++++---------- bun.lockb | Bin 286429 -> 286429 bytes nginx.conf | 31 ++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index f218aeb..109471d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:20 as builder -WORKDIR /usr/src/app +WORKDIR /src/app COPY package*.json ./ @@ -8,18 +8,22 @@ RUN npm install COPY . . -RUN npm run build auth -RUN npm run build backend +RUN npm run build -FROM nginx:alpine +# Runtime stage +FROM node:20-alpine -RUN rm /etc/nginx/conf.d/default.conf +RUN apk add --no-cache nginx +COPY nginx.conf /etc/nginx/nginx.conf -COPY nginx.conf /etc/nginx/conf.d +WORKDIR /app -COPY --from=builder /usr/src/app/dist/apps/auth /usr/share/nginx/html/auth -COPY --from=builder /usr/src/app/dist/apps/backend /usr/share/nginx/html/backend +COPY --from=builder /src/app/dist/apps/auth ./auth +COPY --from=builder /src/app/dist/apps/backend ./backend +COPY package*.json ./ -EXPOSE 443 +RUN npm install -CMD ["nginx", "-g", "daemon off;"] +EXPOSE 80 + +CMD ["sh", "-c", "nginx -g 'daemon off;' & node auth/main.js & node backend/main.js"] diff --git a/bun.lockb b/bun.lockb index aafac7209b6f7d998a51ac765adaacfba39ac5c2..d8412183864ff8303a75e7d80bcbd80d3c59380a 100755 GIT binary patch delta 48375 zcmeFad7O@A|Nno@IVN){yJ0Zatiz0%VHVrPV1(>jL$<+S##m;vl+37vwBW>H%aW9o zB#CyC(n2Jqow}8#<<4D7`}g@e&+|fV-JkpOef++U-#=ZCUf#$1b-a)5eY}t5JYAPQ ze753;&sJQtzNm4C&ll(j>$b@i-*%)E@eOy3KZt8YVA#%?9D zDzZwL&sPRH)RXIxmCy_FS|zETF;gbzPsz#fjhT>j~SPfG2TF2 zNrCLrK3`enlSox)e;X26bTr&mCS^^^j2S!HcK}_+{gE=BG;wTJ-c+A&Qr6_GNm=;? zX&%3k8dS+r{wR-}Kp~B8s3-d%RZmd1MX7?Gm7kH9mod|qHF-j2URFU-ehV_>o&{8- z0;3D_M^BmQ^F2)K;#50$e!g*GY*jw9D8 z=JSoCq^doF#IpEBr2K``r%azreuS&fLOR_<`pJJ9JITj9`2|y~*&b2Njh69KraIm3 zfsRS>zo;C~G%L1=B^A8glfiJAlvyxg%GiA0+Za|P|M_xurBbm)t5GYUj+ruLVix1O z8(saKg;aYzh%WUjFJpSl=qY1oF0Sd$XH=AHH+p74riMBNdyOk&%$Urn1^K>8wOqR~ zQ}P^7Tw2fA%+ZD83GU}h`74DI@rseN)I&AJ3v1i&m8xBIxUSp8%*pV4rq*?bIu0rG zv03BBiBCsYv5Y(_wLKEGoXA z1nw>V%ICUtdgkb<8Dl2;IyH7U73+RjjDaV!n~}QY}`Dx zaPrJCQ^sb#O1^^PQDhC|1|;ELTm`9)JV3pgRBsC?-W%)ISCBC^V{}fYue=6Ugn>mu zvwDh=$wisdGAA>+zCGwyp??zRj_6&aJQ38lldlH1F=DxX9l9E}y>j0txFa9g(k-8v zKPDq5V{GQ<@M`ZUvPdI%nS>geML|u)XrxATG|>&u=a6!BHd~zLd^5US`YpODnu~rl zavYLrE>7_}(jvtj(0U}!DDDBzf+;@T+L`;}Bdzhj8hi>uau$-1FJ6JHk8DN(H4urc zjErdK>cKhA&zv+(bN@cPJTo&RCx>m(_q-Pai`u*V=)8#);4 zun4KCqRPT7%I9`=+wEIKhDKPni!1XoGsYCeWaS5siQyh!JI$>qN5Me_K9B#=t0;pG zs=ua)V_$ zQihX}nyYbx-Kgd8%^XwU^R@Hxdn4qjLyi(_l2M<1j`Kf`5Ga=9_FsvH<7Y0=h<&ZuZR8x-PA;7bIy+;GQJz)cIz!< z9qs5lN4W)8A{A>p$WXz*TCG22_k$K|_Q^>prf+J@|jOjHpW1(T-c;2B>Ogen^CoKPGgunMJ& z$ulQq6pWb=TzucbtHLta%L83$SRSZ5*`2~1q#u17QXQ>4#jPg{sdAc1wRard&bcZo z;UEb${Np6ILO%ty4FsoPeXiTU2jt5WS$XzTSJke+guPDv8^T~T#pALjkBupu>dPS7 zRNdeL`{%1_7X>$+ibNPQUi=dg5sB=Ge-v>qO>?`+6>&<=v`k;&bk}YoQq>Pfs@prU z(+U|(c?~Z(j7x{x)r+_!R~8j7oaq*53Zc-w@&>ozmA%~xBjHu?2k=#pDL1;~nTOOk z-oj3Pj-tcz^ONYRXU8nJV-?|LH|!?2odKTgf-J8H8=EEI5q^v!e(TPDmUrcN9m!!v;| zI8%e^lzPrCH)RY#8{F+OXRzh*Y17fm&zmwyH^}z4xE-6AlUYcWzQ<@t9W8sSdrkX= ze03~yM&=lnr!RRfD_px=aE(lvoLQhdI~Vt?F)`yNH^V?NFg2rKLd?X>nf1}-fe55C zRY*19L#m_GCuEJ$`I41S6Uc&jZtSGZckRET1Ld)+x4@g41^8dF`56Z4$fS&kJS%18 zJ&l1H{s~^e7`!6{L;tU)=ot2zl4gutL0KKC4u48#B$MxOI~;s6S$_xq*9azL&X}U7 z$FZ3=WaNyW;>*a-m`WL+p@0gO-0Akb!(DF08Tm6OkC~WNaF>^V6H*QHBvnw5m6`W6 zWfcpXJeirFlcg9tve<3!Wl!E#w8SN5d6GN9WHp?FF3*h3WZJXxeFM;|qhGq5o~EL! z;FlvCBD3>jW@b#v@p0Fgn!#myKD<1Ul^^3cZPhZjy{z$*r|@X(D=NZ3BUG+Vkl;yJ z0Ixayt6&^UzB>4waVyNrnv|C@?ypx5;lmE$^WC$;t2ZZu6L_qBcg5;O{isXR|IA9a zFTu-l@RS`pWlTX{*5u6K?LBx~Z$mk)tZ1(<<1+FKa%m$kvoJqv+)UrItKE95TzZ_Jp3jlavY!)q0X|C+P!w_exJ7`bD9ttKIMdbPe4A{*bD@z(l;zJrc_)!>FH z6^871wDz7d75dD7B5(YgOV&lNdSuakH{{%Ce_JhX$JC}X!mOV{?2^Pl*eHvK6Z`AL zwqfC+K3{vK{F$U;@v~*0X_;(o2(?R+0{#ynlORLv?Xk)J%B7qqh~iLtdt$QxI<($S zf!r3!)`3!XNpird9cD+T1pH&exM-3WW}k^m_CJQ!6D`z^k4v_G53@^B0#@u5c6952 zKl2KokKiaSWp9s9wjRC0-jW#be-D}BkQBF?m$tJL0{-cxeZEeRmQ%~V(soHg!1{wc zY8e?GtVNw)9B!Ai4p<+C+tF+M-6r7g$KDg5xX;c_NcP{2rpij$>iqkDJGyPa zDqqIVZX57l!va)Zh@BgoY;7%Lmyq{G89TaNz(0+((8bB4A^%h7HEQwyj;0pEG?YuU zb}ZVyfZ;%ZUq?@efCHYryON)o605+kvBK?l721t6BwnOUHnfUcoNu z81OHs5UiRxw_d4WXQu}IrCEY1j!zhA2ejr+WoNEWDTNYeZw<8duO_7y2_afMh30lq z?KiCC_RYz&234}7I|uypJTk-%9spB(YlJAofh!tRS9+}7pHz)#V$z;SPdfV=&pgVd0gpQ*^}D0 z3n$@DzozjwG|!8ZQo^eS`=lWip4Svn+s>7`&!+`vRj6)nfgH%bp5#_d|87U?alY!e&|IHu zg;wGDrt996b*y6O12lIS@$HhWs9N@x-h^N+y96=6 zmL1(E;D5Q+`Izl|`TqN_tbKl?VZhg*DXdG`@m*5FQ4~O-PBb1zOF$$3SnyBPv7`G1 ztmSp>Y{Un3?JfNR{#y0ia0+!|ygQoa#oe@4*0Zzw2mHq%HFc%5N&3U;2Un2>)E!M5 zD0!*L{^@A#(8BFA$;s9;_3bSK0{(wO1|XU1_R0R3tIumG$=1ZH?d*X8Ys=O4mVp8P zY4Wba(x;VSr8lsn2L-I-4eabe0e>^DUz&2apM_`|rr(b5m291CXh#obv*QW3t7D?k zn#T5)!2#?{}H6(l}(wRRf@5ruMhZp#JF`j-Sw|R8+N|VAJDvEQlMR|9X%}IznMwk zHeOs)4)GsEQ%Hn6lUlJwFf`Aw^!lMG1PRpGSRw!Xd4#FWRJFE zuIG3prM|h{Ux}vfhpE=Euh7~#_D$lQIuhFYN0ZY0W5ICOqPcEjWd47kb$9Z%cO(o5 z%bsY?zHiM;uuDb;tTHX_=uvD~E!|qkV+B9b(%upuus%bK3Rq2B*(Hb@TiMYW0qccU zb~Yk1(cY2~2>UFN{@GhI+J-e^fL)aG7m(6uiBCqmg=AO9;x|(5?3kz8l}h&cMnO4= zbSLi&RKSiI)2p+J(Kq}iwCA20IoKz?Q*w;x7Al1!Dtv;XnlvIC*OKj`&jZn&O zlj1)Ht=;5MTRVDOz^dQQ&PI%AXKxu7@IQe2_5eOZqn=_yih5N=N**Hgk#&KJ96bF9H|iB@lJ3eY;)TU)maCm|OzOjgmaXbJ|l|FmR( zVn25uWVD=ev-{awrU$~FgiNzzR<;Z8AG`#qw`R2qAK-S+jgL-f zw4i8VT`oZ*6s58D-a$LxKYz}^iyZC0AMN~-@UI~H-0gPz;AHWj+Bh{F{1ab&7}TC+#m*~J0> zD-c>V-2ORke;H|)vPH0%3+~@GHf2&2s>tK+ptrlx+>*wHpZO==XkYp$JkqL351=69PG4q?N~edR-S3b z`h3&vm|NS1waDb}s+F?VX4)lWT{X_BqTF4}K&vJGtD{Yb;y)fXv9g+pb>6qIhC492GD4nA+tPrh@ z?%&pPlkMm`xaLj?Hlw3&2wE4Xd-BRXXzCCLAvfC7Xzavp$ou1_1_z?onOwI!>=@jAdZ4kcv|;ne_ov0<-$|ZE!O*y&l+M$&K@Yon$L!v= zv?=UE)A7v39G_pEADk;jV-3l-vlj*YYaqQXi%B|;reV+lw!^vw!2;59(9~6DyY_GK zH21Z|X*3OrJe4bS%Q07k$Y8Vt@`!CZF&|BvK8HMezzc=Wx~e}-{g0W$&5P89jq4bi zHYc_L-c>Z3ZkN~r|I+Dv-vrWi+0STR{|U~d8RvIFw%i-hdN`K3%TmHoE^KChn!SIY zc;C$6bZT<}7nh3;t=e#1rehxJEGvnR1|l`UQD8h;bIS3mi3{cb;4B+sHNvCh^*lwOwMWx{Q^Jclx*SCt z!rf>b4P@zd^lOn_vL+DLqgb2Z*K68Z&lcO+_i&$|V{f@9;2$z4SX8^m%V>&gcaW8C zc3p)=l>a&A`;wQ0B{$sUl<+@A@P8k(w^2zPm9W3C6CFV7;PhL^ap_xVz@EgeeGMr$ z_OxH!f~Gj9Q*?MInwFkB?~QKt`K~{&%|W{`w};SF&OIx7%niOO)X@IC^{yaKLt{gr zy+dePb1V+#?~2=82Qdkh>x!m4_Z~PKO%vq4(AtgG$<@}SgrjH(1jx>m@OiFV+0q!n zYBYDAr<_ z_JaZcZ;;;ln3e2Ly~A}zxZ~d~Xo>;$E!Mz$UY>hDsd{H{wP^f3&@?aXNUXAAG`F0N z#7EKG#Uaa|(BwDg*z@->R2-eNS(nR}+ag67Vdj@i(=gFBga&Ea?3B@YMu%OD3* zfGwEQqRb*!^D7*ziHqzlTLb?07rP^(PWIkvOU`S#$^Jzbw82E0r@3$8!tG%E>mfV^ zO}iFz!ib8|G+NFGdiDfb4>X=mA56Z?M!GZ@mbsi^9nta}V+HABXzof?0G~#4!&!NC zmjySq?ITmdQ9MK9@;)?$ta}qUfz}GmIZymmmb+eK6oYv~xZK|ISipZ9q_#a?5cNs+ z??nrs5%GleA81KVaZOL6IiH7?kER~EN8CebasrpvZYkj?>H|ABk+s6@0?~Zwz2H)Z zbdarrgDB)NYBQSp;=VZhY=s^DM8F@l((Mx7X0-i1jk`1j=A&t}yunIOw)U^Iqjv@T zl~=igrk}}d-Dp}sEMh$V09s$CJ&o^2G>ynvsKHlBe|~G!Vzv8tiU$*u!)Bv(uqU-` zYaL!~XFth<+#0uYPN-Y0*VrYHt02{+Q^x-Vnj7PsonbBRacYQf7fxc3W5oPg`|h!$ zpAKB=o`64YtCe<7r%*?D(wy>hRe%_GGh_u5+!T{k)p7-pj#ofz10`uUD!t=F@5 zL?3+4vR1a=QS!#@(EqT@?I=0=T&UHu=Z-D!%(nc0dh@Yk!h3r{{43dr6{lQwyQPGr z?#TWi5b`Id?zT8mNb}L0Q1z_C)EM=ddG$Ae$Gcy4YT5%@+@)Z^Pi*!xB*-N9stx^)wkJA_#Q;cU@Nff z_McU=dwv!kgcGJukfV>Jy0l9QA4%!ErSSPrQg%-Qh5LS>7GDA4Uj_PHM%w#6t5~GS zI|NkrJ)p850hRq2=yS2u()h$-|12xQeFs$TdoTaLkZ!2_7luxim$OrYXMqOtqhs}F zsU`6{hy+z->`B>2hIpYG>19Z2B5Nb%iTX$xHt_N3j}I@FJRbzq=pCmD`D45|DPNOg2HQfqlUQXfh2 z6Fpth^4YaZu5i}C6pxlva;hhDJzi4P=X<)Ok_DbD^yD;=d?dB!-GWp%<|DPnmLe6B ztC9L3-53<1ajiuvZ=GlG-$<3a*DJpXDMvr#>5q8zTr5Rx^Z0GSHE@9-*E3K>J28~) z6J7yHC3kspx5rD${y9&-SZeXT?eP~&O~`viUdDS!CExdSNxA$(q(*oGsjQRw;}R+A z6EFW$FJDsSKJ#=*>0cm4m3VyFU_vs)-uY#nq6o4>Xj{#P>JLwCQFLWVba85c`Ave@GRIU3cL!1? zckzenSuDkq>i9BeMJnMLtU$^&YmusGohR2LHKI*OeI%8Czo%bLDr+-;NN(}$B$a&F zlSPksiAO#8m?t0iFNnA3^H#pCr3Pk?%v#P*NlM$dey? zyrlROo;>OCl1hH+=@(1c{{vng`vNJ~e}xqPHIn~)|I{Dak=%qU<@&QuhAUOz2Tzw& z@|>q%EY;9&9)GbEZ>e=p%A}O1OFBDJm`7YJWmwwdCDjpqjZM<;$ueI4#Zu+2f|o3Z z)at6{*-0u{!_y_5_*X&|*Yso*lK*^l_(L9P=<$t^se``%t%i9|@;G2hT9gGw6!cM^fv4sHaQHwIe-UQWcN#bV)Th&f~`;)uAje zUs4-bZs-7aqy-S_`3$5gnu$~cvym!T?D2DuvYYGi^E`e&QXk0>yVsd&&Q5=)mwT6& zE2$H48B(g}uFpvjNlAvGaCAywdKq$>Iqsn5ky z`M-Jje;~Eb`sH*_Dp}UkB~?#(PnT4cm5NBHB7K`vpZ`XxgVnuqHIR+aJ9_0MmEYCN zzsBPw)q(Dw9+XAFgrqvs!!zvZ8D1=v-^=4Kmg0MR{N<$b`*``1()%LmP?4{{M-1@f zKu-=r%0=m(J_M>&P{Iu300KsWk{;PM5L%G z9)GcveXeJh=jBU^FYx$6kH4H$M`p-Q@i)^ec%xU~VyWeHs|=BIJ-d0HT;SPDD*tw* zsJlG=VyTWTf|p$4HkTF?6~vn1Ev0cuY$o?Gzu)Wp{a(ktasB;X=kNDAf4|q!`yD-B{QX{M6rRwt>fi5moJ*u$^f;f3 zrKrE(>s-_)wUT0VPt3u9&nPW9Wt_=w@^`b(qGsi21lr?jsAWEAf zBIZ|yNUR0nH*;!1q*Q@8CE`jGUmN1Ih-I}Q%9@iRRz#3Ws{>KqEUp95tt!M>5fx2p zU5Ilc8migKCbuTU=4uenMnXiGpQ=KnM?ws*22ssCSPde)I>e^B5Y^2Y)v;T|pn4EB z&4zjq6KX(&)rY8M`qhVstO>DGL>&`yHN*iCnO8&9GuuVXh=Pb{0CBa+XaLcq7Q}uL z4Nb*{5XVIvYzWcV>=iM;Hblcl5KT>PBZ!nb5XVF`GxZunoE9;+F+_|xB4R~dh{Prk zEzF!I5Z&rQoDval;+sO86S1r*L`!p0#OC@CY0(ggW^pt``qdC;MI@WlW)R^GAT~9F zXl>4j*ezmEbBMNPLvx4;4I#o}AljRLF%XfBAa;uAU_xRc4v5H%g-A8qMa*an5zzvo zv&m=y(WD7P`wkFkreYk#aS?@a5Z9Q!BIY-RXc!OC-Q>nYq(nm;6LGDnmjH2E#M}gk zUgn6170nVAD1~<|j+=25%hD*#y zz#^gzMaGznHWX>n5@NrIOjEHf#BmXYZ6U^+y&~qff@s(dBFp5qgGfn)I3{AEsn;Ij zw1~OwAtsq4B32|pBnBX+m^lH6Zpjd*MC6+I4iM)=)RNcp%}EiPQy|hhLKK?C9U;c-K3^Mgtvj%lnOD^oDs2G#GpS!Nb1Ceph=oE2eAYEOvpG>A<-Ay%3*B6f=y)C*#@+0YAOLRW~e-Vpbge!U?g zuYuSpVx0-;193n^W*>+RX1jRA0kEj=1u(}9x|_pI4vS(0K`@^a{$DOo)903*k+mzgy_}_;?98(+s(To z&WUJ02x5m>FbHCEZ-}o&>@=+hL!|eC_$>lrmsu<#yf4Jr!4OZH)axL2i`aA>#M9=C zhzb252BkwhYc`}qMD~XW8v^mX={E%8fQX$U_L`8P5HkiqWDbScXSR!IG7uu-dWiid z<9dkWBKC`T*;E_`F@F$5;V_6-%w7>GgCQCYhj`884u?1`;+TkorrrpM71u$`9Rcx% zIU=H4Iz-|~h$CjsNQiSHPKh{b;zvPj9s;p!6vQ!eQbhVth_nodcg*4pi16zn&Wd=~ zq>hHzEn?GXi1*DI5fg?%+*t|YL-TGWh{)j(?JGlkY!+08I3VI{5hqRSDiAY9K-^OW z;#2d5h$bT;dTAO?nUxU`$3+aP3h}wwP!(eSC=p{IzAzicK%`_qgpGyx()1e(aazPq z5nr2-Oo$buAu=-|{%N+0=r#r-VjRR7lQ9nB+?bGZ=B05VJwm=S6~{wt9*afccr4DE zy&}>xAsSAA_|fD}fCwK4A&*%<`Aq$+killRn7LUnzxd3XVkV4-Nz8`%&1Y`ThKZa2 zb4tt~K9evJ=75-G6R9}FoSaC-GqNCtPk{(Ei*q2FWJ8pl3K3>fCqWz+@vMl_=8TB> z6Coz$Lio*w$q*?y5Miq0O4Dx&#Ay-tNKk0TIilLDVxRMa(FGNSh9EwOKqJqDdjdSrH9Q>I{hE zA~wx{Xl%}im_H3-&`gM?X2VQ~l<5#*H$XHq{ceCbEn=sL7!z_M#EKaZnKweTFxy3R zn+Xvy3nJcR%z`*4V(YCCEltInAU59sQFs$XqS-4V{YHp}vmugA?rezgSrErWv^MpM zAa;wGTLjV891$_$CWyphi1ubqF+}8Sh*KgunD{vm2ShBJ1CeS@ikMLZk#;jgXS4Wb zh$h7lXGNr$)LS5qi`aAv#5LxOi1~9M2C3`a&4ybcQf`I_n+tKR={FbR^vx`QC+4yM zdYO>hAXeOhMdoc-^fB8-bh{NIVje_4lQ9qCoQVA*2AGQTAvVv2D4Y*5$m|u7ej7x? z1rXPn+y(p*J`duUh#{ul?GU>~%)K4rdUHg?g!vGO3n7M^ISU~o7eEZ(4l&Zi-vMzz z#IideGR#R4Gj4}SyAxuJS$ro%lZ6mxMP!=PyC9B>*mM`fcymU?{5v29-3^gtHrx%7 zawkOCB8Z8m-y(?9B6fH53yUs-1QKv%@Gk3 z)d3PMu;XGAkK=|WK!>g zI4)w-eGm_rGa}~S3o&RD#DiwTCWw@c5MlR2JY@Ra4{=(=P7zy8$O8~7?t^f`VVfHc z-8Ml)Y{p`{$=Hm=IT8Cs>@XD{gxGvPMB#%FJI!7Z=?_3O+yb%7mr)c8J2qA&!~7BGMm&Xt)#N z9h18gB76tLF%j>YdQU*?7BTk;i1*DA5fdJVNZbYSp_#J_B626hDG?vLht>fR%XUMs zS~PPro`7&>?o)T>n(Tr&`y>{pOzKlu92c?aDTvR_84>e$LkxNv;tR9kX^503A;O-4 z_|o)y2I91cog%(AAVAmhA~x-Z zC~eM&n7kzv| z%zYiAra2;F!mAL82O(;iIR_ykUxPR$qK=6_1aUybvO^H{%t;Y5UWZ6~1LA74_zj3A z2O-XiXlPOoLmU^e=`ciNb4JAcLlA?GKr}TQjzFZm0TK2lL^IRxO^DMXc8Z8GAx9xr z9ENbFp@lmQ-Ht#+yoE))$#@Hkb0YSOXz31r^P3RP@Dt76V_2jgg=qLT7Re^}ZHVx< zAe^qZcDue?#N2nVXlstVgT;hn5Q)bj+M7AYAtK*~I3=QkiGLU3fQZCOL`tVkke z#yc<()nPhYW^{F!CdXm+i%GLgr5Z5D#T3?nxyCXt!0ej;Zb)RjSzp##Z`X}2Rdy3! z(<$fU-zi2$>{{Hyniyg_Mp&i7j_}Q^G9v5{SuZu;R=29`y1u1VF=W@Mmey5&`qTb4 zrkYt_#%kx3jW|@>{g*Y}x9slUN%AYh*)80focedv=gVDYPFd@Dx4okc>4@|xuKrS! z^X;p>3L)lr6dl-H!D?=~7OVK-U0IcQNPUa6rgbpiL|HY$rgh+7C3SoL?>%)HLyhO0 zTC7#=%t!UDL)L`h=8>zdo5S{u^!nxX!)fK;Ytu-&UDA(h96e$uKlqhPFCKjbeq-6< zLaSjlr(e=X)pv(8Blpd5|AFS+$nqNAMz^C++`OxHtTisA%q{a=@7_6Y*Ig~FX;xS- zeimIpmUSG<>OOuL&9^_#cR*j1839+;)Qh!#aXamFau@p5?tJf>+R7>p)f|>G;Z3ay zyY8!CjVk4Ni@xnq-*zo+Z@suEq|&Y-hpf<$h$v!4pV9m+oBpCr-(#P#u^LC{*D?Cs z>=kHC`bN|5h*hl!pZjk0=%%En(>--*uE#}_*0(wJxy|F6kzV2bZO}Y8C7Xj)9;Y8D zsKpqt(c^+YQiz4S&*S*P!cfPs1>}Psc_)SWPd|V5ZSlCfJuV(j--gylziv8T#4-$5v;->?>XoI$GEWH8aIR=<-FmjVt0`CI=Z zB5|$3aG-CDukyGyq({44l5e#~wuKlE^jYI^`Vnlt!|;zQ!>PUYAkFxXT5X#Kf3b6) zXP!#d2Q*p(so!MCyc38ula5+rLZZx(qs)0DbLyzoI8*KkJIlkWY-&f;FW#S!Y7;sEYnz zKMBRevmQ5q^t+@L6Z&a~>;{70Nhl8qQkOvW+B_&Jnf;#Dt8bAFn%KCdB_ z909fgP4z*~j*ZdzLZ_x$e$p66fxbXf{f5V7kp2gm9g&CO0qH*OchPTXmKiPiI+7~X>8PrT9&!KSzCJ7!FQ}`A=Y<3^VxAL(0(aZ zE&~Rv04u?2pzqpi3$6ufgF2uts0ZqU2B48?_^wr_Y%IwZAP&Tve(zdW7qurD03Cq- z*0&0X0JqSMTftm#8>kPi2Ks5i?_e6~=|DUDOmG9Z5!?i{!xw{dj6ys6C*V`?4{!>6 z20jO;^{)ebL81hF3BCs3fD_;((DG1}Ukw_7Mj#5*0^BWpbBy9MY=_xi&AR4@rl2Kx5Fe((~|m;SRrW+{Gx zphHT(N9YH71O2u^zk9d_bOYT%N016SfweTGFVE`>_4-YW4z!g(huAu>9^4Cbfa$=x zA3Ojy1D#bmn;rsN!6Tt;4ckdPN~PPu4xocd2h$y-_0tj`4TXYI;0jP0Ofox95LVN1 z%uJAtnGT&CpfCCAVA7$ZL*@{80~`Twf}`Lqa16Wyj)Ql>>);^JG93oADUAXdU@XW4 zD9wV~^v+YO$l&X-j$coY5@cpp?GF9Lk6pNV`&=2>tKG$k_{ z=oE_w$)F=h1zo}Y*gOcH06W1_Dk)FrDgZwy1D4HZUWl4PXX=H+JST7&Qh_iF^j+}U?J!MUISafBS0r+KX8}{?}RL% zlc&Ht;9V1P!m3mBBh^)-x=2tR=vU?gKyT0mB!gx^C)st_E?`UrK)(k%jr}Q55&kLU z)8HBK9M}s&;jcmV1ie6S&<9)xbi{qAAN;1FB!N*78DKOR1IB_(Fb<3d6F?To1`~mP zHMbm8q9bL&QpUFoEC)^K>>t?ZNY@cQ5DWrq!Fo_gzJ7GQB&?77GrAg7bbrZG(;AKCl4j-2Vvk7r-vC4a^3& z>A;#tLN}w9^yv_I1L#(yThK?~W6;t(_=(lHs4mII)b%9&($C9&puI1^*Pt}~CrDkS z_8@ih5hqxn3A%1R3qUmW=ra8q(3Qsr7Xp4ItxJb48Bahj27`fq?*4fQkBX;B=rVJ{ z4EfZmnKp#HhN!yB=$D7%Ktr$)gaHfaXTiF>=%@0Z0R0T^4*1gGOZ0H_DJ>Db#d!$xBU2(Sk+ez!%q+MIP_Z0x=>Cn-LbjoXnoLycQzc>)- zaY5Uw(67|b`TQixc$uY3^$p{wfqp zItNSy<3J`D3&w!aU=#>|PPFeFW*w0_K|27gb)Bf%^HM-ppcz+0bOXuY8ju7MK`YP_ zsPhRR9&`e!KxJju1*pN!Aeh&YbWyOuwkU0Y8dL+Sur*L2jXcsvoX%0{vXPWsf1nM_ybkt(m%!7YNDV#(c7Vr#3O@=S1`mO|!Chbrco4`do55=E09XX>1b2X-?Q+uh z0okksi-9_@45+=OU@U^G^fcze4 z&i@5e<~wi}C{J}MPyA0n^M4M=1G|Ci1^gd0(DRg@BAq}%-Url)3~`_`2#)$F{BJ;= z`xX2G)YxEwiEj1>);PcT5(skg}LD|av^}KX&3e*vmk+Q#?MuEv&u3~`_-2sW-8wPK(XxKqzbdS=$MK?@LcYaD5H zEEcIsnW4tobRrjUkrC+c49}0$mbZ{LQ37P!y~v6{Q0Rfc8L@ zsS;J7tGd#H8jw`EppA4x1Is~Y(ie9^^j|-g!ab1P!L=Y* zVPDcJ@c%|K|EgTksNPQ|t+|uyHXwrmm4U7RQjm@!ttcLj90^7Mm7j!Ev}Ys71C|yw zS3qz&j*@;8907;H{hI$nBwh!vfmgvn@P>!Ck#B)x;2rP}pwV1B5(TG%Ow&=1JPqU| zkT;LjMu{2k<>mx-EPgkf66_dL0=9nuCU*0jLk;PR*A(R1du_ zr~_(&C{PpB0QX|2*LLMVS#TvN1N>klW%VBB3Q!7!f?we*qz^>v{Qn8!1o}DTAK-WJ z8<62I;Ac+;(;*ZLx-g7%IJgR^q4Gd0qyn-kvJ$d7s0Jzn{eyuQPfdj0j|S(aD(PUw zk)*fLsX3rFX^mRxM}YX?$TyI_n!L5hCdfvhG0=HWxodi}>=3esDcx!Yut`2H$+avkU*MUDYg=xqxfGl5UWGA4< zvsB~J!*dyhqTN^6QN3+YMFT;w0Y#=dGZdtQV54P7tFmYs2sWxWX2FQwXzKlFRZdivD2+qK zH4gPp+;HSDFajv8Z6g-`3zPMuRkO8JVJunW!8kD5Gt2U%@=2b5=cWy#lzH|?t6Euk zAeX#m$T{ZZkJi=0H3NE0djpsWW`OD71bhLq5KIHIQ{F6~z3?VvG0?R_Au8DrGypfF z&jIsI#yP9nkT;-iL(Qg`o=D}}1wd}ift!aMgPf1N9o*^RUTp6seHTyw%|+&cSHVP3 z$s9Sy-}NmaOR-XbT!Pdf7lOs4HFkZ}`|UtB%i))Sr9k(Pi&vY03of=5q=Rw2inQ$3 zA=iS{;2y9>-x*O%DS$bw95>0G4};C%exSTfARpWZFgyQ%yAl0faPc$l1LzNd3Fg>O zRtNw2EK}=e{u1LPr7wP5K1y}ZU>)MME>FJ%9!1}W+zXxu&w1yF~6d0`$7PAN;6aNbp&;B$PjQwfV)W5E7}EMpqGm!T%Lw)9qI) zs;0J&s^qR4{nb|)U61hmJfd}MTx=W{6;t%9)r;@W9{km+&aY)Y|J7<4@|CIi8{(|# z^P5#YF_g}fQQ?Ng&6~~598KZG*!b9%-d{SC({*-}?p=$2=x2qDh^JQSt!eI|Xhq#T zA~A>@+HS|kdCO~AW_W05b@TIYR>10EnMXoGM_7X_Q_c#l%Xj)(SfSPVGG+N$p*3nU zM0NPkioB7f8*Z6ng~Y|Sj7?w|+blEN3Qe$Hu*_ptXiu+LlAJ?e6ldH#^|ouL&EZKs z*p*BBP~m*eDa-T@4UM*bv`k#7(7I+-XlU=KG@M$Iiu|7s{zt^5FR!fw~=EhBZ=tY9t>poYS3Z+9MlZIkZ4vVMX z{A~2*{Kw~c{fuW}dO7#De7pC$O+O_&IdQS%xMd^4=-b0(&5Y6vVoEvlFx5rr@St3A z^H-03we@i2G%F-NHo+-0tDHH3g>`p1^Rr5#q#0 zVLe~Yyc&+tYvs)Pie$cB&P4jrKQCu`N)ji1)DeDNO9YaR9k_oLW_fSTklph-_u6Y zI2^6TTDI!Bq2Jzd;wG&de9eqMMvkAH4kP|G?dQ(x9`;La)Woq!P}w6@%%ZZ4#Iv-a?!_wRSL~y{VBuA! zg>QGwU$ip(6wyWRhy~93YUUBuc=?hE)`)5*jy@*+MIVW}%jP(HNw7|Ov7$V^^y-Sb zo&7@|x$*nATNgbvHC?gpbjD{#F0b(Ka}=C-v#LVq1kG}(iWm_}DHTJ<1*gecT+@6| znUQ!iWy)0vjWbb|Li=BA@o<&UguYQx?p*y;w*JnDj_bd*LMnL7QC6Y$#TNV!WyEV4 z-#aUYv9$74s^#Y7g-!hIbjL;K)aN8^m_AnURh0>4`ZKihf9}uCmvp7rT3Fk}M&RBv zb-3TraP{_OtfbO2%P>K4u_>CMa&@^#lauyI`~AY7=Ujf(Xw(r^6n zROOpG5P0y;hYHD3G4n68tYS4tt9d=;UoStP$ zs)PjED?SqA?yPGb$0F&{1ET^K39)p-cbKvYeE*boFO~FL^)xO^j7>-)dOI{QQPt?* z{oI$TVKJ)vm@Dfq81x?f)8>iqo^4?IR-^rG+^RIj3IBZjpZ!}EG{qv(-Kd^#Xhuy7 zjW9c^h1QCC1vAagKfZdZ#-6&r9lbWbUYrJk`idZ5|pChKbeK?14)9_n>6MXL^!-DvFy76(o){V_D8jk9Og}V4a=GTk6y!-PI zEI5@lJ6_kK-eVBj)l+)B`c1;>8Z)qHrS^HeXwPDYidT-3{9w6v#Hzpx68GjTq)w(?pSh)>85eSY^}+e>K=HJ zm!I||t&O;7^J`6<7VJlBE~5=hTojZWT2*Ve@}1zYmkUn*e(;edt;(*T9qr8Y$hA3N zybi?|Hgm`N*7^=5sV9FO=5z{k=h%B)6<29)K8s@F8Z|dHY9Ti?H@$1YKib@^%|pK0 z-0aTd(0RNTr^en`6BQ51^&lx8<9Fjsg(*zwCse3Np4fir{M@4-yi7B4DSf(34$Da$ zAVP9#vwOXasCMY#h4mm-U-IU9aPxrNv3NIng{P%jXi!tom3U#j-M%zCAas zu2DU?!UfChBy*MO`j%1}Q`NZYxj&~(yxr-KeYRUI~-sFKtet+;n8|)t= zRM%-Ncn?%OYvI~E`~O;pjWOtrpx^$Y7r{2P6RDMp`O8Vc@|lw(KWIM*%BiUBE;&z^~OixckPyC!c=u zmb#^X)=8}Q0Xz;iH+`|N>US_xyM}VPY7ko6YSzK570H>my8%nEbqBNO539=ZhM}dR z22+)Gw#R>}{obPDwYw{Xq`_2#$>?C}Hl$Qe2h*k@r8rwM1P^Xw5kk=c80yQ%XJ4!8}8$q))L_5U;(r(zI>QW>u{casWm_@z-@U?Pw17 zqYkew^UMG)uWdS-yhe1xtvJ$3>uB~g!pI$Iq}8{hncS1>^Hq&QYt;DbR!#jstX4^j zI=ValwycMay}-ZL#&y0fW|3ssE@piSt{dLPeAGBJl7G|W$Ht*8t=eg(d6NsYmEP5il3vi& z+|Yz#3%i;}kWp`Tb$6~UUlix3eB5H+g&3c8jro-lQR!H4oh)8)fArTsem3sQ3l{E_ zjtidkQH$DpgBOvBR*?H zv#`Run+ef$)4N7QRqXCY_QO5DPVZOkHQgG$l2P5w;b;ct`Xkbc?{4Cf&^vZFwVTmq zPp{tkYcTOqb|JiJAY*5MD6m)~5a%>7rBqbCC5Amdu|9Ca{DXPR>**m$kE zDu$*wPbrZ!Je{!BTguG8K~IT9mov zgA0~~#EDoQ8iMooq*v$7<*!?j)vHN|3zorod#SfMOl(+h^)`>TKz`QS#Pvmb^Jo3o z+x#v*I9s88O!HyrT+n*P;mISWDDJ}Mv+@~<5l+*rL;8TETVcNG>MsGqd9 zWUl+f>}(INWzX|&@p@yXwqzS6BJ1AC!LYt%XsO@^Ykl6|>}h#n>6{PiU^xHO-wb0i zqc&daF2=hyJT~aJo8B0tsc|DHINgy0m$P$6H67^2Mb(~H{qynXZajEl4wD9&kN)ep z;3Vl8)s?y`5IN;PUDl|6+;ty%&LhBo?`M`KUexViTm<7`@IcdB=g1}N+Zs90jOueC zJ{-qHEv)UXj@^rAecWmN^hGq|tqyM|`oinSm)SSo=5Ld<4ufQlJSOp{fy4_kjM-XGQ$ICyW?qZZJ%7G6Y!?N8ru_hk3(&&Ze{=NRZ|U0RcO zJeW4ELu*8h8|+?2-fDk#Yl8Kiyw3`E9{Zis#M$$l)5IIf|J0%L`S)CBI<~=WFI?x| z2S#iuU;mrB)2lenNs)6tB!}nS;-!-c4^>;4d&7ksE8X0svh~yXJ3%bchu$)HcgRi- z8XW7C?Mx1vXYtkrtKZ)#WeOT~VS6tGh|K9a8zq{*##jEM2LR)G- zg9Q&m#Wjzv7~O93(g7DNqK3F-KTiDq+n*{n_~(V3PD9KH%36avnApLzvKGq->hO(w z?!~VcIsMQ9hx=KH#qF`UY+NO@SCPW!nF7PW?&XFAZ+xIw0_Wb>hB zdAraqR$!?4pk3&|sP)&o^U`qt{R0};%q|ry*~;f0D3MW5V^N8^Q>#3bRb$)W8?o@V zga3JK+lQHN)#{tW%prN<>tXKOpEge}9QaGb*vH(~c>m%nGu#9^V(E=2*k98-z|*DM zdA75TY-blMe=%@Q)I`^;YN@#36kJ-hS5ej4NjzS=7rWIFyGm-zeP$xv@?P1y58PGF ztsO$6{-Uv#v8{cVdf6Ly(#lcp6=}q~-^TV{Ut>XVyzz-U$7)J!`~Ph|{i&iMxBPGWQhI$Krg4%1ktGrE&U2O>|3a+jnpGfsN}PztE9j z2QF>cz2!s(4{`HMS2|!%H2><#Y!>F2j@QtU?{eIYW7ZYJSGT`o(`2gk9$0ujEx3jU zxA({JjlTH-(U+w@GYiKTa|yMpL_gvb$UdFzZe>6}tVOO0GSus$z?K_J9whe1W2D6fMms>9LNWmhY}2kOV$Z zG_lJ^a{|rbFLxzSRT5fP{6mKXw4D;E{ZzC^ z@f;53L6;Yf^ZraCRmC6_n>&exa3idmggFh+7!NetjeFr-yXSu8ICvPcJ4n#Vxs>w7 z)HDg*5e^vx(MDJ(j<@j^DhXskrb4SgX&a|)GvNObRm*x04)|#{p@eCne-DXj{{hcc z@%3QEQ8rR1vpBU`X=$5Q8svj1=;z6hg|1a;R_dN)5ixvus$ysE2n`MMv^1E~4NU~T zM&(d4RRqCwXQv3&RxV6lvb1TI518c}1Kci3p{odyl_e?U7L0`hFFzlQX8{;*!CWWu zcB&t_dDL}F1%&#=-M4Gotlmb>*ZtxxIo%ES^q5B%m1K>4dG16aIwf)iO$&p7B%arujYzQQYuBYEJE`I6BG4$^ zy)Ji%yK4v3ppiZ2#{YQYO>_yKKuygMjwSRd zzbE#T%<3xS!O^II$A983Q?s~7gWS%+xa30p0kd?5D|zlv>%L*FKi`8{xa%_kvrVNc zj_R6qHb~qL(v`>}4cf95R~)c>s#hw!(&@(;Q6z7(E$Nv7ZCjdJWD7qu zCEZ+o{=N5kS)AJ$BA>SS;sy)sd+p-CtzE7<@CDT#zUMr%DSb9p)C&}CKyfAoV zsC$OXMo{QUiUHYlWH#jcX?*bM;)y3P;atfNmDJIqR(Uj#{bg7W;`MzmV`^!?YWZRj8*O$=`LH(bvwvV}={DOW z(8#%9`iC`iJ__xPHI#&BwZmGG3plrba^-h<3nH~1TA^LH0Nr~@6hv3&Lh3RPqv+X| zo@;3|hP{vSB)WGowh;Li&j19I&OzZv;EMx*7^eGa10}>kke(x{0xkY4<*7Iptd{4C5XV|Qb;r9( zyK9&ofehzwe?C1wACr9riuT}dUiwcdmWaA-l0Sgbf8h(?ZJk{sd_sIy)hY&mzh|}T z>a~2zMPJpuK#V?o$mH;DE5Fe%us|pM3#bMZ%J2d@J0G^)BAfay0B&z4RVLF(D0}BG zK#$M@+P(nLaqB=4p?5u8(B?vR)ei8*&sS`~*a8|DkEPlQXfAIv3Mf5=J5UC!k4(3Q2pd^5#Tv}A;jQ3AM`o;axwTO2Ub}; z0&CEBuwMXYMC==_i1ANJH1+Gc0Udix<~bR8b0w^>co8@;7t%AlU0z6~iy*qyg<@3y z;z30{4|arb(Lu{UNeZ1(SzSoS(ZiWtlF5vGRLLeWoZFVVv-Vcs4#jXlw|sXXg=#iq z;%PQ2H)9w9%?Hr;X4X@=PSpAp+*x{5$g<@%;i6yZ*FAcBQ8ez4aSw&rP^G541W3I4 zb>WC#D;oW`FaJ6Nc?!e^+NC}y_MZ2v2TdrYe&x+6%E-ppVS!9 z&MQEXDHgEmJT`D928gfT0`Zy4MY#EKIr&eQ+B{c0tBW;i!WL9H?C(!Gq z+zIW18n~h&KDpvrdhu7DtmnGKbTZbZnM+t{GaA>(uzQ7@@pQ#zgf|$N4BZGslBvMI zT~1~t(mNQHwNq(#3d?NXBWfGZW1u4>m7OrixWL0V2Q9r!;Ok+bq{- z#8pd&#|l+nd~0a_CY(mX#p7Zl)UE;``%Sc&}T~7XuynSxhk~-ub*KRc}xEy!6ApeM662J>!dK?xjM; zPCsTj5U}BC`Q5)wKt;ul34X)n_o@pioyLs!uAPME$3buImESr2(?u22@!S#5lvDH8 z;rE6di^nsMqqm>9QufZv{hz4GzzbdsIr7st2bRs>_(c((`RNoM7ar+m|KPxu9@MRf zbsyVc5d)t;&=@hk@wdv>R-yfGZCUZ{V<(;Ki02OIcfq#5=#!wD&u|25s7L=Zf#R*y zP{_KsFUC3G1-iHQoV*t3ADBrCide0COkzUJ5{qSCg3Y{?hHPRb^wlP|k0KhFN~3~Z zS~*sfu~-+nxrKG<5|v<$A7r&HHP5qJmn0=6T5Q%qF^OiXb1)y;cNy2~L(ABc)N~mo z)#pp`+t(%Rq@%8a9qS`Lx5P`wu3EOmg?g4Se;O6);_3)J#)h_aUm9hZZyl5rWwFk) zIP71up#~aCY&^X=&BfbMMr@Tr5hZLUt=-1FY0CG^)lq$x9hgl8#Vm>15hmNYnT>G7 Tl(JF-)daY7a_sbX8Pezf`wY*H delta 48232 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 diff --git a/nginx.conf b/nginx.conf index 6c402b4..6e330da 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,4 +1,8 @@ -events {} +worker_processes 1; + +events { + worker_connections 1024; +} http { include mime.types; @@ -7,18 +11,23 @@ http { keepalive_timeout 65; server { - listen 443; - server_name localhost; + listen 80; - location /auth { - alias /usr/share/nginx/html/auth; - try_files $uri $uri/ /auth/index.html; - } + location /auth { + proxy_pass http://localhost:7001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } - location /backend { - alias /usr/share/nginx/html/backend; - try_files $uri $uri/ /backend/index.html; - } + location / { + proxy_pass http://localhost:7000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } # error pages error_page 500 502 503 504 /50x.html; From 020d5299314bbcb4de52a390ac666c3785b53712 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 21 Feb 2024 17:38:46 -0500 Subject: [PATCH 024/259] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_syncrow(dev).yml | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/dev_syncrow(dev).yml diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml new file mode 100644 index 0000000..8c30361 --- /dev/null +++ b/.github/workflows/dev_syncrow(dev).yml @@ -0,0 +1,71 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - syncrow + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v3 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'dev' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_B0E5D07AA5C94CCABC05A696CFDFA185 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D2A87194F5BF4F2CA24CE8C4D3EDDF24 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_4E6606241F0B4089BAFF050BC609DDE0 }} + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'syncrow' + slot-name: 'dev' + package: . + \ No newline at end of file From eb3f6bdbc8e89eefe4cfb3281d50c28694d1d7c5 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:04:06 +0300 Subject: [PATCH 025/259] updating the pipeline --- .github/workflows/azure-webapps-node.yml | 59 -------------------- .github/workflows/dev_syncrow(dev).yml | 42 +++++++------- .github/workflows/dev_syncrow-dev.yml | 71 ------------------------ 3 files changed, 21 insertions(+), 151 deletions(-) delete mode 100644 .github/workflows/azure-webapps-node.yml delete mode 100644 .github/workflows/dev_syncrow-dev.yml diff --git a/.github/workflows/azure-webapps-node.yml b/.github/workflows/azure-webapps-node.yml deleted file mode 100644 index c648b24..0000000 --- a/.github/workflows/azure-webapps-node.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 8c30361..6ebdc01 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,6 +1,3 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - name: Build and deploy Node.js app to Azure Web App - syncrow on: @@ -9,6 +6,11 @@ on: - dev workflow_dispatch: +env: + NODE_VERSION: '20.x' + AZURE_WEB_APP_NAME: 'syncrow' + AZURE_SLOT_NAME: 'dev' + jobs: build: runs-on: ubuntu-latest @@ -16,10 +18,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Node.js version + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20.x' + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' - name: npm install, build, and test run: | @@ -28,7 +31,7 @@ jobs: npm run test --if-present - name: Zip artifact for deployment - run: zip release.zip ./* -r + run: zip -r release.zip . -x node_modules\* \*.git\* \*.github\* \*tests\* - name: Upload artifact for deployment job uses: actions/upload-artifact@v3 @@ -41,9 +44,10 @@ jobs: needs: build environment: name: 'dev' + # Dynamically set or adjust as needed url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT + permissions: + id-token: write # For federated credentials, if applicable steps: - name: Download artifact from build job @@ -53,19 +57,15 @@ jobs: - name: Unzip artifact for deployment run: unzip release.zip - - - name: Login to Azure - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_B0E5D07AA5C94CCABC05A696CFDFA185 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D2A87194F5BF4F2CA24CE8C4D3EDDF24 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_4E6606241F0B4089BAFF050BC609DDE0 }} + + - name: Login to Azure + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp + - name: Deploy to Azure Web App uses: azure/webapps-deploy@v2 with: - app-name: 'syncrow' - slot-name: 'dev' - package: . - \ No newline at end of file + app-name: ${{ env.AZURE_WEB_APP_NAME }} + slot-name: ${{ env.AZURE_SLOT_NAME }} + package: release.zip diff --git a/.github/workflows/dev_syncrow-dev.yml b/.github/workflows/dev_syncrow-dev.yml deleted file mode 100644 index 134b4cd..0000000 --- a/.github/workflows/dev_syncrow-dev.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Node.js app to Azure Web App - syncrow-dev - -on: - push: - branches: - - dev - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js version - uses: actions/setup-node@v3 - with: - node-version: '20.x' - - - name: npm install, build, and test - run: | - npm install - npm run build --if-present - npm run test --if-present - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v3 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: Login to Azure - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_F5089EEA95DF450E90E990B230B63FEA }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_6A9379B9B88748918EE02EE725C051AD }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_8041F0A416EB4B24ADE667B446A2BD0D }} - - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp - uses: azure/webapps-deploy@v2 - with: - app-name: 'syncrow-dev' - slot-name: 'Production' - package: . - \ No newline at end of file From 28abc668c4350302431341d801998c976277ec21 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:11:13 +0300 Subject: [PATCH 026/259] downdgrade azure loging to v1 --- .github/workflows/dev_syncrow(dev).yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 6ebdc01..65b9294 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -59,7 +59,7 @@ jobs: run: unzip release.zip - name: Login to Azure - uses: azure/login@v2 + uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret From 55bd17688007a3a59b2a950fffd975c1f5a04537 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:26:25 +0300 Subject: [PATCH 027/259] use azure container registry --- .github/workflows/dev_syncrow(dev).yml | 75 ++++++++------------------ 1 file changed, 23 insertions(+), 52 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 65b9294..00ed555 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,4 +1,4 @@ -name: Build and deploy Node.js app to Azure Web App - syncrow +name: Build and deploy Docker image to Azure Web App for Containers on: push: @@ -7,65 +7,36 @@ on: workflow_dispatch: env: - NODE_VERSION: '20.x' AZURE_WEB_APP_NAME: 'syncrow' - AZURE_SLOT_NAME: 'dev' + AZURE_WEB_APP_SLOT_NAME: 'dev' + ACR_REGISTRY: 'syncrow.azurecr.io' # Replace with your ACR name + IMAGE_NAME: 'backend' # Replace with your image name + IMAGE_TAG: 'latest' # Consider using dynamic tags, e.g., GitHub SHA jobs: - build: + build_and_deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - 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: Zip artifact for deployment - run: zip -r release.zip . -x node_modules\* \*.git\* \*.github\* \*tests\* - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v3 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'dev' - # Dynamically set or adjust as needed - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write # For federated credentials, if applicable - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: Login to Azure + - name: Log in to Azure uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} # JSON Service Principal credentials for Azure in a secret + creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Deploy to Azure Web App - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEB_APP_NAME }} - slot-name: ${{ env.AZURE_SLOT_NAME }} - package: release.zip + - name: Log in to Azure Container Registry + run: az acr login --name ${{ env.ACR_REGISTRY }} + + - name: Build and push Docker image + run: | + docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + - name: Set Web App with Docker container + run: | + az webapp config container set \ + --name ${{ env.AZURE_WEB_APP_NAME }} \ + --resource-group YourResourceGroupName \ # Replace with your resource group name + --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From 2a62fd5965b6dbcf9cf7af17ced99edebd7462b8 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:27:26 +0300 Subject: [PATCH 028/259] use azure container registry --- .github/workflows/dev_syncrow(dev).yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 00ed555..c779f3c 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -9,9 +9,9 @@ on: env: AZURE_WEB_APP_NAME: 'syncrow' AZURE_WEB_APP_SLOT_NAME: 'dev' - ACR_REGISTRY: 'syncrow.azurecr.io' # Replace with your ACR name - IMAGE_NAME: 'backend' # Replace with your image name - IMAGE_TAG: 'latest' # Consider using dynamic tags, e.g., GitHub SHA + ACR_REGISTRY: 'syncrow.azurecr.io' + IMAGE_NAME: 'backend' + IMAGE_TAG: 'latest' jobs: build_and_deploy: @@ -37,6 +37,6 @@ jobs: run: | az webapp config container set \ --name ${{ env.AZURE_WEB_APP_NAME }} \ - --resource-group YourResourceGroupName \ # Replace with your resource group name + --resource-group syncrow \ --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From 8468e5c890021ec2b75b8a4f6a72a107fc8dbcd6 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:36:37 +0300 Subject: [PATCH 029/259] troubleshooting pipeline --- .github/workflows/dev_syncrow(dev).yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index c779f3c..4250a37 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -28,6 +28,9 @@ jobs: - name: Log in to Azure Container Registry run: az acr login --name ${{ env.ACR_REGISTRY }} + - name: List build output + run: ls -R dist/apps/ + - name: Build and push Docker image run: | docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} From e287dae4b5584bd68b87b77b19367ddea3dae966 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 02:40:43 +0300 Subject: [PATCH 030/259] troubleshooting pipeline --- .github/workflows/dev_syncrow(dev).yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 4250a37..9c58403 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -20,6 +20,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies and build project + run: | + npm install + npm run build + + - name: List build output + run: ls -R dist/apps/ + - name: Log in to Azure uses: azure/login@v1 with: From eeb3124d06cee7eaedf8e1ba2db4892bb033ac95 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 03:02:39 +0300 Subject: [PATCH 031/259] fix dockerfile --- Dockerfile | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 109471d..25be02c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ -FROM node:20 as builder +FROM node:20-alpine -WORKDIR /src/app +RUN apk add --no-cache nginx + +COPY nginx.conf /etc/nginx/nginx.conf + +WORKDIR /app COPY package*.json ./ @@ -10,20 +14,6 @@ COPY . . RUN npm run build -# Runtime stage -FROM node:20-alpine - -RUN apk add --no-cache nginx -COPY nginx.conf /etc/nginx/nginx.conf - -WORKDIR /app - -COPY --from=builder /src/app/dist/apps/auth ./auth -COPY --from=builder /src/app/dist/apps/backend ./backend -COPY package*.json ./ - -RUN npm install - EXPOSE 80 -CMD ["sh", "-c", "nginx -g 'daemon off;' & node auth/main.js & node backend/main.js"] +CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] \ No newline at end of file From 0cf017458c2fc543c15f49373c33e84ffb77f198 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Thu, 22 Feb 2024 03:10:20 +0300 Subject: [PATCH 032/259] fix resource group --- .github/workflows/dev_syncrow(dev).yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 9c58403..fe53da8 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -53,6 +53,6 @@ jobs: run: | az webapp config container set \ --name ${{ env.AZURE_WEB_APP_NAME }} \ - --resource-group syncrow \ + --resource-group backend \ --docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ --docker-registry-server-url https://${{ env.ACR_REGISTRY }} From c04344788a4910f224beea28a15330ada0a0fa80 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 27 Feb 2024 04:33:03 +0300 Subject: [PATCH 033/259] merge with dev --- package-lock.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index b884ead..c16173a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3969,6 +3969,22 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", From 89e17fcdc8366803654162c834024ba90436e8d9 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 27 Feb 2024 10:39:31 +0300 Subject: [PATCH 034/259] trust reverse proxy --- apps/auth/src/main.ts | 6 ++++++ apps/backend/src/main.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index b3d2e2b..d5ea3ca 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -8,6 +8,12 @@ import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AuthModule); + // Enable 'trust proxy' setting + app.use((req, res, next) => { + app.getHttpAdapter().getInstance().set('trust proxy', 1); + next(); + }); + app.enableCors(); app.use( diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index d97e1d4..2ea3690 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -6,6 +6,14 @@ import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(BackendModule); + + // Enable 'trust proxy' setting + app.use((req, res, next) => { + app.getHttpAdapter().getInstance().set('trust proxy', 1); + next(); + }); + + app.enableCors(); app.use( From f67adb6d906444a0216084a2847e6b8569668335 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 27 Feb 2024 10:42:18 +0300 Subject: [PATCH 035/259] gitignote --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4b56acf..9823e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +config.dev \ No newline at end of file From 14c4d1f3d0fb4188c2950acda6693bc45aa9e273 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 27 Feb 2024 10:47:48 +0300 Subject: [PATCH 036/259] change port to 8080 --- Dockerfile | 2 +- nginx.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 25be02c..6897a22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ COPY . . RUN npm run build -EXPOSE 80 +EXPOSE 8080 CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index 6e330da..3190ba8 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,7 +11,7 @@ http { keepalive_timeout 65; server { - listen 80; + listen 8080; location /auth { proxy_pass http://localhost:7001; From 8cd16c433e28eb56d4768ea9786cf81359f2e2b8 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 04:38:37 -0500 Subject: [PATCH 037/259] update ports --- apps/auth/src/main.ts | 2 +- apps/backend/src/main.ts | 2 +- nginx.conf | 4 +- package-lock.json | 373 +++++++++++++++++++++++++-------------- 4 files changed, 245 insertions(+), 136 deletions(-) diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index d5ea3ca..3c841d6 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -34,7 +34,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - await app.listen(7001); + await app.listen(4001); } console.log('Starting auth at port 7001...'); bootstrap(); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 2ea3690..156de0f 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -29,7 +29,7 @@ async function bootstrap() { }), ); app.useGlobalPipes(new ValidationPipe()); - await app.listen(7000); + await app.listen(4000); } console.log('Starting backend at port 7000...'); diff --git a/nginx.conf b/nginx.conf index 3190ba8..0ccf92e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -14,7 +14,7 @@ http { listen 8080; location /auth { - proxy_pass http://localhost:7001; + proxy_pass http://localhost:4001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -22,7 +22,7 @@ http { } location / { - proxy_pass http://localhost:7000; + proxy_pass http://localhost:4000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/package-lock.json b/package-lock.json index 93fe131..52f47cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -347,9 +347,9 @@ } }, "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -357,11 +357,11 @@ "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -491,9 +491,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -551,14 +551,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -650,9 +650,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -839,9 +839,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -851,23 +851,23 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", @@ -876,8 +876,8 @@ "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -895,9 +895,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -1038,9 +1038,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1624,9 +1624,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", + "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -1638,9 +1638,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "devOptional": true, "engines": { "node": ">=6.0.0" @@ -1672,9 +1672,9 @@ "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1754,6 +1754,75 @@ } } }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@nestjs/common": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.3.tgz", @@ -2200,9 +2269,9 @@ "dev": true }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.4", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.4.tgz", + "integrity": "sha512-lG1GLUnL5vuRBGb3MgWUWLdGMH2Hps+pERuyQXCfWozuGKdnhf9Pbg4pkcrVUHjKrU7Rl+GCZ/299ObBXZFAxg==", "dev": true, "dependencies": { "@types/estree": "*", @@ -2325,17 +2394,17 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", + "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==", "dev": true }, "node_modules/@types/range-parser": { @@ -2345,9 +2414,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/send": { @@ -2378,9 +2447,9 @@ "dev": true }, "node_modules/@types/superagent": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz", - "integrity": "sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.4.tgz", + "integrity": "sha512-uzSBYwrpal8y2X2Pul5ZSWpzRiDha2FLcquaN95qUPnOjYgm/zQ5LIdqeJpQJTRWNTN+Rhm0aC8H06Ds2rqCYw==", "dev": true, "dependencies": { "@types/cookiejar": "^2.1.5", @@ -3240,6 +3309,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3261,9 +3344,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -3280,8 +3363,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -3375,14 +3458,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "set-function-length": "^1.2.0" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -3410,9 +3494,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001587", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", - "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", "dev": true, "funding": [ { @@ -3964,13 +4048,11 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, - "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4050,14 +4132,13 @@ } }, "node_modules/define-data-property": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.3.tgz", - "integrity": "sha512-h3GBouC+RPtNX2N0hHVLo2ZwPYurq8mLmXpOLTsw71gr7lHt5VaI4vVkDUNOfiWmm48JEXe3VM7PmLX45AMmmg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -4198,9 +4279,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.666", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.666.tgz", - "integrity": "sha512-q4lkcbQrUdlzWCUOxk6fwEza6bNCfV12oi4AJph5UibguD1aTfL4uD0nuzFv9hbPANXQMuUS0MxPSHQ1gqq5dg==", + "version": "1.4.686", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz", + "integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==", "dev": true }, "node_modules/emittery": { @@ -4229,9 +4310,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz", + "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -4250,6 +4331,17 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -4290,16 +4382,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -4709,6 +4801,20 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -4966,9 +5072,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/follow-redirects": { @@ -5318,20 +5424,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -5719,14 +5825,14 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" }, @@ -5772,9 +5878,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -6643,9 +6749,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.10.56", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.56.tgz", - "integrity": "sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==" + "version": "1.10.57", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.57.tgz", + "integrity": "sha512-OjsEd9y4LgcX+Ig09SbxWqcGESxliDDFNVepFhB9KEsQZTrnk3UdEU+cO0sW1APvLprHstQpS23OQpZ3bwxy6Q==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -7790,9 +7896,9 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -8514,6 +8620,12 @@ "node": ">=0.10.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -8521,12 +8633,6 @@ "engines": { "node": ">= 10.x" } - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, "node_modules/sprintf-js": { "version": "1.0.3", @@ -8791,9 +8897,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.28.1.tgz", + "integrity": "sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -9560,10 +9666,11 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", - "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -9629,6 +9736,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -9642,6 +9750,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } From c2c8833a53a17ac506456b05dc1fca20e3fb7fed Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 07:22:03 -0500 Subject: [PATCH 038/259] export ports --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6897a22..8783b8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN npm run build EXPOSE 8080 -CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] \ No newline at end of file +CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] From e4e0124558e84f13e5e7d80c7ca0063a159b8948 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 08:20:30 -0500 Subject: [PATCH 039/259] github action --- .github/workflows/dev_syncrow(dev).yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index fe53da8..8373a4f 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,4 +1,4 @@ -name: Build and deploy Docker image to Azure Web App for Containers +name: Auth and Backend using Docker to Azure App Service on: push: From 96a7bab4cd760080f9bbc7e36aa507d9215c0b76 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 08:42:30 -0500 Subject: [PATCH 040/259] port --- Dockerfile | 2 +- nginx.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8783b8c..b0c3e7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ COPY . . RUN npm run build -EXPOSE 8080 +EXPOSE 80 CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] diff --git a/nginx.conf b/nginx.conf index 0ccf92e..9c3841f 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,7 +11,7 @@ http { keepalive_timeout 65; server { - listen 8080; + listen 80; location /auth { proxy_pass http://localhost:4001; From 28c6dd0bab526e5275fdad4d6f6cab022bd70533 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 08:50:33 -0500 Subject: [PATCH 041/259] nest cli --- Dockerfile | 2 +- nginx.conf | 2 +- package.json | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index b0c3e7d..8783b8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ COPY . . RUN npm run build -EXPOSE 80 +EXPOSE 8080 CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] diff --git a/nginx.conf b/nginx.conf index 9c3841f..0ccf92e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,7 +11,7 @@ http { keepalive_timeout 65; server { - listen 80; + listen 8080; location /auth { proxy_pass http://localhost:4001; diff --git a/package.json b/package.json index c4d8f6a..d331e9a 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "scripts": { "build": "nest build", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "backend:dev": "nest start backend --watch", - "auth:dev": "nest start auth --watch", - "start:debug": "nest start --debug --watch", + "start": "npx nest start", + "start:dev": "npx nest start --watch", + "backend:dev": "npx nest start backend --watch", + "auth:dev": "npx nest start auth --watch", + "start:debug": "npx nest start --debug --watch", "start:prod": "node dist/apps/backend/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", @@ -20,8 +20,8 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json", - "start:auth": "nest start auth", - "start:backend": "nest start backend", + "start:auth": "npx nest start auth", + "start:backend": "npx nest start backend", "start:all": "concurrently \"npm run start:auth\" \"npm run start:backend\"" }, "dependencies": { From 7bdad08df22d4e321bad987b062d696c48bde586 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 09:35:48 -0500 Subject: [PATCH 042/259] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/dev_syncrow(dev).yml | 79 +++++++++++++++----------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 8373a4f..98f5c7b 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,4 +1,7 @@ -name: Auth and Backend using Docker to Azure App Service +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - syncrow on: push: @@ -6,53 +9,63 @@ on: - dev workflow_dispatch: -env: - AZURE_WEB_APP_NAME: 'syncrow' - AZURE_WEB_APP_SLOT_NAME: 'dev' - ACR_REGISTRY: 'syncrow.azurecr.io' - IMAGE_NAME: 'backend' - IMAGE_TAG: 'latest' - jobs: - build_and_deploy: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Node.js + - name: Set up Node.js version uses: actions/setup-node@v3 with: - node-version: '20' + node-version: '20.x' - - name: Install dependencies and build project + - name: npm install, build, and test run: | npm install - npm run build + npm run build --if-present + npm run test --if-present - - name: List build output - run: ls -R dist/apps/ + - name: Zip artifact for deployment + run: zip release.zip ./* -r - - name: Log in to Azure - uses: azure/login@v1 + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v3 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + name: node-app + path: release.zip - - name: Log in to Azure Container Registry - run: az acr login --name ${{ env.ACR_REGISTRY }} + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'dev' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT - - name: List build output - run: ls -R dist/apps/ + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v3 + with: + name: node-app - - name: Build and push Docker image - run: | - docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - docker push ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_BC1CF8CCEBC14B44B009FB9557BAD1A8 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_7A710FEDFC48473BA2E60430D22C994D }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_C599EA05F8D4418FB72503DC7D7F88A2 }} - - name: Set Web App with Docker container - run: | - az webapp config container set \ - --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 }} + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'syncrow' + slot-name: 'dev' + package: . + \ No newline at end of file From e02beafc13e5390f4006163acdaa5b98fbf3c3cf Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 09:48:01 -0500 Subject: [PATCH 043/259] test From b3179a5c1f5e77c7044d4b8edbb178541f7696e1 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Sun, 3 Mar 2024 09:51:03 -0500 Subject: [PATCH 044/259] npx --- .github/workflows/dev_syncrow(dev).yml | 79 +++++++++++--------------- package.json | 2 +- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 98f5c7b..8373a4f 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,7 +1,4 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Node.js app to Azure Web App - syncrow +name: Auth and Backend using Docker to Azure App Service on: push: @@ -9,63 +6,53 @@ on: - dev workflow_dispatch: +env: + AZURE_WEB_APP_NAME: 'syncrow' + AZURE_WEB_APP_SLOT_NAME: 'dev' + ACR_REGISTRY: 'syncrow.azurecr.io' + IMAGE_NAME: 'backend' + IMAGE_TAG: 'latest' + jobs: - build: + build_and_deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Node.js version + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20.x' + node-version: '20' - - name: npm install, build, and test + - name: Install dependencies and build project run: | npm install - npm run build --if-present - npm run test --if-present + npm run build - - name: Zip artifact for deployment - run: zip release.zip ./* -r + - name: List build output + run: ls -R dist/apps/ - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v3 + - name: Log in to Azure + uses: azure/login@v1 with: - name: node-app - path: release.zip + creds: ${{ secrets.AZURE_CREDENTIALS }} - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'dev' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT + - name: Log in to Azure Container Registry + run: az acr login --name ${{ env.ACR_REGISTRY }} - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v3 - with: - name: node-app + - name: List build output + run: ls -R dist/apps/ - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: Login to Azure - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_BC1CF8CCEBC14B44B009FB9557BAD1A8 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_7A710FEDFC48473BA2E60430D22C994D }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_C599EA05F8D4418FB72503DC7D7F88A2 }} + - name: Build and push Docker image + run: | + docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp - uses: azure/webapps-deploy@v2 - with: - app-name: 'syncrow' - slot-name: 'dev' - package: . - \ No newline at end of file + - name: Set Web App with Docker container + run: | + az webapp config container set \ + --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 }} diff --git a/package.json b/package.json index d331e9a..b88e486 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "npx nest build", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "npx nest start", "start:dev": "npx nest start --watch", From c5537b3230646cabd335199fb4807ef8b67fc6ff Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:49:51 +0300 Subject: [PATCH 045/259] convert project from microservices to rest apis --- apps/auth/src/auth.controller.spec.ts | 22 ------ apps/auth/src/auth.controller.ts | 12 --- apps/auth/src/auth.module.ts | 18 ----- apps/auth/src/auth.service.ts | 8 -- apps/auth/src/config/index.ts | 4 - apps/auth/src/config/jwt.config.ts | 11 --- .../src/modules/authentication/dtos/index.ts | 4 - apps/auth/test/app.e2e-spec.ts | 24 ------ apps/auth/test/jest-e2e.json | 9 --- apps/auth/tsconfig.app.json | 9 --- apps/backend/src/backend.controller.ts | 12 --- apps/backend/src/backend.module.ts | 20 ----- apps/backend/src/backend.service.ts | 8 -- apps/backend/src/config/app.config.ts | 9 --- apps/backend/src/config/auth.config.ts | 10 --- apps/backend/src/config/index.ts | 4 - apps/backend/src/main.ts | 36 --------- .../src/modules/user/controllers/index.ts | 1 - apps/backend/src/modules/user/dtos/index.ts | 1 - .../src/modules/user/services/index.ts | 1 - apps/backend/tsconfig.app.json | 9 --- libs/common/src/auth/auth.module.ts | 2 +- libs/common/src/auth/services/auth.service.ts | 6 +- .../src/auth/strategies/jwt.strategy.ts | 2 +- libs/common/src/common.module.ts | 4 +- libs/common/src/config/email.config.ts | 2 +- libs/common/src/database/database.module.ts | 3 +- .../strategies/snack-naming.strategy.ts | 1 - libs/common/src/guards/jwt.auth.guard.ts | 2 +- libs/common/src/helper/services/index.ts | 2 +- .../common/src/modules/abstract/dtos/index.ts | 2 +- .../abstract/entities/abstract.entity.ts | 6 +- .../src/modules/session/dtos/session.dto.ts | 1 - .../src/modules/session/entities/index.ts | 2 +- .../session/entities/session.entity.ts | 2 +- .../common/src/modules/user-otp/dtos/index.ts | 2 +- .../src/modules/user-otp/dtos/user-otp.dto.ts | 2 +- .../src/modules/user-otp/entities/index.ts | 2 +- .../user-otp/entities/user-otp.entity.ts | 6 +- .../src/modules/user/entities/user.entity.ts | 2 +- .../common/src/response/response.decorator.ts | 2 +- .../src/response/response.interceptor.ts | 8 +- libs/common/src/util/types.ts | 79 +++++++++---------- .../src/util/user-auth.swagger.utils.ts | 4 +- nest-cli.json | 42 +++------- package.json | 9 +-- src/app.module.ts | 17 ++++ .../auth/auth.module.ts | 10 +-- .../constants/login.response.constant.ts | 0 .../controllers/authentication.controller.ts | 0 .../auth}/controllers/index.ts | 0 .../auth}/controllers/user-auth.controller.ts | 4 +- src/auth/dtos/index.ts | 4 + .../auth}/dtos/user-auth.dto.ts | 16 ++-- .../auth}/dtos/user-login.dto.ts | 0 .../auth}/dtos/user-otp.dto.ts | 2 +- .../auth}/dtos/user-password.dto.ts | 0 .../auth}/services/authentication.service.ts | 1 - .../auth}/services/index.ts | 0 .../auth}/services/user-auth.service.ts | 16 ++-- {apps/auth/src => src}/config/app.config.ts | 0 {apps/auth/src => src}/config/auth.config.ts | 0 src/config/index.ts | 4 + .../backend/src => src}/config/jwt.config.ts | 2 +- {apps/auth/src => src}/main.ts | 11 ++- src/users/controllers/index.ts | 1 + .../users}/controllers/user.controller.ts | 7 +- src/users/dtos/index.ts | 1 + .../user => src/users}/dtos/user.list.dto.ts | 15 ++-- src/users/services/index.ts | 1 + .../users}/services/user.service.ts | 0 .../modules/user => src/users}/user.module.ts | 0 72 files changed, 155 insertions(+), 384 deletions(-) delete mode 100644 apps/auth/src/auth.controller.spec.ts delete mode 100644 apps/auth/src/auth.controller.ts delete mode 100644 apps/auth/src/auth.module.ts delete mode 100644 apps/auth/src/auth.service.ts delete mode 100644 apps/auth/src/config/index.ts delete mode 100644 apps/auth/src/config/jwt.config.ts delete mode 100644 apps/auth/src/modules/authentication/dtos/index.ts delete mode 100644 apps/auth/test/app.e2e-spec.ts delete mode 100644 apps/auth/test/jest-e2e.json delete mode 100644 apps/auth/tsconfig.app.json delete mode 100644 apps/backend/src/backend.controller.ts delete mode 100644 apps/backend/src/backend.module.ts delete mode 100644 apps/backend/src/backend.service.ts delete mode 100644 apps/backend/src/config/app.config.ts delete mode 100644 apps/backend/src/config/auth.config.ts delete mode 100644 apps/backend/src/config/index.ts delete mode 100644 apps/backend/src/main.ts delete mode 100644 apps/backend/src/modules/user/controllers/index.ts delete mode 100644 apps/backend/src/modules/user/dtos/index.ts delete mode 100644 apps/backend/src/modules/user/services/index.ts delete mode 100644 apps/backend/tsconfig.app.json create mode 100644 src/app.module.ts rename apps/auth/src/modules/authentication/authentication.module.ts => src/auth/auth.module.ts (61%) rename {apps/auth/src/modules/authentication => src/auth}/constants/login.response.constant.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/controllers/authentication.controller.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/controllers/index.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/controllers/user-auth.controller.ts (93%) create mode 100644 src/auth/dtos/index.ts rename {apps/auth/src/modules/authentication => src/auth}/dtos/user-auth.dto.ts (70%) rename {apps/auth/src/modules/authentication => src/auth}/dtos/user-login.dto.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/dtos/user-otp.dto.ts (83%) rename {apps/auth/src/modules/authentication => src/auth}/dtos/user-password.dto.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/services/authentication.service.ts (98%) rename {apps/auth/src/modules/authentication => src/auth}/services/index.ts (100%) rename {apps/auth/src/modules/authentication => src/auth}/services/user-auth.service.ts (85%) rename {apps/auth/src => src}/config/app.config.ts (100%) rename {apps/auth/src => src}/config/auth.config.ts (100%) create mode 100644 src/config/index.ts rename {apps/backend/src => src}/config/jwt.config.ts (98%) rename {apps/auth/src => src}/main.ts (79%) create mode 100644 src/users/controllers/index.ts rename {apps/backend/src/modules/user => src/users}/controllers/user.controller.ts (65%) create mode 100644 src/users/dtos/index.ts rename {apps/backend/src/modules/user => src/users}/dtos/user.list.dto.ts (61%) create mode 100644 src/users/services/index.ts rename {apps/backend/src/modules/user => src/users}/services/user.service.ts (100%) rename {apps/backend/src/modules/user => src/users}/user.module.ts (100%) diff --git a/apps/auth/src/auth.controller.spec.ts b/apps/auth/src/auth.controller.spec.ts deleted file mode 100644 index d59df3e..0000000 --- a/apps/auth/src/auth.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; - -describe('AuthController', () => { - let authController: AuthController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AuthController], - providers: [AuthService], - }).compile(); - - authController = app.get(AuthController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(authController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/auth/src/auth.controller.ts b/apps/auth/src/auth.controller.ts deleted file mode 100644 index 758fe3b..0000000 --- a/apps/auth/src/auth.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AuthService } from './auth.service'; - -@Controller() -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Get() - getHello(): string { - return this.authService.getHello(); - } -} diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts deleted file mode 100644 index 1701fc9..0000000 --- a/apps/auth/src/auth.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { ConfigModule } from '@nestjs/config'; -import config from './config'; -import { AuthenticationModule } from './modules/authentication/authentication.module'; -import { AuthenticationController } from './modules/authentication/controllers/authentication.controller'; -@Module({ - imports: [ - ConfigModule.forRoot({ - load: config, - }), - AuthenticationModule, - ], - controllers: [AuthController,AuthenticationController], - providers: [AuthService], -}) -export class AuthModule {} diff --git a/apps/auth/src/auth.service.ts b/apps/auth/src/auth.service.ts deleted file mode 100644 index 53400c8..0000000 --- a/apps/auth/src/auth.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AuthService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/apps/auth/src/config/index.ts b/apps/auth/src/config/index.ts deleted file mode 100644 index f1ccc51..0000000 --- a/apps/auth/src/config/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import AuthConfig from './auth.config'; -import AppConfig from './app.config' - -export default [AuthConfig,AppConfig]; diff --git a/apps/auth/src/config/jwt.config.ts b/apps/auth/src/config/jwt.config.ts deleted file mode 100644 index 17a1e92..0000000 --- a/apps/auth/src/config/jwt.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export default registerAs( - 'jwt', - (): Record => ({ - secret: process.env.JWT_SECRET, - expire_time: process.env.JWT_EXPIRE_TIME, - secret_refresh: process.env.JWT_SECRET_REFRESH, - expire_refresh: process.env.JWT_EXPIRE_TIME_REFRESH, - }) -); \ No newline at end of file diff --git a/apps/auth/src/modules/authentication/dtos/index.ts b/apps/auth/src/modules/authentication/dtos/index.ts deleted file mode 100644 index 069bcc1..0000000 --- a/apps/auth/src/modules/authentication/dtos/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './user-auth.dto' -export * from './user-login.dto' -export * from './user-otp.dto' -export * from './user-password.dto' \ No newline at end of file diff --git a/apps/auth/test/app.e2e-spec.ts b/apps/auth/test/app.e2e-spec.ts deleted file mode 100644 index 6eb0624..0000000 --- a/apps/auth/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AuthModule } from './../src/auth.module'; - -describe('AuthController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AuthModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/apps/auth/test/jest-e2e.json b/apps/auth/test/jest-e2e.json deleted file mode 100644 index e9d912f..0000000 --- a/apps/auth/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/apps/auth/tsconfig.app.json b/apps/auth/tsconfig.app.json deleted file mode 100644 index 01d9c9a..0000000 --- a/apps/auth/tsconfig.app.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": false, - "outDir": "../../dist/apps/auth" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] -} diff --git a/apps/backend/src/backend.controller.ts b/apps/backend/src/backend.controller.ts deleted file mode 100644 index 6fc2a0e..0000000 --- a/apps/backend/src/backend.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './backend.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get('healthcheck') - getHello(): string { - return this.appService.healthcheck(); - } -} diff --git a/apps/backend/src/backend.module.ts b/apps/backend/src/backend.module.ts deleted file mode 100644 index 9afbd2f..0000000 --- a/apps/backend/src/backend.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './backend.controller'; -import { AppService } from './backend.service'; -import { UserModule } from './modules/user/user.module'; -import { ConfigModule } from '@nestjs/config'; -import config from './config'; -import { CommonModule } from '@app/common'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - load: config, - }), - UserModule, - CommonModule, - ], - controllers: [AppController], - providers: [AppService], -}) -export class BackendModule {} diff --git a/apps/backend/src/backend.service.ts b/apps/backend/src/backend.service.ts deleted file mode 100644 index 4f15a30..0000000 --- a/apps/backend/src/backend.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - healthcheck(): string { - return 'Healthcheck!'; - } -} diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts deleted file mode 100644 index 0ea13da..0000000 --- a/apps/backend/src/config/app.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default () => ({ - DB_HOST: process.env.AZURE_POSTGRESQL_HOST, - DB_PORT: process.env.AZURE_POSTGRESQL_PORT, - DB_USER: process.env.AZURE_POSTGRESQL_USER, - DB_PASSWORD: process.env.AZURE_POSTGRESQL_PASSWORD, - DB_NAME: process.env.AZURE_POSTGRESQL_DATABASE, - DB_SYNC: process.env.AZURE_POSTGRESQL_SYNC, - DB_SSL: process.env.AZURE_POSTGRESQL_SSL, -}); diff --git a/apps/backend/src/config/auth.config.ts b/apps/backend/src/config/auth.config.ts deleted file mode 100644 index 3a3f870..0000000 --- a/apps/backend/src/config/auth.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export default registerAs( - 'auth-config', - (): Record => ({ - //DEVICE_ID: process.env.DEVICE_ID, - ACCESS_KEY: process.env.ACCESS_KEY, - SECRET_KEY: process.env.SECRET_KEY, - }), -); diff --git a/apps/backend/src/config/index.ts b/apps/backend/src/config/index.ts deleted file mode 100644 index f8720ad..0000000 --- a/apps/backend/src/config/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import AuthConfig from './auth.config'; -import AppConfig from './app.config' -import JwtConfig from './jwt.config' -export default [AuthConfig,AppConfig,JwtConfig]; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts deleted file mode 100644 index 156de0f..0000000 --- a/apps/backend/src/main.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { BackendModule } from './backend.module'; -import rateLimit from 'express-rate-limit'; -import helmet from 'helmet'; -import { ValidationPipe } from '@nestjs/common'; - -async function bootstrap() { - const app = await NestFactory.create(BackendModule); - - // Enable 'trust proxy' setting - app.use((req, res, next) => { - app.getHttpAdapter().getInstance().set('trust proxy', 1); - next(); - }); - - - app.enableCors(); - - app.use( - rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - max: 500, // limit each IP to 500 requests per windowMs - }), - ); - - app.use( - helmet({ - contentSecurityPolicy: false, - }), - ); - app.useGlobalPipes(new ValidationPipe()); - await app.listen(4000); -} - -console.log('Starting backend at port 7000...'); -bootstrap(); diff --git a/apps/backend/src/modules/user/controllers/index.ts b/apps/backend/src/modules/user/controllers/index.ts deleted file mode 100644 index bb9c2b0..0000000 --- a/apps/backend/src/modules/user/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user.controller' \ No newline at end of file diff --git a/apps/backend/src/modules/user/dtos/index.ts b/apps/backend/src/modules/user/dtos/index.ts deleted file mode 100644 index ad0550f..0000000 --- a/apps/backend/src/modules/user/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user.list.dto' \ No newline at end of file diff --git a/apps/backend/src/modules/user/services/index.ts b/apps/backend/src/modules/user/services/index.ts deleted file mode 100644 index 5a6a9db..0000000 --- a/apps/backend/src/modules/user/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user.service' \ No newline at end of file diff --git a/apps/backend/tsconfig.app.json b/apps/backend/tsconfig.app.json deleted file mode 100644 index ffc1047..0000000 --- a/apps/backend/tsconfig.app.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": false, - "outDir": "../../dist/apps/backend" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] -} diff --git a/libs/common/src/auth/auth.module.ts b/libs/common/src/auth/auth.module.ts index 64de838..236fea3 100644 --- a/libs/common/src/auth/auth.module.ts +++ b/libs/common/src/auth/auth.module.ts @@ -22,7 +22,7 @@ import { UserRepository } from '../modules/user/repositories'; }), HelperModule, ], - providers: [JwtStrategy, UserSessionRepository,AuthService,UserRepository], + providers: [JwtStrategy, UserSessionRepository, AuthService, UserRepository], exports: [AuthService], }) export class AuthModule {} diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 2d3c0ee..d305d94 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { HelperHashService } from '../../helper/services'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; -import { UserSessionEntity } from '@app/common/modules/session/entities'; +import { UserRepository } from '../../../../common/src/modules/user/repositories'; +import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; +import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; @Injectable() export class AuthService { diff --git a/libs/common/src/auth/strategies/jwt.strategy.ts b/libs/common/src/auth/strategies/jwt.strategy.ts index 833f42c..ac43543 100644 --- a/libs/common/src/auth/strategies/jwt.strategy.ts +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -2,7 +2,7 @@ import { ConfigService } from '@nestjs/config'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; +import { UserSessionRepository } from '../../../src/modules/session/repositories/session.repository'; import { AuthInterface } from '../interfaces/auth.interface'; @Injectable() diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index 62ea3a4..e9d3f01 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -8,8 +8,8 @@ import config from './config'; import { EmailService } from './util/email.service'; @Module({ - providers: [CommonService,EmailService], - exports: [CommonService, HelperModule, AuthModule,EmailService], + providers: [CommonService, EmailService], + exports: [CommonService, HelperModule, AuthModule, EmailService], imports: [ ConfigModule.forRoot({ load: config, diff --git a/libs/common/src/config/email.config.ts b/libs/common/src/config/email.config.ts index 199d084..bd249e7 100644 --- a/libs/common/src/config/email.config.ts +++ b/libs/common/src/config/email.config.ts @@ -5,7 +5,7 @@ export default registerAs( (): Record => ({ SMTP_HOST: process.env.SMTP_HOST, SMTP_PORT: parseInt(process.env.SMTP_PORT), - SMTP_SECURE: process.env.SMTP_SECURE === 'true', + SMTP_SECURE: process.env.SMTP_SECURE === 'true', SMTP_USER: process.env.SMTP_USER, SMTP_PASSWORD: process.env.SMTP_PASSWORD, }), diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index a80d929..c3aafad 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -19,7 +19,7 @@ import { UserOtpEntity } from '../modules/user-otp/entities'; username: configService.get('DB_USER'), password: configService.get('DB_PASSWORD'), database: configService.get('DB_NAME'), - entities: [UserEntity, UserSessionEntity,UserOtpEntity], + entities: [UserEntity, UserSessionEntity, UserOtpEntity], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), logging: true, @@ -32,7 +32,6 @@ import { UserOtpEntity } from '../modules/user-otp/entities'; }, continuationLocalStorage: true, ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), - }), }), ], diff --git a/libs/common/src/database/strategies/snack-naming.strategy.ts b/libs/common/src/database/strategies/snack-naming.strategy.ts index fed7e37..4953cc2 100644 --- a/libs/common/src/database/strategies/snack-naming.strategy.ts +++ b/libs/common/src/database/strategies/snack-naming.strategy.ts @@ -32,7 +32,6 @@ export class SnakeNamingStrategy firstTableName: string, secondTableName: string, firstPropertyName: any, - _secondPropertyName: string, ): string { return snakeCase( firstTableName + diff --git a/libs/common/src/guards/jwt.auth.guard.ts b/libs/common/src/guards/jwt.auth.guard.ts index 1b57b77..a0ccdea 100644 --- a/libs/common/src/guards/jwt.auth.guard.ts +++ b/libs/common/src/guards/jwt.auth.guard.ts @@ -2,7 +2,7 @@ import { UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; export class JwtAuthGuard extends AuthGuard('jwt') { - handleRequest(err, user, info) { + handleRequest(err, user) { if (err || !user) { throw err || new UnauthorizedException(); } diff --git a/libs/common/src/helper/services/index.ts b/libs/common/src/helper/services/index.ts index f3f4682..0ede816 100644 --- a/libs/common/src/helper/services/index.ts +++ b/libs/common/src/helper/services/index.ts @@ -1 +1 @@ -export * from './helper.hash.service' \ No newline at end of file +export * from './helper.hash.service'; diff --git a/libs/common/src/modules/abstract/dtos/index.ts b/libs/common/src/modules/abstract/dtos/index.ts index fd297f8..8876774 100644 --- a/libs/common/src/modules/abstract/dtos/index.ts +++ b/libs/common/src/modules/abstract/dtos/index.ts @@ -1 +1 @@ -export * from './abstract.dto' \ No newline at end of file +export * from './abstract.dto'; diff --git a/libs/common/src/modules/abstract/entities/abstract.entity.ts b/libs/common/src/modules/abstract/entities/abstract.entity.ts index c065c9b..bc27899 100644 --- a/libs/common/src/modules/abstract/entities/abstract.entity.ts +++ b/libs/common/src/modules/abstract/entities/abstract.entity.ts @@ -8,11 +8,11 @@ import { } from 'typeorm'; import { AbstractDto } from '../dtos'; -import { Constructor } from '@app/common/util/types'; +import { Constructor } from '../../../../../common/src/util/types'; export abstract class AbstractEntity< T extends AbstractDto = AbstractDto, - O = never + O = never, > { @PrimaryGeneratedColumn('increment') @Exclude() @@ -38,7 +38,7 @@ export abstract class AbstractEntity< // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!dtoClass) { throw new Error( - `You need to use @UseDto on class (${this.constructor.name}) be able to call toDto function` + `You need to use @UseDto on class (${this.constructor.name}) be able to call toDto function`, ); } diff --git a/libs/common/src/modules/session/dtos/session.dto.ts b/libs/common/src/modules/session/dtos/session.dto.ts index 7005249..8e7c052 100644 --- a/libs/common/src/modules/session/dtos/session.dto.ts +++ b/libs/common/src/modules/session/dtos/session.dto.ts @@ -22,5 +22,4 @@ export class SessionDto { @IsBoolean() @IsNotEmpty() public isLoggedOut: boolean; - } diff --git a/libs/common/src/modules/session/entities/index.ts b/libs/common/src/modules/session/entities/index.ts index 737110c..626a6a7 100644 --- a/libs/common/src/modules/session/entities/index.ts +++ b/libs/common/src/modules/session/entities/index.ts @@ -1 +1 @@ -export * from './session.entity' \ No newline at end of file +export * from './session.entity'; diff --git a/libs/common/src/modules/session/entities/session.entity.ts b/libs/common/src/modules/session/entities/session.entity.ts index a4518f2..d16ad2b 100644 --- a/libs/common/src/modules/session/entities/session.entity.ts +++ b/libs/common/src/modules/session/entities/session.entity.ts @@ -5,7 +5,7 @@ import { SessionDto } from '../dtos/session.dto'; @Entity({ name: 'userSession' }) export class UserSessionEntity extends AbstractEntity { @Column({ - type: 'uuid', + type: 'uuid', default: () => 'gen_random_uuid()', nullable: false, }) diff --git a/libs/common/src/modules/user-otp/dtos/index.ts b/libs/common/src/modules/user-otp/dtos/index.ts index 2848db5..114762e 100644 --- a/libs/common/src/modules/user-otp/dtos/index.ts +++ b/libs/common/src/modules/user-otp/dtos/index.ts @@ -1 +1 @@ -export * from './user-otp.dto' \ No newline at end of file +export * from './user-otp.dto'; diff --git a/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts b/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts index e98c700..febdacb 100644 --- a/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts +++ b/libs/common/src/modules/user-otp/dtos/user-otp.dto.ts @@ -4,7 +4,7 @@ export class UserOtpDto { @IsString() @IsNotEmpty() public uuid: string; - + @IsString() @IsNotEmpty() public email: string; diff --git a/libs/common/src/modules/user-otp/entities/index.ts b/libs/common/src/modules/user-otp/entities/index.ts index f1c339e..d09957f 100644 --- a/libs/common/src/modules/user-otp/entities/index.ts +++ b/libs/common/src/modules/user-otp/entities/index.ts @@ -1 +1 @@ -export * from './user-otp.entity' \ No newline at end of file +export * from './user-otp.entity'; diff --git a/libs/common/src/modules/user-otp/entities/user-otp.entity.ts b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts index 6bd1540..454a3b1 100644 --- a/libs/common/src/modules/user-otp/entities/user-otp.entity.ts +++ b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts @@ -1,13 +1,13 @@ import { Column, Entity } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserOtpDto } from '../dtos'; -import { OtpType } from '@app/common/constants/otp-type.enum'; +import { OtpType } from '../../../../src/constants/otp-type.enum'; @Entity({ name: 'user-otp' }) export class UserOtpEntity extends AbstractEntity { @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', + type: 'uuid', + default: () => 'gen_random_uuid()', nullable: false, }) public uuid: string; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index c168c11..63f2185 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -5,7 +5,7 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @Column({ - type: 'uuid', + type: 'uuid', default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value nullable: false, }) diff --git a/libs/common/src/response/response.decorator.ts b/libs/common/src/response/response.decorator.ts index 407f4e9..aab6ec9 100644 --- a/libs/common/src/response/response.decorator.ts +++ b/libs/common/src/response/response.decorator.ts @@ -1,4 +1,4 @@ import { SetMetadata } from '@nestjs/common'; export const ResponseMessage = (message: string) => - SetMetadata('response_message', message); \ No newline at end of file + SetMetadata('response_message', message); diff --git a/libs/common/src/response/response.interceptor.ts b/libs/common/src/response/response.interceptor.ts index 7186e9c..6a456f2 100644 --- a/libs/common/src/response/response.interceptor.ts +++ b/libs/common/src/response/response.interceptor.ts @@ -1,5 +1,5 @@ export interface Response { - statusCode: number; - message: string; - data?: T; - } \ No newline at end of file + statusCode: number; + message: string; + data?: T; +} diff --git a/libs/common/src/util/types.ts b/libs/common/src/util/types.ts index c04b857..49a9efe 100644 --- a/libs/common/src/util/types.ts +++ b/libs/common/src/util/types.ts @@ -1,46 +1,45 @@ export type Constructor = new ( - ...arguments_: Arguments - ) => T; - - export type Plain = T; - export type Optional = T | undefined; - export type Nullable = T | null; - - export type PathImpl = Key extends string - ? T[Key] extends Record - ? - | `${Key}.${PathImpl> & - string}` - | `${Key}.${Exclude & string}` + ...arguments_: Arguments +) => T; + +export type Plain = T; +export type Optional = T | undefined; +export type Nullable = T | null; + +export type PathImpl = Key extends string + ? T[Key] extends Record + ? + | `${Key}.${PathImpl> & + string}` + | `${Key}.${Exclude & string}` + : never + : never; + +export type PathImpl2 = PathImpl | keyof T; + +export type Path = keyof T extends string + ? PathImpl2 extends string | keyof T + ? PathImpl2 + : keyof T + : never; + +export type PathValue< + T, + P extends Path, +> = P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends Path + ? PathValue : never - : never; - - export type PathImpl2 = PathImpl | keyof T; - - export type Path = keyof T extends string - ? PathImpl2 extends string | keyof T - ? PathImpl2 - : keyof T - : never; - - export type PathValue< - T, - P extends Path - > = P extends `${infer Key}.${infer Rest}` - ? Key extends keyof T - ? Rest extends Path - ? PathValue - : never - : never - : P extends keyof T + : never + : P extends keyof T ? T[P] : never; - - export type KeyOfType = { - [P in keyof Required]: Required[P] extends U - ? P - : Required[P] extends U[] + +export type KeyOfType = { + [P in keyof Required]: Required[P] extends U + ? P + : Required[P] extends U[] ? P : never; - }[keyof Entity]; - \ No newline at end of file +}[keyof Entity]; diff --git a/libs/common/src/util/user-auth.swagger.utils.ts b/libs/common/src/util/user-auth.swagger.utils.ts index faf1798..f6f053a 100644 --- a/libs/common/src/util/user-auth.swagger.utils.ts +++ b/libs/common/src/util/user-auth.swagger.utils.ts @@ -3,7 +3,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; export function setupSwaggerAuthentication(app: INestApplication): void { const options = new DocumentBuilder() - .setTitle('Authentication-Service') + .setTitle('APIs Documentation') .addBearerAuth({ type: 'http', scheme: 'bearer', @@ -13,7 +13,7 @@ export function setupSwaggerAuthentication(app: INestApplication): void { .build(); const document = SwaggerModule.createDocument(app, options); - SwaggerModule.setup('api/authentication/documentation', app, document, { + SwaggerModule.setup('api', app, document, { swaggerOptions: { persistAuthorization: true, }, diff --git a/nest-cli.json b/nest-cli.json index e6cd962..cf7551a 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,41 +1,19 @@ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", - "sourceRoot": "apps/backend/src", + "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, "webpack": true, - "tsConfigPath": "apps/backend/tsconfig.app.json" + "tsConfigPath": "tsconfig.build.json" }, - "monorepo": true, - "root": "apps/backend", - "projects": { - "backend": { - "type": "application", - "root": "apps/backend", - "entryFile": "main", - "sourceRoot": "apps/backend/src", - "compilerOptions": { - "tsConfigPath": "apps/backend/tsconfig.app.json" - } - }, - "auth": { - "type": "application", - "root": "apps/auth", - "entryFile": "main", - "sourceRoot": "apps/auth/src", - "compilerOptions": { - "tsConfigPath": "apps/auth/tsconfig.app.json" - } - }, - "common": { - "type": "library", - "root": "libs/common", - "entryFile": "index", - "sourceRoot": "libs/common/src", - "compilerOptions": { - "tsConfigPath": "libs/common/tsconfig.lib.json" - } + "common": { + "type": "library", + "root": "libs/common", + "entryFile": "index", + "sourceRoot": "libs/common/src", + "compilerOptions": { + "tsConfigPath": "libs/common/tsconfig.lib.json" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index b88e486..1cd47fb 100644 --- a/package.json +++ b/package.json @@ -10,19 +10,14 @@ "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "npx nest start", "start:dev": "npx nest start --watch", - "backend:dev": "npx nest start backend --watch", - "auth:dev": "npx nest start auth --watch", "start:debug": "npx nest start --debug --watch", - "start:prod": "node dist/apps/backend/main", + "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json", - "start:auth": "npx nest start auth", - "start:backend": "npx nest start backend", - "start:all": "concurrently \"npm run start:auth\" \"npm run start:backend\"" + "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" }, "dependencies": { "@nestjs/common": "^10.0.0", diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..ff8439b --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import config from './config'; +import { AuthenticationModule } from './auth/auth.module'; +import { AuthenticationController } from './auth/controllers/authentication.controller'; +import { UserModule } from './users/user.module'; +@Module({ + imports: [ + ConfigModule.forRoot({ + load: config, + }), + AuthenticationModule, + UserModule, + ], + controllers: [AuthenticationController], +}) +export class AuthModule {} diff --git a/apps/auth/src/modules/authentication/authentication.module.ts b/src/auth/auth.module.ts similarity index 61% rename from apps/auth/src/modules/authentication/authentication.module.ts rename to src/auth/auth.module.ts index 11069eb..2165d5e 100644 --- a/apps/auth/src/modules/authentication/authentication.module.ts +++ b/src/auth/auth.module.ts @@ -2,13 +2,13 @@ import { Module } from '@nestjs/common'; import { AuthenticationController } from './controllers/authentication.controller'; import { AuthenticationService } from './services/authentication.service'; import { ConfigModule } from '@nestjs/config'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { CommonModule } from '@app/common'; +import { UserRepositoryModule } from '../../libs/common/src/modules/user/user.repository.module'; +import { CommonModule } from '../../libs/common/src'; import { UserAuthController } from './controllers'; import { UserAuthService } from './services'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; -import { UserOtpRepository } from '@app/common/modules/user-otp/repositories/user-otp.repository'; +import { UserRepository } from '../../libs/common/src/modules/user/repositories'; +import { UserSessionRepository } from '../../libs/common/src/modules/session/repositories/session.repository'; +import { UserOtpRepository } from '../../libs/common/src/modules/user-otp/repositories/user-otp.repository'; @Module({ imports: [ConfigModule, UserRepositoryModule, CommonModule], diff --git a/apps/auth/src/modules/authentication/constants/login.response.constant.ts b/src/auth/constants/login.response.constant.ts similarity index 100% rename from apps/auth/src/modules/authentication/constants/login.response.constant.ts rename to src/auth/constants/login.response.constant.ts diff --git a/apps/auth/src/modules/authentication/controllers/authentication.controller.ts b/src/auth/controllers/authentication.controller.ts similarity index 100% rename from apps/auth/src/modules/authentication/controllers/authentication.controller.ts rename to src/auth/controllers/authentication.controller.ts diff --git a/apps/auth/src/modules/authentication/controllers/index.ts b/src/auth/controllers/index.ts similarity index 100% rename from apps/auth/src/modules/authentication/controllers/index.ts rename to src/auth/controllers/index.ts diff --git a/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts similarity index 93% rename from apps/auth/src/modules/authentication/controllers/user-auth.controller.ts rename to src/auth/controllers/user-auth.controller.ts index 085dd2a..c434ef8 100644 --- a/apps/auth/src/modules/authentication/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -10,9 +10,9 @@ import { import { UserAuthService } from '../services/user-auth.service'; import { UserSignUpDto } from '../dtos/user-auth.dto'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; -import { ResponseMessage } from '@app/common/response/response.decorator'; +import { ResponseMessage } from '../../../libs/common/src/response/response.decorator'; import { UserLoginDto } from '../dtos/user-login.dto'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; @Controller({ diff --git a/src/auth/dtos/index.ts b/src/auth/dtos/index.ts new file mode 100644 index 0000000..df1bcaf --- /dev/null +++ b/src/auth/dtos/index.ts @@ -0,0 +1,4 @@ +export * from './user-auth.dto'; +export * from './user-login.dto'; +export * from './user-otp.dto'; +export * from './user-password.dto'; diff --git a/apps/auth/src/modules/authentication/dtos/user-auth.dto.ts b/src/auth/dtos/user-auth.dto.ts similarity index 70% rename from apps/auth/src/modules/authentication/dtos/user-auth.dto.ts rename to src/auth/dtos/user-auth.dto.ts index 8e77057..729b735 100644 --- a/apps/auth/src/modules/authentication/dtos/user-auth.dto.ts +++ b/src/auth/dtos/user-auth.dto.ts @@ -3,32 +3,32 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; export class UserSignUpDto { @ApiProperty({ - description:'email', - required:true + description: 'email', + required: true, }) @IsEmail() @IsNotEmpty() public email: string; @ApiProperty({ - description:'password', - required:true + description: 'password', + required: true, }) @IsString() @IsNotEmpty() public password: string; @ApiProperty({ - description:'first name', - required:true + description: 'first name', + required: true, }) @IsString() @IsNotEmpty() public firstName: string; @ApiProperty({ - description:'last name', - required:true + description: 'last name', + required: true, }) @IsString() @IsNotEmpty() diff --git a/apps/auth/src/modules/authentication/dtos/user-login.dto.ts b/src/auth/dtos/user-login.dto.ts similarity index 100% rename from apps/auth/src/modules/authentication/dtos/user-login.dto.ts rename to src/auth/dtos/user-login.dto.ts diff --git a/apps/auth/src/modules/authentication/dtos/user-otp.dto.ts b/src/auth/dtos/user-otp.dto.ts similarity index 83% rename from apps/auth/src/modules/authentication/dtos/user-otp.dto.ts rename to src/auth/dtos/user-otp.dto.ts index d4fad12..bab47c8 100644 --- a/apps/auth/src/modules/authentication/dtos/user-otp.dto.ts +++ b/src/auth/dtos/user-otp.dto.ts @@ -1,4 +1,4 @@ -import { OtpType } from '@app/common/constants/otp-type.enum'; +import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsEnum, IsNotEmpty, IsString } from 'class-validator'; diff --git a/apps/auth/src/modules/authentication/dtos/user-password.dto.ts b/src/auth/dtos/user-password.dto.ts similarity index 100% rename from apps/auth/src/modules/authentication/dtos/user-password.dto.ts rename to src/auth/dtos/user-password.dto.ts diff --git a/apps/auth/src/modules/authentication/services/authentication.service.ts b/src/auth/services/authentication.service.ts similarity index 98% rename from apps/auth/src/modules/authentication/services/authentication.service.ts rename to src/auth/services/authentication.service.ts index dc9e0b2..1d5d580 100644 --- a/apps/auth/src/modules/authentication/services/authentication.service.ts +++ b/src/auth/services/authentication.service.ts @@ -89,7 +89,6 @@ export class AuthenticationService { async getRequestSign( path: string, method: string, - headers: { [k: string]: string } = {}, query: { [k: string]: any } = {}, body: { [k: string]: any } = {}, ) { diff --git a/apps/auth/src/modules/authentication/services/index.ts b/src/auth/services/index.ts similarity index 100% rename from apps/auth/src/modules/authentication/services/index.ts rename to src/auth/services/index.ts diff --git a/apps/auth/src/modules/authentication/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts similarity index 85% rename from apps/auth/src/modules/authentication/services/user-auth.service.ts rename to src/auth/services/user-auth.service.ts index 4f75152..ae8c721 100644 --- a/apps/auth/src/modules/authentication/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -1,19 +1,19 @@ -import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserRepository } from '../../../libs/common/src/modules/user/repositories'; import { BadRequestException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { UserSignUpDto } from '../dtos/user-auth.dto'; -import { HelperHashService } from '@app/common/helper/services'; +import { HelperHashService } from '../../../libs/common/src/helper/services'; import { UserLoginDto } from '../dtos/user-login.dto'; -import { AuthService } from '@app/common/auth/services/auth.service'; -import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; -import { UserOtpRepository } from '@app/common/modules/user-otp/repositories/user-otp.repository'; +import { AuthService } from '../../../libs/common/src/auth/services/auth.service'; +import { UserSessionRepository } from '../../../libs/common/src/modules/session/repositories/session.repository'; +import { UserOtpRepository } from '../../../libs/common/src/modules/user-otp/repositories/user-otp.repository'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; -import { EmailService } from '@app/common/util/email.service'; -import { OtpType } from '@app/common/constants/otp-type.enum'; -import { UserEntity } from '@app/common/modules/user/entities/user.entity'; +import { EmailService } from '../../../libs/common/src/util/email.service'; +import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; +import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; import { ILoginResponse } from '../constants/login.response.constant'; @Injectable() diff --git a/apps/auth/src/config/app.config.ts b/src/config/app.config.ts similarity index 100% rename from apps/auth/src/config/app.config.ts rename to src/config/app.config.ts diff --git a/apps/auth/src/config/auth.config.ts b/src/config/auth.config.ts similarity index 100% rename from apps/auth/src/config/auth.config.ts rename to src/config/auth.config.ts diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..0dfc023 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,4 @@ +import AuthConfig from './auth.config'; +import AppConfig from './app.config'; + +export default [AuthConfig, AppConfig]; diff --git a/apps/backend/src/config/jwt.config.ts b/src/config/jwt.config.ts similarity index 98% rename from apps/backend/src/config/jwt.config.ts rename to src/config/jwt.config.ts index a4ff896..36c2f96 100644 --- a/apps/backend/src/config/jwt.config.ts +++ b/src/config/jwt.config.ts @@ -7,5 +7,5 @@ export default registerAs( expire_time: process.env.JWT_EXPIRE_TIME, secret_refresh: process.env.JWT_SECRET_REFRESH, expire_refresh: process.env.JWT_EXPIRE_TIME_REFRESH, - }) + }), ); diff --git a/apps/auth/src/main.ts b/src/main.ts similarity index 79% rename from apps/auth/src/main.ts rename to src/main.ts index 3c841d6..ed08082 100644 --- a/apps/auth/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ import { NestFactory } from '@nestjs/core'; -import { AuthModule } from './auth.module'; +import { AuthModule } from './app.module'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; -import { setupSwaggerAuthentication } from '@app/common/util/user-auth.swagger.utils'; +import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { @@ -13,7 +13,7 @@ async function bootstrap() { app.getHttpAdapter().getInstance().set('trust proxy', 1); next(); }); - + app.enableCors(); app.use( @@ -30,11 +30,10 @@ async function bootstrap() { ); setupSwaggerAuthentication(app); - - app.useGlobalPipes(new ValidationPipe()); + app.useGlobalPipes(new ValidationPipe()); await app.listen(4001); } -console.log('Starting auth at port 7001...'); +console.log('Starting auth at port 4001...'); bootstrap(); diff --git a/src/users/controllers/index.ts b/src/users/controllers/index.ts new file mode 100644 index 0000000..edd3705 --- /dev/null +++ b/src/users/controllers/index.ts @@ -0,0 +1 @@ +export * from './user.controller'; diff --git a/apps/backend/src/modules/user/controllers/user.controller.ts b/src/users/controllers/user.controller.ts similarity index 65% rename from apps/backend/src/modules/user/controllers/user.controller.ts rename to src/users/controllers/user.controller.ts index 705a748..4620cd0 100644 --- a/apps/backend/src/modules/user/controllers/user.controller.ts +++ b/src/users/controllers/user.controller.ts @@ -1,7 +1,8 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { UserService } from '../services/user.service'; import { UserListDto } from '../dtos/user.list.dto'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; @ApiTags('User Module') @Controller({ @@ -11,6 +12,8 @@ import { ApiTags } from '@nestjs/swagger'; export class UserController { constructor(private readonly userService: UserService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('list') async userList(@Query() userListDto: UserListDto) { try { diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts new file mode 100644 index 0000000..46d6f1d --- /dev/null +++ b/src/users/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.list.dto'; diff --git a/apps/backend/src/modules/user/dtos/user.list.dto.ts b/src/users/dtos/user.list.dto.ts similarity index 61% rename from apps/backend/src/modules/user/dtos/user.list.dto.ts rename to src/users/dtos/user.list.dto.ts index da7d79a..327a2ba 100644 --- a/apps/backend/src/modules/user/dtos/user.list.dto.ts +++ b/src/users/dtos/user.list.dto.ts @@ -1,15 +1,20 @@ -import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { + IsNotEmpty, + IsNumberString, + IsOptional, + IsString, +} from 'class-validator'; export class UserListDto { @IsString() @IsOptional() schema: string; - @IsNumber() + @IsNumberString() @IsNotEmpty() page_no: number; - @IsNumber() + @IsNumberString() @IsNotEmpty() page_size: number; @@ -17,11 +22,11 @@ export class UserListDto { @IsOptional() username: string; - @IsNumber() + @IsNumberString() @IsOptional() start_time: number; - @IsNumber() + @IsNumberString() @IsOptional() end_time: number; } diff --git a/src/users/services/index.ts b/src/users/services/index.ts new file mode 100644 index 0000000..e17ee5c --- /dev/null +++ b/src/users/services/index.ts @@ -0,0 +1 @@ +export * from './user.service'; diff --git a/apps/backend/src/modules/user/services/user.service.ts b/src/users/services/user.service.ts similarity index 100% rename from apps/backend/src/modules/user/services/user.service.ts rename to src/users/services/user.service.ts diff --git a/apps/backend/src/modules/user/user.module.ts b/src/users/user.module.ts similarity index 100% rename from apps/backend/src/modules/user/user.module.ts rename to src/users/user.module.ts From a553481c9aeae93685eb83e653129fbad21a9f93 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:07:13 +0300 Subject: [PATCH 046/259] create home entity --- libs/common/src/database/database.module.ts | 3 +- libs/common/src/modules/home/dtos/home.dto.ts | 15 ++++++++++ libs/common/src/modules/home/dtos/index.ts | 1 + .../src/modules/home/entities/home.entity.ts | 29 +++++++++++++++++++ .../common/src/modules/home/entities/index.ts | 1 + .../modules/home/home.repository.module.ts | 11 +++++++ .../home/repositories/home.repository.ts | 10 +++++++ .../src/modules/home/repositories/index.ts | 1 + 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/modules/home/dtos/home.dto.ts create mode 100644 libs/common/src/modules/home/dtos/index.ts create mode 100644 libs/common/src/modules/home/entities/home.entity.ts create mode 100644 libs/common/src/modules/home/entities/index.ts create mode 100644 libs/common/src/modules/home/home.repository.module.ts create mode 100644 libs/common/src/modules/home/repositories/home.repository.ts create mode 100644 libs/common/src/modules/home/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index c3aafad..e263571 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -5,6 +5,7 @@ import { SnakeNamingStrategy } from './strategies'; import { UserEntity } from '../modules/user/entities/user.entity'; import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; +import { HomeEntity } from '../modules/home/entities'; @Module({ imports: [ @@ -19,7 +20,7 @@ import { UserOtpEntity } from '../modules/user-otp/entities'; username: configService.get('DB_USER'), password: configService.get('DB_PASSWORD'), database: configService.get('DB_NAME'), - entities: [UserEntity, UserSessionEntity, UserOtpEntity], + entities: [UserEntity, UserSessionEntity, UserOtpEntity, HomeEntity], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), logging: true, diff --git a/libs/common/src/modules/home/dtos/home.dto.ts b/libs/common/src/modules/home/dtos/home.dto.ts new file mode 100644 index 0000000..9db59f2 --- /dev/null +++ b/libs/common/src/modules/home/dtos/home.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class HomeDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public homeId: string; +} diff --git a/libs/common/src/modules/home/dtos/index.ts b/libs/common/src/modules/home/dtos/index.ts new file mode 100644 index 0000000..217847e --- /dev/null +++ b/libs/common/src/modules/home/dtos/index.ts @@ -0,0 +1 @@ +export * from './home.dto'; diff --git a/libs/common/src/modules/home/entities/home.entity.ts b/libs/common/src/modules/home/entities/home.entity.ts new file mode 100644 index 0000000..7760343 --- /dev/null +++ b/libs/common/src/modules/home/entities/home.entity.ts @@ -0,0 +1,29 @@ +import { Column, Entity } from 'typeorm'; +import { HomeDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; + +@Entity({ name: 'home' }) +export class HomeEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + userUuid: string; + + @Column({ + nullable: false, + unique: true, + }) + public homeId: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/home/entities/index.ts b/libs/common/src/modules/home/entities/index.ts new file mode 100644 index 0000000..dbf8038 --- /dev/null +++ b/libs/common/src/modules/home/entities/index.ts @@ -0,0 +1 @@ +export * from './home.entity'; diff --git a/libs/common/src/modules/home/home.repository.module.ts b/libs/common/src/modules/home/home.repository.module.ts new file mode 100644 index 0000000..e2527b8 --- /dev/null +++ b/libs/common/src/modules/home/home.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { HomeEntity } from './entities/home.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([HomeEntity])], +}) +export class HomeRepositoryModule {} diff --git a/libs/common/src/modules/home/repositories/home.repository.ts b/libs/common/src/modules/home/repositories/home.repository.ts new file mode 100644 index 0000000..3f525e2 --- /dev/null +++ b/libs/common/src/modules/home/repositories/home.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { HomeEntity } from '../entities/home.entity'; + +@Injectable() +export class HomeRepository extends Repository { + constructor(private dataSource: DataSource) { + super(HomeEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/home/repositories/index.ts b/libs/common/src/modules/home/repositories/index.ts new file mode 100644 index 0000000..af66ad7 --- /dev/null +++ b/libs/common/src/modules/home/repositories/index.ts @@ -0,0 +1 @@ +export * from './home.repository'; From 63411cada43a75f9a4aabc6ec8c53db7bfe28e03 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 10 Mar 2024 22:43:02 +0300 Subject: [PATCH 047/259] create get and post home api --- libs/common/src/modules/home/dtos/home.dto.ts | 4 ++ .../src/modules/home/entities/home.entity.ts | 5 ++ src/app.module.ts | 2 + src/home/controllers/home.controller.ts | 36 ++++++++++ src/home/controllers/index.ts | 1 + src/home/dtos/add.home.dto.ts | 20 ++++++ src/home/dtos/home.list.dto.ts | 32 +++++++++ src/home/dtos/index.ts | 2 + src/home/home.module.ts | 14 ++++ src/home/services/home.service.ts | 66 +++++++++++++++++++ src/home/services/index.ts | 1 + 11 files changed, 183 insertions(+) create mode 100644 src/home/controllers/home.controller.ts create mode 100644 src/home/controllers/index.ts create mode 100644 src/home/dtos/add.home.dto.ts create mode 100644 src/home/dtos/home.list.dto.ts create mode 100644 src/home/dtos/index.ts create mode 100644 src/home/home.module.ts create mode 100644 src/home/services/home.service.ts create mode 100644 src/home/services/index.ts diff --git a/libs/common/src/modules/home/dtos/home.dto.ts b/libs/common/src/modules/home/dtos/home.dto.ts index 9db59f2..eae0630 100644 --- a/libs/common/src/modules/home/dtos/home.dto.ts +++ b/libs/common/src/modules/home/dtos/home.dto.ts @@ -12,4 +12,8 @@ export class HomeDto { @IsString() @IsNotEmpty() public homeId: string; + + @IsString() + @IsNotEmpty() + public homeName: string; } diff --git a/libs/common/src/modules/home/entities/home.entity.ts b/libs/common/src/modules/home/entities/home.entity.ts index 7760343..ac85641 100644 --- a/libs/common/src/modules/home/entities/home.entity.ts +++ b/libs/common/src/modules/home/entities/home.entity.ts @@ -22,6 +22,11 @@ export class HomeEntity extends AbstractEntity { }) public homeId: string; + @Column({ + nullable: false, + }) + public homeName: string; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/app.module.ts b/src/app.module.ts index ff8439b..18a0a42 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import config from './config'; import { AuthenticationModule } from './auth/auth.module'; import { AuthenticationController } from './auth/controllers/authentication.controller'; import { UserModule } from './users/user.module'; +import { HomeModule } from './home/home.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -11,6 +12,7 @@ import { UserModule } from './users/user.module'; }), AuthenticationModule, UserModule, + HomeModule, ], controllers: [AuthenticationController], }) diff --git a/src/home/controllers/home.controller.ts b/src/home/controllers/home.controller.ts new file mode 100644 index 0000000..6b4eaa4 --- /dev/null +++ b/src/home/controllers/home.controller.ts @@ -0,0 +1,36 @@ +import { HomeService } from './../services/home.service'; +import { Body, Controller, Get, Post, Param, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddHomeDto } from '../dtos/add.home.dto'; + +@ApiTags('Home Module') +@Controller({ + version: '1', + path: 'home', +}) +export class HomeController { + constructor(private readonly homeService: HomeService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':userUuid') + async userList(@Param('userUuid') userUuid: string) { + try { + return await this.homeService.getHomesByUserId(userUuid); + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addHome(@Body() addHomeDto: AddHomeDto) { + try { + return await this.homeService.addHome(addHomeDto); + } catch (err) { + throw new Error(err); + } + } +} diff --git a/src/home/controllers/index.ts b/src/home/controllers/index.ts new file mode 100644 index 0000000..66a5c99 --- /dev/null +++ b/src/home/controllers/index.ts @@ -0,0 +1 @@ +export * from './home.controller'; diff --git a/src/home/dtos/add.home.dto.ts b/src/home/dtos/add.home.dto.ts new file mode 100644 index 0000000..f0d5fbe --- /dev/null +++ b/src/home/dtos/add.home.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddHomeDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + + @ApiProperty({ + description: 'homeName', + required: true, + }) + @IsString() + @IsNotEmpty() + public homeName: string; +} diff --git a/src/home/dtos/home.list.dto.ts b/src/home/dtos/home.list.dto.ts new file mode 100644 index 0000000..327a2ba --- /dev/null +++ b/src/home/dtos/home.list.dto.ts @@ -0,0 +1,32 @@ +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; +} diff --git a/src/home/dtos/index.ts b/src/home/dtos/index.ts new file mode 100644 index 0000000..12618f9 --- /dev/null +++ b/src/home/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './home.list.dto'; +export * from './add.home.dto'; diff --git a/src/home/home.module.ts b/src/home/home.module.ts new file mode 100644 index 0000000..76af90d --- /dev/null +++ b/src/home/home.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { HomeService } from './services/home.service'; +import { HomeController } from './controllers/home.controller'; +import { ConfigModule } from '@nestjs/config'; +import { HomeRepositoryModule } from '@app/common/modules/home/home.repository.module'; +import { HomeRepository } from '@app/common/modules/home/repositories'; + +@Module({ + imports: [ConfigModule, HomeRepositoryModule], + controllers: [HomeController], + providers: [HomeService, HomeRepository], + exports: [HomeService], +}) +export class HomeModule {} diff --git a/src/home/services/home.service.ts b/src/home/services/home.service.ts new file mode 100644 index 0000000..8c6602d --- /dev/null +++ b/src/home/services/home.service.ts @@ -0,0 +1,66 @@ +import { HomeRepository } from './../../../libs/common/src/modules/home/repositories/home.repository'; +import { HomeEntity } from './../../../libs/common/src/modules/home/entities/home.entity'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddHomeDto } from '../dtos'; + +@Injectable() +export class HomeService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly homeRepository: HomeRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async getHomesByUserId(userUuid: string) { + const homesData = await this.findHomes(userUuid); + + return homesData; + } + + async findHomes(userUuid: string) { + return await this.homeRepository.find({ + where: { + userUuid: userUuid, + }, + }); + } + async addHome(addHomeDto: AddHomeDto) { + try { + const path = `/v2.0/cloud/space/creation`; + const data = await this.tuya.request({ + method: 'POST', + path, + body: { name: addHomeDto.homeName }, + }); + if (data.success) { + const homeEntity = { + userUuid: addHomeDto.userUuid, + homeId: data.result, + homeName: addHomeDto.homeName, + } as HomeEntity; + const savedHome = await this.homeRepository.save(homeEntity); + return savedHome; + } + return { + success: data.success, + homeId: data.result, + }; + } catch (error) { + throw new HttpException( + 'Error adding home', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/home/services/index.ts b/src/home/services/index.ts new file mode 100644 index 0000000..23d0070 --- /dev/null +++ b/src/home/services/index.ts @@ -0,0 +1 @@ +export * from './home.service'; From 35feab71bbd8eb5daa68c6682c27bbc832d2b1a3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 11 Mar 2024 09:26:22 +0300 Subject: [PATCH 048/259] finshed homes endpoint --- src/home/services/home.service.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/home/services/home.service.ts b/src/home/services/home.service.ts index 8c6602d..74da59b 100644 --- a/src/home/services/home.service.ts +++ b/src/home/services/home.service.ts @@ -25,15 +25,27 @@ export class HomeService { async getHomesByUserId(userUuid: string) { const homesData = await this.findHomes(userUuid); - return homesData; + const homesMapper = homesData.map((home) => ({ + homeId: home.homeId, + homeName: home.homeName, + })); + + return homesMapper; } async findHomes(userUuid: string) { - return await this.homeRepository.find({ - where: { - userUuid: userUuid, - }, - }); + try { + return await this.homeRepository.find({ + where: { + userUuid: userUuid, + }, + }); + } catch (error) { + throw new HttpException( + 'Error get homes', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } async addHome(addHomeDto: AddHomeDto) { try { @@ -50,7 +62,10 @@ export class HomeService { homeName: addHomeDto.homeName, } as HomeEntity; const savedHome = await this.homeRepository.save(homeEntity); - return savedHome; + return { + homeId: savedHome.homeId, + homeName: savedHome.homeName, + }; } return { success: data.success, From b2e835c8050c3de05be47231f8d0f8379c009dc7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:31:47 +0300 Subject: [PATCH 049/259] finished add room api --- src/app.module.ts | 2 + src/room/controllers/index.ts | 1 + src/room/controllers/room.controller.ts | 36 ++++++++++++++ src/room/dtos/add.room.dto.ts | 20 ++++++++ src/room/dtos/index.ts | 1 + src/room/room.module.ts | 11 +++++ src/room/services/index.ts | 1 + src/room/services/room.service.ts | 66 +++++++++++++++++++++++++ 8 files changed, 138 insertions(+) create mode 100644 src/room/controllers/index.ts create mode 100644 src/room/controllers/room.controller.ts create mode 100644 src/room/dtos/add.room.dto.ts create mode 100644 src/room/dtos/index.ts create mode 100644 src/room/room.module.ts create mode 100644 src/room/services/index.ts create mode 100644 src/room/services/room.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 18a0a42..b890f1a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { AuthenticationModule } from './auth/auth.module'; import { AuthenticationController } from './auth/controllers/authentication.controller'; import { UserModule } from './users/user.module'; import { HomeModule } from './home/home.module'; +import { RoomModule } from './room/room.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -13,6 +14,7 @@ import { HomeModule } from './home/home.module'; AuthenticationModule, UserModule, HomeModule, + RoomModule, ], controllers: [AuthenticationController], }) diff --git a/src/room/controllers/index.ts b/src/room/controllers/index.ts new file mode 100644 index 0000000..4225d61 --- /dev/null +++ b/src/room/controllers/index.ts @@ -0,0 +1 @@ +export * from './room.controller'; diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts new file mode 100644 index 0000000..e7c2b68 --- /dev/null +++ b/src/room/controllers/room.controller.ts @@ -0,0 +1,36 @@ +import { RoomService } from '../services/room.service'; +import { Body, Controller, Get, Post, Param, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddRoomDto } from '../dtos/add.room.dto'; + +@ApiTags('Room Module') +@Controller({ + version: '1', + path: 'room', +}) +export class RoomController { + constructor(private readonly roomService: RoomService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':userUuid') + async userList(@Param('userUuid') userUuid: string) { + try { + return await this.roomService.getHomesByUserId(userUuid); + } catch (err) { + throw new Error(err); + } + } + + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Post() + async addRoom(@Body() addRoomDto: AddRoomDto) { + try { + return await this.roomService.addRoom(addRoomDto); + } catch (err) { + throw new Error(err); + } + } +} diff --git a/src/room/dtos/add.room.dto.ts b/src/room/dtos/add.room.dto.ts new file mode 100644 index 0000000..3d39559 --- /dev/null +++ b/src/room/dtos/add.room.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; + +export class AddRoomDto { + @ApiProperty({ + description: 'roomName', + required: true, + }) + @IsString() + @IsNotEmpty() + public roomName: string; + + @ApiProperty({ + description: 'homeId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public homeId: string; +} diff --git a/src/room/dtos/index.ts b/src/room/dtos/index.ts new file mode 100644 index 0000000..a510b75 --- /dev/null +++ b/src/room/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.room.dto'; diff --git a/src/room/room.module.ts b/src/room/room.module.ts new file mode 100644 index 0000000..cd520c6 --- /dev/null +++ b/src/room/room.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { RoomService } from './services/room.service'; +import { RoomController } from './controllers/room.controller'; +import { ConfigModule } from '@nestjs/config'; +@Module({ + imports: [ConfigModule], + controllers: [RoomController], + providers: [RoomService], + exports: [RoomService], +}) +export class RoomModule {} diff --git a/src/room/services/index.ts b/src/room/services/index.ts new file mode 100644 index 0000000..4f45e9a --- /dev/null +++ b/src/room/services/index.ts @@ -0,0 +1 @@ +export * from './room.service'; diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts new file mode 100644 index 0000000..14e765c --- /dev/null +++ b/src/room/services/room.service.ts @@ -0,0 +1,66 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddRoomDto } from '../dtos'; + +@Injectable() +export class RoomService { + private tuya: TuyaContext; + constructor(private readonly configService: ConfigService) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async getHomesByUserId(userUuid: string) { + // const homesData = await this.findHomes(userUuid); + + // const homesMapper = homesData.map((home) => ({ + // homeId: home.homeId, + // homeName: home.homeName, + // })); + + // return homesMapper; + console.log(userUuid); + } + + // async findHomes(userUuid: string) { + // try { + // return await this.homeRepository.find({ + // where: { + // userUuid: userUuid, + // }, + // }); + // } catch (error) { + // throw new HttpException( + // 'Error get homes', + // HttpStatus.INTERNAL_SERVER_ERROR, + // ); + // } + // } + async addRoom(addRoomDto: AddRoomDto) { + try { + const path = `/v2.0/cloud/space/creation`; + const data = await this.tuya.request({ + method: 'POST', + path, + body: { name: addRoomDto.roomName, parent_id: addRoomDto.homeId }, + }); + + return { + success: data.success, + roomId: data.result, + }; + } catch (error) { + throw new HttpException( + 'Error adding room', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} From 65ff07b0e6dcdd51da0dc360dc1c8136f6f555be Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:34:03 +0300 Subject: [PATCH 050/259] fix some issues --- src/home/dtos/home.list.dto.ts | 32 -------------------------------- src/home/dtos/index.ts | 1 - 2 files changed, 33 deletions(-) delete mode 100644 src/home/dtos/home.list.dto.ts diff --git a/src/home/dtos/home.list.dto.ts b/src/home/dtos/home.list.dto.ts deleted file mode 100644 index 327a2ba..0000000 --- a/src/home/dtos/home.list.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/home/dtos/index.ts b/src/home/dtos/index.ts index 12618f9..912a7bd 100644 --- a/src/home/dtos/index.ts +++ b/src/home/dtos/index.ts @@ -1,2 +1 @@ -export * from './home.list.dto'; export * from './add.home.dto'; From e522a3a207eab1b38e00ff9603421da99ce3fd47 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:40:24 +0300 Subject: [PATCH 051/259] finished get rooms api --- src/room/controllers/room.controller.ts | 10 +-- src/room/interfaces/get.room.interface.ts | 11 ++++ src/room/services/room.service.ts | 77 ++++++++++++++++------- 3 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 src/room/interfaces/get.room.interface.ts diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index e7c2b68..2545254 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -14,17 +14,17 @@ export class RoomController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':userUuid') - async userList(@Param('userUuid') userUuid: string) { + @Get(':homeId') + async userList(@Param('homeId') homeId: string) { try { - return await this.roomService.getHomesByUserId(userUuid); + return await this.roomService.getRoomsByHomeId(homeId); } catch (err) { throw new Error(err); } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { diff --git a/src/room/interfaces/get.room.interface.ts b/src/room/interfaces/get.room.interface.ts new file mode 100644 index 0000000..2d000a5 --- /dev/null +++ b/src/room/interfaces/get.room.interface.ts @@ -0,0 +1,11 @@ +export class GetRoomDetailsInterface { + result: { + id: string; + name: string; + }; +} +export class GetRoomsIdsInterface { + result: { + data: []; + }; +} diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 14e765c..0bc5b18 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -2,6 +2,10 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddRoomDto } from '../dtos'; +import { + GetRoomDetailsInterface, + GetRoomsIdsInterface, +} from '../interfaces/get.room.interface'; @Injectable() export class RoomService { @@ -17,32 +21,61 @@ export class RoomService { }); } - async getHomesByUserId(userUuid: string) { - // const homesData = await this.findHomes(userUuid); + async getRoomsByHomeId(homeId: string) { + try { + const roomsIds = await this.getRoomsIds(homeId); - // const homesMapper = homesData.map((home) => ({ - // homeId: home.homeId, - // homeName: home.homeName, - // })); + const roomsDetails = await Promise.all( + roomsIds.result.data.map(async (roomId) => { + const roomData = await this.getRoomDetails(roomId); + return { + roomId: roomData?.result?.id, + roomName: roomData ? roomData.result.name : null, + }; + }), + ); - // return homesMapper; - console.log(userUuid); + return roomsDetails; + } catch (error) { + throw new HttpException( + 'Error fetching rooms', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } + async getRoomsIds(homeId: string): Promise { + try { + const path = `/v2.0/cloud/space/child`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { space_id: homeId }, + }); + return response as GetRoomsIdsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching rooms ids', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getRoomDetails(roomId: string): Promise { + // Added return type + try { + const path = `/v2.0/cloud/space/${roomId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); - // async findHomes(userUuid: string) { - // try { - // return await this.homeRepository.find({ - // where: { - // userUuid: userUuid, - // }, - // }); - // } catch (error) { - // throw new HttpException( - // 'Error get homes', - // HttpStatus.INTERNAL_SERVER_ERROR, - // ); - // } - // } + return response as GetRoomDetailsInterface; // Cast response to RoomData + } catch (error) { + throw new HttpException( + 'Error fetching rooms details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async addRoom(addRoomDto: AddRoomDto) { try { const path = `/v2.0/cloud/space/creation`; From e222bef5775a5cef54edf7a28b160c0dd9c37f64 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 06:24:52 -0400 Subject: [PATCH 052/259] move to rest --- .github/workflows/dev_syncrow(dev).yml | 7 ++--- Dockerfile | 6 +--- nginx.conf | 38 -------------------------- src/main.ts | 2 +- 4 files changed, 4 insertions(+), 49 deletions(-) delete mode 100644 nginx.conf diff --git a/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml index 8373a4f..d38510c 100644 --- a/.github/workflows/dev_syncrow(dev).yml +++ b/.github/workflows/dev_syncrow(dev).yml @@ -1,4 +1,4 @@ -name: Auth and Backend using Docker to Azure App Service +name: Backend deployment to Azure App Service on: push: @@ -30,9 +30,6 @@ jobs: npm install npm run build - - name: List build output - run: ls -R dist/apps/ - - name: Log in to Azure uses: azure/login@v1 with: @@ -42,7 +39,7 @@ jobs: run: az acr login --name ${{ env.ACR_REGISTRY }} - name: List build output - run: ls -R dist/apps/ + run: ls -R dist/ - name: Build and push Docker image run: | diff --git a/Dockerfile b/Dockerfile index 8783b8c..8bd0930 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,5 @@ FROM node:20-alpine -RUN apk add --no-cache nginx - -COPY nginx.conf /etc/nginx/nginx.conf - WORKDIR /app COPY package*.json ./ @@ -14,6 +10,6 @@ COPY . . RUN npm run build -EXPOSE 8080 +EXPOSE 4000 CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index 0ccf92e..0000000 --- a/nginx.conf +++ /dev/null @@ -1,38 +0,0 @@ -worker_processes 1; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 65; - - server { - listen 8080; - - location /auth { - proxy_pass http://localhost:4001; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location / { - proxy_pass http://localhost:4000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # error pages - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - } -} diff --git a/src/main.ts b/src/main.ts index ed08082..dc73a00 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,7 +33,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - await app.listen(4001); + await app.listen(4000); } console.log('Starting auth at port 4001...'); bootstrap(); From 5ed60d970f5823afc7888dc2f6790a0fc0961177 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 06:45:55 -0400 Subject: [PATCH 053/259] fix dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8bd0930..9d436d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,4 @@ RUN npm run build EXPOSE 4000 -CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start:all"] +CMD ["sh", "-c", "npm run start:dev"] From 5c1e8fc823b8c7e55f789060a756fd83c0004f0b Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 18:36:05 -0400 Subject: [PATCH 054/259] deploy From 2f9d9c2f6684d20e1ba50691335f0ed683ab3dab Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 18:47:13 -0400 Subject: [PATCH 055/259] deploy From c264ad521e16786be4907e429b2240a12179b930 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 18:53:36 -0400 Subject: [PATCH 056/259] deploy From 404a7013375ef1b45183aa20cfe0cb81aa4d0104 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 18:59:24 -0400 Subject: [PATCH 057/259] nest cli --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 9d436d3..680427b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app COPY package*.json ./ RUN npm install +RUN npm install -g @nestjs/cli COPY . . From 66cdee665ae115136ed710ca4ac94014dc2fe90c Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 19:09:16 -0400 Subject: [PATCH 058/259] fix port --- Dockerfile | 2 +- package-lock.json | 2 +- package.json | 2 +- src/main.ts | 8 +------- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 680427b..4be2071 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,4 @@ RUN npm run build EXPOSE 4000 -CMD ["sh", "-c", "npm run start:dev"] +CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index 52f47cd..a41b90f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "typeorm": "^0.3.20" }, "devDependencies": { - "@nestjs/cli": "^10.0.0", + "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", diff --git a/package.json b/package.json index 1cd47fb..46f189f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "typeorm": "^0.3.20" }, "devDependencies": { - "@nestjs/cli": "^10.0.0", + "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", diff --git a/src/main.ts b/src/main.ts index dc73a00..423dd9e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,12 +8,6 @@ import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AuthModule); - // Enable 'trust proxy' setting - app.use((req, res, next) => { - app.getHttpAdapter().getInstance().set('trust proxy', 1); - next(); - }); - app.enableCors(); app.use( @@ -33,7 +27,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); - await app.listen(4000); + await app.listen(process.env.PORT || 4000); } console.log('Starting auth at port 4001...'); bootstrap(); From 7cd508383f96252e0eec1dee33ee49a96ca57dd3 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 19:16:41 -0400 Subject: [PATCH 059/259] port --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46f189f..759b7e8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "npx nest build", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", - "start": "npx nest start", + "start": "node dist/main", "start:dev": "npx nest start --watch", "start:debug": "npx nest start --debug --watch", "start:prod": "node dist/main", From 63d1d427b3cb1606791ed1f2352048712bdec6b6 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 19:28:02 -0400 Subject: [PATCH 060/259] port --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 423dd9e..95f8ed1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,5 +29,5 @@ async function bootstrap() { await app.listen(process.env.PORT || 4000); } -console.log('Starting auth at port 4001...'); +console.log('Starting auth at port ...', process.env.PORT || 4000); bootstrap(); From fa3f6a9e013ebc9b45c2d095b9b3404d95b84997 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Mon, 11 Mar 2024 19:35:11 -0400 Subject: [PATCH 061/259] port From 1cf345d28db3121b6dfa2ed942037910d3177a21 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:56:14 +0300 Subject: [PATCH 062/259] finished groups endpoint --- src/app.module.ts | 2 + src/group/controllers/group.controller.ts | 48 ++++++++ src/group/controllers/index.ts | 1 + src/group/dtos/add.group.dto.ts | 36 ++++++ src/group/dtos/get.group.dto.ts | 20 ++++ src/group/dtos/index.ts | 1 + src/group/group.module.ts | 11 ++ src/group/interfaces/get.group.interface.ts | 20 ++++ src/group/services/group.service.ts | 124 ++++++++++++++++++++ src/group/services/index.ts | 1 + 10 files changed, 264 insertions(+) create mode 100644 src/group/controllers/group.controller.ts create mode 100644 src/group/controllers/index.ts create mode 100644 src/group/dtos/add.group.dto.ts create mode 100644 src/group/dtos/get.group.dto.ts create mode 100644 src/group/dtos/index.ts create mode 100644 src/group/group.module.ts create mode 100644 src/group/interfaces/get.group.interface.ts create mode 100644 src/group/services/group.service.ts create mode 100644 src/group/services/index.ts diff --git a/src/app.module.ts b/src/app.module.ts index b890f1a..1da5880 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AuthenticationController } from './auth/controllers/authentication.cont import { UserModule } from './users/user.module'; import { HomeModule } from './home/home.module'; import { RoomModule } from './room/room.module'; +import { GroupModule } from './group/group.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -15,6 +16,7 @@ import { RoomModule } from './room/room.module'; UserModule, HomeModule, RoomModule, + GroupModule, ], controllers: [AuthenticationController], }) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts new file mode 100644 index 0000000..4cdb617 --- /dev/null +++ b/src/group/controllers/group.controller.ts @@ -0,0 +1,48 @@ +import { GroupService } from '../services/group.service'; +import { + Body, + Controller, + Get, + Post, + UseGuards, + Query, + Param, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddGroupDto } from '../dtos/add.group.dto'; +import { GetGroupDto } from '../dtos/get.group.dto'; + +@ApiTags('Group Module') +@Controller({ + version: '1', + path: 'group', +}) +export class GroupController { + constructor(private readonly groupService: GroupService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':homeId') + async userList( + @Param('homeId') homeId: string, + @Query() getGroupsDto: GetGroupDto, + ) { + try { + return await this.groupService.getGroupsByHomeId(homeId, getGroupsDto); + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addGroup(@Body() addGroupDto: AddGroupDto) { + try { + return await this.groupService.addGroup(addGroupDto); + } catch (err) { + throw new Error(err); + } + } +} diff --git a/src/group/controllers/index.ts b/src/group/controllers/index.ts new file mode 100644 index 0000000..daf2953 --- /dev/null +++ b/src/group/controllers/index.ts @@ -0,0 +1 @@ +export * from './group.controller'; diff --git a/src/group/dtos/add.group.dto.ts b/src/group/dtos/add.group.dto.ts new file mode 100644 index 0000000..b91f793 --- /dev/null +++ b/src/group/dtos/add.group.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; + +export class AddGroupDto { + @ApiProperty({ + description: 'groupName', + required: true, + }) + @IsString() + @IsNotEmpty() + public groupName: string; + + @ApiProperty({ + description: 'homeId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public homeId: string; + + @ApiProperty({ + description: 'productId', + required: true, + }) + @IsString() + @IsNotEmpty() + public productId: string; + + @ApiProperty({ + description: 'The list of up to 20 device IDs, separated with commas (,)', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceIds: string; +} diff --git a/src/group/dtos/get.group.dto.ts b/src/group/dtos/get.group.dto.ts new file mode 100644 index 0000000..16f8236 --- /dev/null +++ b/src/group/dtos/get.group.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumberString } from 'class-validator'; + +export class GetGroupDto { + @ApiProperty({ + description: 'pageSize', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public pageSize: number; + + @ApiProperty({ + description: 'pageNo', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public pageNo: number; +} diff --git a/src/group/dtos/index.ts b/src/group/dtos/index.ts new file mode 100644 index 0000000..61cffa2 --- /dev/null +++ b/src/group/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.group.dto'; diff --git a/src/group/group.module.ts b/src/group/group.module.ts new file mode 100644 index 0000000..3969d39 --- /dev/null +++ b/src/group/group.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { GroupService } from './services/group.service'; +import { GroupController } from './controllers/group.controller'; +import { ConfigModule } from '@nestjs/config'; +@Module({ + imports: [ConfigModule], + controllers: [GroupController], + providers: [GroupService], + exports: [GroupService], +}) +export class GroupModule {} diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts new file mode 100644 index 0000000..3e93b95 --- /dev/null +++ b/src/group/interfaces/get.group.interface.ts @@ -0,0 +1,20 @@ +export class GetRoomDetailsInterface { + result: { + id: string; + name: string; + }; +} +export class GetGroupsInterface { + result: { + count: number; + data_list: []; + }; +} + +export class addGroupInterface { + success: boolean; + msg: string; + result: { + id: string; + }; +} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts new file mode 100644 index 0000000..f05dc10 --- /dev/null +++ b/src/group/services/group.service.ts @@ -0,0 +1,124 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddGroupDto } from '../dtos'; +import { + GetGroupsInterface, + GetRoomDetailsInterface, + addGroupInterface, +} from '../interfaces/get.group.interface'; +import { GetGroupDto } from '../dtos/get.group.dto'; + +@Injectable() +export class GroupService { + private tuya: TuyaContext; + constructor(private readonly configService: ConfigService) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async getGroupsByHomeId(homeId: string, getGroupDto: GetGroupDto) { + try { + const response = await this.getGroupsTuya(homeId, getGroupDto); + + const groups = response.result.data_list.map((group: any) => ({ + groupId: group.id, + groupName: group.name, + })); + + return { + count: response.result.count, + groups: groups, + }; + } catch (error) { + throw new HttpException( + 'Error fetching groups', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getGroupsTuya( + homeId: string, + getGroupDto: GetGroupDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + space_id: homeId, + page_size: getGroupDto.pageSize, + page_no: getGroupDto.pageNo, + }, + }); + return response as unknown as GetGroupsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching groups ', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getRoomDetails(roomId: string): Promise { + // Added return type + try { + const path = `/v2.0/cloud/space/${roomId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as GetRoomDetailsInterface; // Cast response to RoomData + } catch (error) { + throw new HttpException( + 'Error fetching rooms details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addGroup(addGroupDto: AddGroupDto) { + const response = await this.addGroupTuya(addGroupDto); + + if (response.success) { + return { + success: true, + groupId: response.result.id, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async addGroupTuya(addGroupDto: AddGroupDto): Promise { + try { + const path = `/v2.0/cloud/thing/group`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: addGroupDto.homeId, + name: addGroupDto.groupName, + product_id: addGroupDto.productId, + device_ids: addGroupDto.deviceIds, + }, + }); + + return response as addGroupInterface; + } catch (error) { + throw new HttpException( + 'Error adding group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/group/services/index.ts b/src/group/services/index.ts new file mode 100644 index 0000000..ee9dd3f --- /dev/null +++ b/src/group/services/index.ts @@ -0,0 +1 @@ +export * from './group.service'; From 70020171c5fe00f3a3953e159cb23d392d7c8b5e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:36:11 +0300 Subject: [PATCH 063/259] finished control group api --- src/group/controllers/group.controller.ts | 12 +++++++ src/group/dtos/control.group.dto.ts | 20 +++++++++++ src/group/interfaces/get.group.interface.ts | 6 ++++ src/group/services/group.service.ts | 39 ++++++++++++++++++++- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/group/dtos/control.group.dto.ts diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 4cdb617..8430fe4 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -12,6 +12,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupDto } from '../dtos/get.group.dto'; +import { ControlGroupDto } from '../dtos/control.group.dto'; @ApiTags('Group Module') @Controller({ @@ -45,4 +46,15 @@ export class GroupController { throw new Error(err); } } + + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Post('control') + async controlGroup(@Body() controlGroupDto: ControlGroupDto) { + try { + return await this.groupService.controlGroup(controlGroupDto); + } catch (err) { + throw new Error(err); + } + } } diff --git a/src/group/dtos/control.group.dto.ts b/src/group/dtos/control.group.dto.ts new file mode 100644 index 0000000..36040b8 --- /dev/null +++ b/src/group/dtos/control.group.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsObject } from 'class-validator'; + +export class ControlGroupDto { + @ApiProperty({ + description: 'groupId', + required: true, + }) + @IsString() + @IsNotEmpty() + public groupId: string; + + @ApiProperty({ + description: 'example {"switch_1":true,"add_ele":300}', + required: true, + }) + @IsObject() + @IsNotEmpty() + public properties: object; +} diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 3e93b95..94dec62 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -18,3 +18,9 @@ export class addGroupInterface { id: string; }; } + +export class controlGroupInterface { + success: boolean; + result: boolean; + msg: string; +} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index f05dc10..286e21f 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -1,13 +1,15 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { AddGroupDto } from '../dtos'; +import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupsInterface, GetRoomDetailsInterface, addGroupInterface, + controlGroupInterface, } from '../interfaces/get.group.interface'; import { GetGroupDto } from '../dtos/get.group.dto'; +import { ControlGroupDto } from '../dtos/control.group.dto'; @Injectable() export class GroupService { @@ -121,4 +123,39 @@ export class GroupService { ); } } + + async controlGroup(controlGroupDto: ControlGroupDto) { + const response = await this.controlGroupTuya(controlGroupDto); + + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async controlGroupTuya( + controlGroupDto: ControlGroupDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group/properties`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + group_id: controlGroupDto.groupId, + properties: controlGroupDto.properties, + }, + }); + + return response as controlGroupInterface; + } catch (error) { + throw new HttpException( + 'Error control group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From db22ceba17b5440b9387a0f5ee3c18a1aac6f9b4 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:36:38 +0300 Subject: [PATCH 064/259] remove comments --- src/group/controllers/group.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 8430fe4..a37abc4 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -47,8 +47,8 @@ export class GroupController { } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post('control') async controlGroup(@Body() controlGroupDto: ControlGroupDto) { try { From 1f5789db1b88b590e6f92f94ed67ad11d1392347 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:49:41 +0300 Subject: [PATCH 065/259] finished rename group api --- src/group/controllers/group.controller.ts | 13 ++++++++ src/group/dtos/control.group.dto.ts | 4 +-- src/group/dtos/rename.group.dto copy.ts | 20 +++++++++++++ src/group/interfaces/get.group.interface.ts | 6 ++++ src/group/services/group.service.ts | 33 +++++++++++++++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/group/dtos/rename.group.dto copy.ts diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index a37abc4..1db7a11 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -7,12 +7,14 @@ import { UseGuards, Query, Param, + Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; +import { RenameGroupDto } from '../dtos/rename.group.dto copy'; @ApiTags('Group Module') @Controller({ @@ -57,4 +59,15 @@ export class GroupController { throw new Error(err); } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename') + async renameGroup(@Body() renameGroupDto: RenameGroupDto) { + try { + return await this.groupService.renameGroup(renameGroupDto); + } catch (err) { + throw new Error(err); + } + } } diff --git a/src/group/dtos/control.group.dto.ts b/src/group/dtos/control.group.dto.ts index 36040b8..33a6870 100644 --- a/src/group/dtos/control.group.dto.ts +++ b/src/group/dtos/control.group.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsObject } from 'class-validator'; +import { IsNotEmpty, IsObject, IsNumberString } from 'class-validator'; export class ControlGroupDto { @ApiProperty({ description: 'groupId', required: true, }) - @IsString() + @IsNumberString() @IsNotEmpty() public groupId: string; diff --git a/src/group/dtos/rename.group.dto copy.ts b/src/group/dtos/rename.group.dto copy.ts new file mode 100644 index 0000000..a85f41b --- /dev/null +++ b/src/group/dtos/rename.group.dto copy.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; + +export class RenameGroupDto { + @ApiProperty({ + description: 'groupId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public groupId: string; + + @ApiProperty({ + description: 'groupName', + required: true, + }) + @IsString() + @IsNotEmpty() + public groupName: string; +} diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 94dec62..69c5050 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -24,3 +24,9 @@ export class controlGroupInterface { result: boolean; msg: string; } + +export class renameGroupInterface { + success: boolean; + result: boolean; + msg: string; +} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 286e21f..c534d39 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -7,9 +7,11 @@ import { GetRoomDetailsInterface, addGroupInterface, controlGroupInterface, + renameGroupInterface, } from '../interfaces/get.group.interface'; import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; +import { RenameGroupDto } from '../dtos/rename.group.dto copy'; @Injectable() export class GroupService { @@ -158,4 +160,35 @@ export class GroupService { ); } } + + async renameGroup(renameGroupDto: RenameGroupDto) { + const response = await this.renameGroupTuya(renameGroupDto); + + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async renameGroupTuya( + renameGroupDto: RenameGroupDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group/${renameGroupDto.groupId}/${renameGroupDto.groupName}`; + const response = await this.tuya.request({ + method: 'PUT', + path, + }); + + return response as renameGroupInterface; + } catch (error) { + throw new HttpException( + 'Error control group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From 4cdba753673f3e0e7178f29005d886d614b257fe Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:11:23 +0300 Subject: [PATCH 066/259] finished delete group api --- src/group/controllers/group.controller.ts | 12 ++++++ src/group/interfaces/get.group.interface.ts | 6 --- src/group/services/group.service.ts | 46 ++++++++++++++++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 1db7a11..7b5b12d 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -8,6 +8,7 @@ import { Query, Param, Put, + Delete, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; @@ -70,4 +71,15 @@ export class GroupController { throw new Error(err); } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':groupId') + async deleteGroup(@Param('groupId') groupId: number) { + try { + return await this.groupService.deleteGroup(groupId); + } catch (err) { + throw new Error(err); + } + } } diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 69c5050..94dec62 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -24,9 +24,3 @@ export class controlGroupInterface { result: boolean; msg: string; } - -export class renameGroupInterface { - success: boolean; - result: boolean; - msg: string; -} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index c534d39..48cadeb 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -7,7 +7,6 @@ import { GetRoomDetailsInterface, addGroupInterface, controlGroupInterface, - renameGroupInterface, } from '../interfaces/get.group.interface'; import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; @@ -165,7 +164,11 @@ export class GroupService { const response = await this.renameGroupTuya(renameGroupDto); if (response.success) { - return response; + return { + success: response.success, + result: response.result, + msg: response.msg, + }; } else { throw new HttpException( response.msg || 'Unknown error', @@ -175,7 +178,7 @@ export class GroupService { } async renameGroupTuya( renameGroupDto: RenameGroupDto, - ): Promise { + ): Promise { try { const path = `/v2.0/cloud/thing/group/${renameGroupDto.groupId}/${renameGroupDto.groupName}`; const response = await this.tuya.request({ @@ -183,10 +186,43 @@ export class GroupService { path, }); - return response as renameGroupInterface; + return response as controlGroupInterface; } catch (error) { throw new HttpException( - 'Error control group', + 'Error rename group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async deleteGroup(groupId: number) { + const response = await this.deleteGroupTuya(groupId); + + if (response.success) { + return { + success: response.success, + result: response.result, + msg: response.msg, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async deleteGroupTuya(groupId: number): Promise { + try { + const path = `/v2.0/cloud/thing/group/${groupId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as controlGroupInterface; + } catch (error) { + throw new HttpException( + 'Error delete group', HttpStatus.INTERNAL_SERVER_ERROR, ); } From db7802776a47d3ad734a6c5eb0d8f2a5605f6f7f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:03:06 +0300 Subject: [PATCH 067/259] add get group by id api --- src/group/controllers/group.controller.ts | 20 ++++--- src/group/dtos/get.group.dto.ts | 8 +++ src/group/interfaces/get.group.interface.ts | 2 +- src/group/services/group.service.ts | 63 +++++++++++++-------- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 7b5b12d..be0c7a3 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -27,18 +27,24 @@ export class GroupController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':homeId') - async userList( - @Param('homeId') homeId: string, - @Query() getGroupsDto: GetGroupDto, - ) { + @Get() + async getGroupsByHomeId(@Query() getGroupsDto: GetGroupDto) { try { - return await this.groupService.getGroupsByHomeId(homeId, getGroupsDto); + return await this.groupService.getGroupsByHomeId(getGroupsDto); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':groupId') + async getGroupsByGroupId(@Param('groupId') groupId: number) { + try { + return await this.groupService.getGroupsByGroupId(groupId); } catch (err) { throw new Error(err); } } - @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() diff --git a/src/group/dtos/get.group.dto.ts b/src/group/dtos/get.group.dto.ts index 16f8236..aad234b 100644 --- a/src/group/dtos/get.group.dto.ts +++ b/src/group/dtos/get.group.dto.ts @@ -2,6 +2,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsNumberString } from 'class-validator'; export class GetGroupDto { + @ApiProperty({ + description: 'homeId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public homeId: string; + @ApiProperty({ description: 'pageSize', required: true, diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 94dec62..970c343 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -1,4 +1,4 @@ -export class GetRoomDetailsInterface { +export class GetGroupDetailsInterface { result: { id: string; name: string; diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 48cadeb..07fbacf 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -3,8 +3,8 @@ import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddGroupDto } from '../dtos/add.group.dto'; import { + GetGroupDetailsInterface, GetGroupsInterface, - GetRoomDetailsInterface, addGroupInterface, controlGroupInterface, } from '../interfaces/get.group.interface'; @@ -26,9 +26,9 @@ export class GroupService { }); } - async getGroupsByHomeId(homeId: string, getGroupDto: GetGroupDto) { + async getGroupsByHomeId(getGroupDto: GetGroupDto) { try { - const response = await this.getGroupsTuya(homeId, getGroupDto); + const response = await this.getGroupsTuya(getGroupDto); const groups = response.result.data_list.map((group: any) => ({ groupId: group.id, @@ -47,17 +47,14 @@ export class GroupService { } } - async getGroupsTuya( - homeId: string, - getGroupDto: GetGroupDto, - ): Promise { + async getGroupsTuya(getGroupDto: GetGroupDto): Promise { try { const path = `/v2.0/cloud/thing/group`; const response = await this.tuya.request({ method: 'GET', path, query: { - space_id: homeId, + space_id: getGroupDto.homeId, page_size: getGroupDto.pageSize, page_no: getGroupDto.pageNo, }, @@ -70,23 +67,7 @@ export class GroupService { ); } } - async getRoomDetails(roomId: string): Promise { - // Added return type - try { - const path = `/v2.0/cloud/space/${roomId}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - return response as GetRoomDetailsInterface; // Cast response to RoomData - } catch (error) { - throw new HttpException( - 'Error fetching rooms details', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async addGroup(addGroupDto: AddGroupDto) { const response = await this.addGroupTuya(addGroupDto); @@ -227,4 +208,38 @@ export class GroupService { ); } } + + async getGroupsByGroupId(groupId: number) { + try { + const response = await this.getGroupsByGroupIdTuya(groupId); + + return { + groupId: response.result.id, + groupName: response.result.name, + }; + } catch (error) { + throw new HttpException( + 'Error fetching group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getGroupsByGroupIdTuya( + groupId: number, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group/${groupId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + return response as GetGroupDetailsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching group ', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From dc51cbb5ef11ede7631aafc6f44278d1db6b1ded Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:29:25 +0300 Subject: [PATCH 068/259] finshed get room by id api --- src/room/controllers/room.controller.ts | 25 +++++++++++++++++++---- src/room/interfaces/get.room.interface.ts | 1 + src/room/services/room.service.ts | 16 +++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 2545254..b90210c 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -1,5 +1,13 @@ import { RoomService } from '../services/room.service'; -import { Body, Controller, Get, Post, Param, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + UseGuards, + Query, + Param, +} from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto } from '../dtos/add.room.dto'; @@ -14,15 +22,24 @@ export class RoomController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':homeId') - async userList(@Param('homeId') homeId: string) { + @Get() + async getRoomsByHomeId(@Query('homeId') homeId: string) { try { return await this.roomService.getRoomsByHomeId(homeId); } catch (err) { throw new Error(err); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':roomId') + async getRoomsByRoomId(@Param('roomId') roomId: string) { + try { + return await this.roomService.getRoomsByRoomId(roomId); + } catch (err) { + throw new Error(err); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() diff --git a/src/room/interfaces/get.room.interface.ts b/src/room/interfaces/get.room.interface.ts index 2d000a5..56c0d49 100644 --- a/src/room/interfaces/get.room.interface.ts +++ b/src/room/interfaces/get.room.interface.ts @@ -2,6 +2,7 @@ export class GetRoomDetailsInterface { result: { id: string; name: string; + root_id: string; }; } export class GetRoomsIdsInterface { diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 0bc5b18..095e8df 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -96,4 +96,20 @@ export class RoomService { ); } } + async getRoomsByRoomId(roomId: string) { + try { + const response = await this.getRoomDetails(roomId); + + return { + homeId: response.result.root_id, + roomId: response.result.id, + roomName: response.result.name, + }; + } catch (error) { + throw new HttpException( + 'Error fetching rooms', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From e011949bdab6b0c939d0a1d275c18cbe92e47ae9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:40:16 +0300 Subject: [PATCH 069/259] finshed get home by id api --- src/home/controllers/home.controller.ts | 24 ++++++++++++++--- src/home/interfaces/get.home.interface.ts | 6 +++++ src/home/services/home.service.ts | 32 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/home/interfaces/get.home.interface.ts diff --git a/src/home/controllers/home.controller.ts b/src/home/controllers/home.controller.ts index 6b4eaa4..c8aeb4b 100644 --- a/src/home/controllers/home.controller.ts +++ b/src/home/controllers/home.controller.ts @@ -1,5 +1,13 @@ import { HomeService } from './../services/home.service'; -import { Body, Controller, Get, Post, Param, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Param, + UseGuards, + Query, +} from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddHomeDto } from '../dtos/add.home.dto'; @@ -14,14 +22,24 @@ export class HomeController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':userUuid') - async userList(@Param('userUuid') userUuid: string) { + @Get() + async getHomesByUserId(@Query('userUuid') userUuid: string) { try { return await this.homeService.getHomesByUserId(userUuid); } catch (err) { throw new Error(err); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':homeId') + async getHomesByHomeId(@Param('homeId') homeId: string) { + try { + return await this.homeService.getHomeByHomeId(homeId); + } catch (err) { + throw new Error(err); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) diff --git a/src/home/interfaces/get.home.interface.ts b/src/home/interfaces/get.home.interface.ts new file mode 100644 index 0000000..c7015f8 --- /dev/null +++ b/src/home/interfaces/get.home.interface.ts @@ -0,0 +1,6 @@ +export class GetHomeDetailsInterface { + result: { + id: string; + name: string; + }; +} diff --git a/src/home/services/home.service.ts b/src/home/services/home.service.ts index 74da59b..f1ad1f4 100644 --- a/src/home/services/home.service.ts +++ b/src/home/services/home.service.ts @@ -4,6 +4,7 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddHomeDto } from '../dtos'; +import { GetHomeDetailsInterface } from '../interfaces/get.home.interface'; @Injectable() export class HomeService { @@ -78,4 +79,35 @@ export class HomeService { ); } } + async getHomeDetails(homeId: string): Promise { + try { + const path = `/v2.0/cloud/space/${homeId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as GetHomeDetailsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching home details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getHomeByHomeId(homeId: string) { + try { + const response = await this.getHomeDetails(homeId); + + return { + homeId: response.result.id, + homeName: response.result.name, + }; + } catch (error) { + throw new HttpException( + 'Error fetching home', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From 5f18d1f4d5c93aeb526f03c1becf6feedc450f95 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:17:22 +0300 Subject: [PATCH 070/259] finshed get devices by room id --- src/app.module.ts | 2 + src/device/controllers/device.controller.ts | 91 +++++++ src/device/controllers/index.ts | 1 + src/device/device.module.ts | 11 + src/device/dtos/add.device.dto.ts | 36 +++ src/device/dtos/control.device.dto.ts | 20 ++ src/device/dtos/get.device.dto.ts | 20 ++ src/device/dtos/index.ts | 4 + src/device/dtos/rename.device.dto copy.ts | 20 ++ src/device/interfaces/get.device.interface.ts | 25 ++ src/device/services/device.service.ts | 242 ++++++++++++++++++ src/device/services/index.ts | 1 + 12 files changed, 473 insertions(+) create mode 100644 src/device/controllers/device.controller.ts create mode 100644 src/device/controllers/index.ts create mode 100644 src/device/device.module.ts create mode 100644 src/device/dtos/add.device.dto.ts create mode 100644 src/device/dtos/control.device.dto.ts create mode 100644 src/device/dtos/get.device.dto.ts create mode 100644 src/device/dtos/index.ts create mode 100644 src/device/dtos/rename.device.dto copy.ts create mode 100644 src/device/interfaces/get.device.interface.ts create mode 100644 src/device/services/device.service.ts create mode 100644 src/device/services/index.ts diff --git a/src/app.module.ts b/src/app.module.ts index b890f1a..0af9133 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AuthenticationController } from './auth/controllers/authentication.cont import { UserModule } from './users/user.module'; import { HomeModule } from './home/home.module'; import { RoomModule } from './room/room.module'; +import { DeviceModule } from './device/device.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -15,6 +16,7 @@ import { RoomModule } from './room/room.module'; UserModule, HomeModule, RoomModule, + DeviceModule, ], controllers: [AuthenticationController], }) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts new file mode 100644 index 0000000..ffedef3 --- /dev/null +++ b/src/device/controllers/device.controller.ts @@ -0,0 +1,91 @@ +import { DeviceService } from '../services/device.service'; +import { + Body, + Controller, + Get, + Post, + UseGuards, + Query, + Param, + Put, + Delete, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddDeviceDto } from '../dtos/add.device.dto'; +import { GetDeviceDto } from '../dtos/get.device.dto'; +import { ControlDeviceDto } from '../dtos/control.device.dto'; +import { RenameDeviceDto } from '../dtos/rename.device.dto copy'; + +@ApiTags('Device Module') +@Controller({ + version: '1', + path: 'device', +}) +export class DeviceController { + constructor(private readonly deviceService: DeviceService) {} + + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Get('room') + async getDevicesByRoomId(@Query() getDevicesDto: GetDeviceDto) { + try { + return await this.deviceService.getDevicesByRoomId(getDevicesDto); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceId') + async getDevicesByDeviceId(@Param('deviceId') deviceId: number) { + try { + return await this.deviceService.getDevicesByDeviceId(deviceId); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addDevice(@Body() addDeviceDto: AddDeviceDto) { + try { + return await this.deviceService.addDevice(addDeviceDto); + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('control') + async controlDevice(@Body() controlDeviceDto: ControlDeviceDto) { + try { + return await this.deviceService.controlDevice(controlDeviceDto); + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename') + async renameDevice(@Body() renameDeviceDto: RenameDeviceDto) { + try { + return await this.deviceService.renameDevice(renameDeviceDto); + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':deviceId') + async deleteDevice(@Param('deviceId') deviceId: number) { + try { + return await this.deviceService.deleteDevice(deviceId); + } catch (err) { + throw new Error(err); + } + } +} diff --git a/src/device/controllers/index.ts b/src/device/controllers/index.ts new file mode 100644 index 0000000..8afa190 --- /dev/null +++ b/src/device/controllers/index.ts @@ -0,0 +1 @@ +export * from './device.controller'; diff --git a/src/device/device.module.ts b/src/device/device.module.ts new file mode 100644 index 0000000..8bac0cf --- /dev/null +++ b/src/device/device.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DeviceService } from './services/device.service'; +import { DeviceController } from './controllers/device.controller'; +import { ConfigModule } from '@nestjs/config'; +@Module({ + imports: [ConfigModule], + controllers: [DeviceController], + providers: [DeviceService], + exports: [DeviceService], +}) +export class DeviceModule {} diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts new file mode 100644 index 0000000..bdb7eb6 --- /dev/null +++ b/src/device/dtos/add.device.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; + +export class AddDeviceDto { + @ApiProperty({ + description: 'deviceName', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceName: string; + + @ApiProperty({ + description: 'homeId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public homeId: string; + + @ApiProperty({ + description: 'productId', + required: true, + }) + @IsString() + @IsNotEmpty() + public productId: string; + + @ApiProperty({ + description: 'The list of up to 20 device IDs, separated with commas (,)', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceIds: string; +} diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts new file mode 100644 index 0000000..1164973 --- /dev/null +++ b/src/device/dtos/control.device.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsObject, IsNumberString } from 'class-validator'; + +export class ControlDeviceDto { + @ApiProperty({ + description: 'deviceId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public deviceId: string; + + @ApiProperty({ + description: 'example {"switch_1":true,"add_ele":300}', + required: true, + }) + @IsObject() + @IsNotEmpty() + public properties: object; +} diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts new file mode 100644 index 0000000..217ab46 --- /dev/null +++ b/src/device/dtos/get.device.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumberString } from 'class-validator'; + +export class GetDeviceDto { + @ApiProperty({ + description: 'roomId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public roomId: string; + + @ApiProperty({ + description: 'pageSize', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public pageSize: number; +} diff --git a/src/device/dtos/index.ts b/src/device/dtos/index.ts new file mode 100644 index 0000000..3409fea --- /dev/null +++ b/src/device/dtos/index.ts @@ -0,0 +1,4 @@ +export * from './add.device.dto'; +export * from './control.device.dto'; +export * from './get.device.dto'; +export * from './rename.device.dto copy'; diff --git a/src/device/dtos/rename.device.dto copy.ts b/src/device/dtos/rename.device.dto copy.ts new file mode 100644 index 0000000..e32a6bb --- /dev/null +++ b/src/device/dtos/rename.device.dto copy.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; + +export class RenameDeviceDto { + @ApiProperty({ + description: 'deviceId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public deviceId: string; + + @ApiProperty({ + description: 'deviceName', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceName: string; +} diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts new file mode 100644 index 0000000..75a2145 --- /dev/null +++ b/src/device/interfaces/get.device.interface.ts @@ -0,0 +1,25 @@ +export class GetDeviceDetailsInterface { + result: { + id: string; + name: string; + }; +} +export class GetDevicesInterface { + success: boolean; + msg: string; + result: []; +} + +export class addDeviceInterface { + success: boolean; + msg: string; + result: { + id: string; + }; +} + +export class controlDeviceInterface { + success: boolean; + result: boolean; + msg: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts new file mode 100644 index 0000000..9a80bfb --- /dev/null +++ b/src/device/services/device.service.ts @@ -0,0 +1,242 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddDeviceDto } from '../dtos/add.device.dto'; +import { + GetDeviceDetailsInterface, + GetDevicesInterface, + addDeviceInterface, + controlDeviceInterface, +} from '../interfaces/get.device.interface'; +import { GetDeviceDto } from '../dtos/get.device.dto'; +import { ControlDeviceDto } from '../dtos/control.device.dto'; +import { RenameDeviceDto } from '../dtos/rename.device.dto copy'; + +@Injectable() +export class DeviceService { + private tuya: TuyaContext; + constructor(private readonly configService: ConfigService) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async getDevicesByRoomId(getDeviceDto: GetDeviceDto) { + try { + const response = await this.getDevicesTuya(getDeviceDto); + + return { + success: response.success, + devices: response.result, + msg: response.msg, + }; + } catch (error) { + throw new HttpException( + 'Error fetching devices', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesTuya( + getDeviceDto: GetDeviceDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/space/device`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + space_ids: getDeviceDto.roomId, + page_size: getDeviceDto.pageSize, + }, + }); + return response as unknown as GetDevicesInterface; + } catch (error) { + throw new HttpException( + 'Error fetching devices ', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addDevice(addDeviceDto: AddDeviceDto) { + const response = await this.addDeviceTuya(addDeviceDto); + + if (response.success) { + return { + success: true, + deviceId: response.result.id, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async addDeviceTuya(addDeviceDto: AddDeviceDto): Promise { + try { + const path = `/v2.0/cloud/thing/device`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: addDeviceDto.homeId, + name: addDeviceDto.deviceName, + product_id: addDeviceDto.productId, + device_ids: addDeviceDto.deviceIds, + }, + }); + + return response as addDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error adding device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async controlDevice(controlDeviceDto: ControlDeviceDto) { + const response = await this.controlDeviceTuya(controlDeviceDto); + + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async controlDeviceTuya( + controlDeviceDto: ControlDeviceDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/device/properties`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + device_id: controlDeviceDto.deviceId, + properties: controlDeviceDto.properties, + }, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error control device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async renameDevice(renameDeviceDto: RenameDeviceDto) { + const response = await this.renameDeviceTuya(renameDeviceDto); + + if (response.success) { + return { + success: response.success, + result: response.result, + msg: response.msg, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async renameDeviceTuya( + renameDeviceDto: RenameDeviceDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/device/${renameDeviceDto.deviceId}/${renameDeviceDto.deviceName}`; + const response = await this.tuya.request({ + method: 'PUT', + path, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error rename device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async deleteDevice(deviceId: number) { + const response = await this.deleteDeviceTuya(deviceId); + + if (response.success) { + return { + success: response.success, + result: response.result, + msg: response.msg, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async deleteDeviceTuya(deviceId: number): Promise { + try { + const path = `/v2.0/cloud/thing/device/${deviceId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error delete device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesByDeviceId(deviceId: number) { + try { + const response = await this.getDevicesByDeviceIdTuya(deviceId); + + return { + deviceId: response.result.id, + deviceName: response.result.name, + }; + } catch (error) { + throw new HttpException( + 'Error fetching device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesByDeviceIdTuya( + deviceId: number, + ): Promise { + try { + const path = `/v2.0/cloud/thing/device/${deviceId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + return response as GetDeviceDetailsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching device ', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/device/services/index.ts b/src/device/services/index.ts new file mode 100644 index 0000000..9101752 --- /dev/null +++ b/src/device/services/index.ts @@ -0,0 +1 @@ +export * from './device.service'; From 1bd30ebb33ead5c7973f3686b1404b7045b03a86 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:57:04 +0300 Subject: [PATCH 071/259] finshed device endpoint --- src/device/controllers/device.controller.ts | 98 +++--- src/device/dtos/add.device.dto.ts | 37 ++- src/device/dtos/control.device.dto.ts | 16 +- src/device/dtos/get.device.dto.ts | 26 +- src/device/dtos/index.ts | 1 - src/device/dtos/rename.device.dto copy.ts | 20 -- src/device/interfaces/get.device.interface.ts | 33 +- src/device/services/device.service.ts | 296 ++++++++++++------ 8 files changed, 343 insertions(+), 184 deletions(-) delete mode 100644 src/device/dtos/rename.device.dto copy.ts diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index ffedef3..936b8e1 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -7,15 +7,18 @@ import { UseGuards, Query, Param, - Put, - Delete, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddDeviceDto } from '../dtos/add.device.dto'; -import { GetDeviceDto } from '../dtos/get.device.dto'; +import { + AddDeviceInGroupDto, + AddDeviceInRoomDto, +} from '../dtos/add.device.dto'; +import { + GetDeviceByGroupIdDto, + GetDeviceByRoomIdDto, +} from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; -import { RenameDeviceDto } from '../dtos/rename.device.dto copy'; @ApiTags('Device Module') @Controller({ @@ -25,12 +28,28 @@ import { RenameDeviceDto } from '../dtos/rename.device.dto copy'; export class DeviceController { constructor(private readonly deviceService: DeviceService) {} - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('room') - async getDevicesByRoomId(@Query() getDevicesDto: GetDeviceDto) { + async getDevicesByRoomId( + @Query() getDeviceByRoomIdDto: GetDeviceByRoomIdDto, + ) { try { - return await this.deviceService.getDevicesByRoomId(getDevicesDto); + return await this.deviceService.getDevicesByRoomId(getDeviceByRoomIdDto); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('group') + async getDevicesByGroupId( + @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, + ) { + try { + return await this.deviceService.getDevicesByGroupId( + getDeviceByGroupIdDto, + ); } catch (err) { throw new Error(err); } @@ -38,7 +57,7 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceId') - async getDevicesByDeviceId(@Param('deviceId') deviceId: number) { + async getDevicesByDeviceId(@Param('deviceId') deviceId: string) { try { return await this.deviceService.getDevicesByDeviceId(deviceId); } catch (err) { @@ -47,15 +66,44 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Post() - async addDevice(@Body() addDeviceDto: AddDeviceDto) { + @Get(':deviceId/functions') + async getDevicesInstructionByDeviceId(@Param('deviceId') deviceId: string) { try { - return await this.deviceService.addDevice(addDeviceDto); + return await this.deviceService.getDevicesInstructionByDeviceId(deviceId); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceId/functions/status') + async getDevicesInstructionStatus(@Param('deviceId') deviceId: string) { + try { + return await this.deviceService.getDevicesInstructionStatus(deviceId); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('room') + async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { + try { + return await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('group') + async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { + try { + return await this.deviceService.addDeviceInGroup(addDeviceInGroupDto); } catch (err) { throw new Error(err); } } - @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('control') @@ -66,26 +114,4 @@ export class DeviceController { throw new Error(err); } } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put('rename') - async renameDevice(@Body() renameDeviceDto: RenameDeviceDto) { - try { - return await this.deviceService.renameDevice(renameDeviceDto); - } catch (err) { - throw new Error(err); - } - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Delete(':deviceId') - async deleteDevice(@Param('deviceId') deviceId: number) { - try { - return await this.deviceService.deleteDevice(deviceId); - } catch (err) { - throw new Error(err); - } - } } diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index bdb7eb6..8b1dfe5 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,14 +1,31 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; -export class AddDeviceDto { +export class AddDeviceInRoomDto { @ApiProperty({ - description: 'deviceName', + description: 'deviceId', required: true, }) @IsString() @IsNotEmpty() - public deviceName: string; + public deviceId: string; + + @ApiProperty({ + description: 'roomId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public roomId: string; +} +export class AddDeviceInGroupDto { + @ApiProperty({ + description: 'deviceId', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceId: string; @ApiProperty({ description: 'homeId', @@ -19,18 +36,10 @@ export class AddDeviceDto { public homeId: string; @ApiProperty({ - description: 'productId', + description: 'groupId', required: true, }) - @IsString() + @IsNumberString() @IsNotEmpty() - public productId: string; - - @ApiProperty({ - description: 'The list of up to 20 device IDs, separated with commas (,)', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceIds: string; + public groupId: string; } diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index 1164973..660cdaf 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -1,20 +1,26 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsObject, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class ControlDeviceDto { @ApiProperty({ description: 'deviceId', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() public deviceId: string; @ApiProperty({ - description: 'example {"switch_1":true,"add_ele":300}', + description: 'code', required: true, }) - @IsObject() + @IsString() @IsNotEmpty() - public properties: object; + public code: string; + @ApiProperty({ + description: 'value', + required: true, + }) + @IsNotEmpty() + public value: any; } diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 217ab46..d49a714 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsNumberString } from 'class-validator'; -export class GetDeviceDto { +export class GetDeviceByRoomIdDto { @ApiProperty({ description: 'roomId', required: true, @@ -18,3 +18,27 @@ export class GetDeviceDto { @IsNotEmpty() public pageSize: number; } +export class GetDeviceByGroupIdDto { + @ApiProperty({ + description: 'groupId', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public groupId: string; + + @ApiProperty({ + description: 'pageSize', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public pageSize: number; + @ApiProperty({ + description: 'pageNo', + required: true, + }) + @IsNumberString() + @IsNotEmpty() + public pageNo: number; +} diff --git a/src/device/dtos/index.ts b/src/device/dtos/index.ts index 3409fea..1a0b6b3 100644 --- a/src/device/dtos/index.ts +++ b/src/device/dtos/index.ts @@ -1,4 +1,3 @@ export * from './add.device.dto'; export * from './control.device.dto'; export * from './get.device.dto'; -export * from './rename.device.dto copy'; diff --git a/src/device/dtos/rename.device.dto copy.ts b/src/device/dtos/rename.device.dto copy.ts deleted file mode 100644 index e32a6bb..0000000 --- a/src/device/dtos/rename.device.dto copy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; - -export class RenameDeviceDto { - @ApiProperty({ - description: 'deviceId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public deviceId: string; - - @ApiProperty({ - description: 'deviceName', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceName: string; -} diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index 75a2145..e6f1361 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -1,25 +1,44 @@ export class GetDeviceDetailsInterface { - result: { - id: string; - name: string; - }; + result: object; + success: boolean; + msg: string; } -export class GetDevicesInterface { +export class GetDevicesByRoomIdInterface { success: boolean; msg: string; result: []; } -export class addDeviceInterface { +export class GetDevicesByGroupIdInterface { success: boolean; msg: string; result: { - id: string; + count: number; + data_list: []; }; } +export class addDeviceInRoomInterface { + success: boolean; + msg: string; + result: boolean; +} + export class controlDeviceInterface { success: boolean; result: boolean; msg: string; } +export class GetDeviceDetailsFunctionsInterface { + result: { + category: string; + functions: []; + }; + success: boolean; + msg: string; +} +export class GetDeviceDetailsFunctionsStatusInterface { + result: [{ id: string; status: [] }]; + success: boolean; + msg: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 9a80bfb..ce13b30 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1,16 +1,24 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { AddDeviceDto } from '../dtos/add.device.dto'; import { + AddDeviceInGroupDto, + AddDeviceInRoomDto, +} from '../dtos/add.device.dto'; +import { + GetDeviceDetailsFunctionsInterface, + GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, - GetDevicesInterface, - addDeviceInterface, + GetDevicesByGroupIdInterface, + GetDevicesByRoomIdInterface, + addDeviceInRoomInterface, controlDeviceInterface, } from '../interfaces/get.device.interface'; -import { GetDeviceDto } from '../dtos/get.device.dto'; +import { + GetDeviceByGroupIdDto, + GetDeviceByRoomIdDto, +} from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; -import { RenameDeviceDto } from '../dtos/rename.device.dto copy'; @Injectable() export class DeviceService { @@ -26,9 +34,9 @@ export class DeviceService { }); } - async getDevicesByRoomId(getDeviceDto: GetDeviceDto) { + async getDevicesByRoomId(getDeviceByRoomIdDto: GetDeviceByRoomIdDto) { try { - const response = await this.getDevicesTuya(getDeviceDto); + const response = await this.getDevicesByRoomIdTuya(getDeviceByRoomIdDto); return { success: response.success, @@ -43,20 +51,62 @@ export class DeviceService { } } - async getDevicesTuya( - getDeviceDto: GetDeviceDto, - ): Promise { + async getDevicesByRoomIdTuya( + getDeviceByRoomIdDto: GetDeviceByRoomIdDto, + ): Promise { try { const path = `/v2.0/cloud/thing/space/device`; const response = await this.tuya.request({ method: 'GET', path, query: { - space_ids: getDeviceDto.roomId, - page_size: getDeviceDto.pageSize, + space_ids: getDeviceByRoomIdDto.roomId, + page_size: getDeviceByRoomIdDto.pageSize, }, }); - return response as unknown as GetDevicesInterface; + return response as GetDevicesByRoomIdInterface; + } catch (error) { + throw new HttpException( + 'Error fetching devices ', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDevicesByGroupId(getDeviceByGroupIdDto: GetDeviceByGroupIdDto) { + try { + const response = await this.getDevicesByGroupIdTuya( + getDeviceByGroupIdDto, + ); + + return { + success: response.success, + devices: response.result.data_list, + count: response.result.count, + msg: response.msg, + }; + } catch (error) { + throw new HttpException( + 'Error fetching devices', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesByGroupIdTuya( + getDeviceByGroupIdDto: GetDeviceByGroupIdDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + space_id: getDeviceByGroupIdDto.groupId, + page_size: getDeviceByGroupIdDto.pageSize, + page_no: getDeviceByGroupIdDto.pageNo, + }, + }); + return response as GetDevicesByGroupIdInterface; } catch (error) { throw new HttpException( 'Error fetching devices ', @@ -65,13 +115,14 @@ export class DeviceService { } } - async addDevice(addDeviceDto: AddDeviceDto) { - const response = await this.addDeviceTuya(addDeviceDto); + async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { + const response = await this.addDeviceInRoomTuya(addDeviceInRoomDto); if (response.success) { return { - success: true, - deviceId: response.result.id, + success: response.success, + result: response.result, + msg: response.msg, }; } else { throw new HttpException( @@ -80,21 +131,58 @@ export class DeviceService { ); } } - async addDeviceTuya(addDeviceDto: AddDeviceDto): Promise { + async addDeviceInRoomTuya( + addDeviceInRoomDto: AddDeviceInRoomDto, + ): Promise { try { - const path = `/v2.0/cloud/thing/device`; + const path = `/v2.0/cloud/thing/${addDeviceInRoomDto.deviceId}/transfer`; const response = await this.tuya.request({ method: 'POST', path, body: { - space_id: addDeviceDto.homeId, - name: addDeviceDto.deviceName, - product_id: addDeviceDto.productId, - device_ids: addDeviceDto.deviceIds, + space_id: addDeviceInRoomDto.roomId, }, }); - return response as addDeviceInterface; + return response as addDeviceInRoomInterface; + } catch (error) { + throw new HttpException( + 'Error adding device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { + const response = await this.addDeviceInGroupTuya(addDeviceInGroupDto); + + if (response.success) { + return { + success: response.success, + result: response.result, + msg: response.msg, + }; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } + async addDeviceInGroupTuya( + addDeviceInGroupDto: AddDeviceInGroupDto, + ): Promise { + try { + const path = `/v2.0/cloud/thing/group/${addDeviceInGroupDto.groupId}/device`; + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + space_id: addDeviceInGroupDto.homeId, + device_ids: addDeviceInGroupDto.deviceId, + }, + }); + + return response as addDeviceInRoomInterface; } catch (error) { throw new HttpException( 'Error adding device', @@ -119,13 +207,14 @@ export class DeviceService { controlDeviceDto: ControlDeviceDto, ): Promise { try { - const path = `/v2.0/cloud/thing/device/properties`; + const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceId}/commands`; const response = await this.tuya.request({ method: 'POST', path, body: { - device_id: controlDeviceDto.deviceId, - properties: controlDeviceDto.properties, + commands: [ + { code: controlDeviceDto.code, value: controlDeviceDto.value }, + ], }, }); @@ -138,81 +227,14 @@ export class DeviceService { } } - async renameDevice(renameDeviceDto: RenameDeviceDto) { - const response = await this.renameDeviceTuya(renameDeviceDto); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async renameDeviceTuya( - renameDeviceDto: RenameDeviceDto, - ): Promise { - try { - const path = `/v2.0/cloud/thing/device/${renameDeviceDto.deviceId}/${renameDeviceDto.deviceName}`; - const response = await this.tuya.request({ - method: 'PUT', - path, - }); - - return response as controlDeviceInterface; - } catch (error) { - throw new HttpException( - 'Error rename device', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async deleteDevice(deviceId: number) { - const response = await this.deleteDeviceTuya(deviceId); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async deleteDeviceTuya(deviceId: number): Promise { - try { - const path = `/v2.0/cloud/thing/device/${deviceId}`; - const response = await this.tuya.request({ - method: 'DELETE', - path, - }); - - return response as controlDeviceInterface; - } catch (error) { - throw new HttpException( - 'Error delete device', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getDevicesByDeviceId(deviceId: number) { + async getDevicesByDeviceId(deviceId: string) { try { const response = await this.getDevicesByDeviceIdTuya(deviceId); return { - deviceId: response.result.id, - deviceName: response.result.name, + success: response.success, + result: response.result, + msg: response.msg, }; } catch (error) { throw new HttpException( @@ -223,10 +245,10 @@ export class DeviceService { } async getDevicesByDeviceIdTuya( - deviceId: number, + deviceId: string, ): Promise { try { - const path = `/v2.0/cloud/thing/device/${deviceId}`; + const path = `/v1.1/iot-03/devices/${deviceId}`; const response = await this.tuya.request({ method: 'GET', path, @@ -239,4 +261,78 @@ export class DeviceService { ); } } + async getDevicesInstructionByDeviceId(deviceId: string) { + try { + const response = await this.getDevicesInstructionByDeviceIdTuya(deviceId); + + return { + success: response.success, + result: { + category: response.result.category, + function: response.result.functions, + }, + msg: response.msg, + }; + } catch (error) { + throw new HttpException( + 'Error fetching device functions', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesInstructionByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.0/iot-03/devices/${deviceId}/functions`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + return response as GetDeviceDetailsFunctionsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching device functions', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDevicesInstructionStatus(deviceId: string) { + try { + const response = await this.getDevicesInstructionStatusTuya(deviceId); + + return { + result: response.result, + success: response.success, + msg: response.msg, + }; + } catch (error) { + throw new HttpException( + 'Error fetching device functions', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesInstructionStatusTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.0/iot-03/devices/status`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + device_ids: deviceId, + }, + }); + return response as unknown as GetDeviceDetailsFunctionsStatusInterface; + } catch (error) { + throw new HttpException( + 'Error fetching device functions', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From 4d6c4ed7879ade76e91bfdc66e907041498c4191 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 13 Mar 2024 07:28:10 -0400 Subject: [PATCH 072/259] deployment --- .deployment | 2 ++ .vscode/settings.json | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 .deployment create mode 100644 .vscode/settings.json diff --git a/.deployment b/.deployment new file mode 100644 index 0000000..6278331 --- /dev/null +++ b/.deployment @@ -0,0 +1,2 @@ +[config] +SCM_DO_BUILD_DURING_DEPLOYMENT=true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..89f79b5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "appService.defaultWebAppToDeploy": "/subscriptions/e59d43d4-4479-41b1-b34b-c989d2f4c82f/resourceGroups/backend/providers/Microsoft.Web/sites/syncrow", + "appService.deploySubpath": ".", + "appService.zipIgnorePattern": [ + "node_modules{,/**}", + ".vscode{,/**}" + ] +} \ No newline at end of file From 57fb43f47bee17cf35d2498dcd8cb4ce5414bdfe Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:17:53 +0300 Subject: [PATCH 073/259] Refactor device service to fetch devices by group ID --- src/device/services/device.service.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ce13b30..1854cd1 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -74,15 +74,19 @@ export class DeviceService { } async getDevicesByGroupId(getDeviceByGroupIdDto: GetDeviceByGroupIdDto) { try { - const response = await this.getDevicesByGroupIdTuya( + const devicesIds = await this.getDevicesByGroupIdTuya( getDeviceByGroupIdDto, ); - + const devicesDetails = await Promise.all( + devicesIds.result.data_list.map(async (device: any) => { + const deviceData = await this.getDevicesByDeviceId(device.dev_id); + return deviceData.result; + }), + ); return { - success: response.success, - devices: response.result.data_list, - count: response.result.count, - msg: response.msg, + success: devicesIds.success, + devices: devicesDetails, + msg: devicesIds.msg, }; } catch (error) { throw new HttpException( @@ -96,12 +100,11 @@ export class DeviceService { getDeviceByGroupIdDto: GetDeviceByGroupIdDto, ): Promise { try { - const path = `/v2.0/cloud/thing/group`; + const path = `/v2.0/cloud/thing/group/${getDeviceByGroupIdDto.groupId}/devices`; const response = await this.tuya.request({ method: 'GET', path, query: { - space_id: getDeviceByGroupIdDto.groupId, page_size: getDeviceByGroupIdDto.pageSize, page_no: getDeviceByGroupIdDto.pageNo, }, From ed210e94c773227f174c210babf096a4e7b21878 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 24 Mar 2024 10:51:17 +0300 Subject: [PATCH 074/259] fix some issues --- src/device/services/device.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 1854cd1..e062076 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -74,9 +74,8 @@ export class DeviceService { } async getDevicesByGroupId(getDeviceByGroupIdDto: GetDeviceByGroupIdDto) { try { - const devicesIds = await this.getDevicesByGroupIdTuya( - getDeviceByGroupIdDto, - ); + const devicesIds: GetDevicesByGroupIdInterface = + await this.getDevicesByGroupIdTuya(getDeviceByGroupIdDto); const devicesDetails = await Promise.all( devicesIds.result.data_list.map(async (device: any) => { const deviceData = await this.getDevicesByDeviceId(device.dev_id); From 22bcd37de706f09060506468718c6e78f0bba534 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:36:00 +0300 Subject: [PATCH 075/259] Add product DTO, entity, repository, and module --- libs/common/src/modules/product/dtos/index.ts | 1 + .../src/modules/product/dtos/product.dto.ts | 19 +++++++++++++ .../src/modules/product/entities/index.ts | 1 + .../product/entities/product.entity.ts | 27 +++++++++++++++++++ .../product/product.repository.module.ts | 11 ++++++++ .../src/modules/product/repositories/index.ts | 1 + .../repositories/product.repository.ts | 10 +++++++ 7 files changed, 70 insertions(+) create mode 100644 libs/common/src/modules/product/dtos/index.ts create mode 100644 libs/common/src/modules/product/dtos/product.dto.ts create mode 100644 libs/common/src/modules/product/entities/index.ts create mode 100644 libs/common/src/modules/product/entities/product.entity.ts create mode 100644 libs/common/src/modules/product/product.repository.module.ts create mode 100644 libs/common/src/modules/product/repositories/index.ts create mode 100644 libs/common/src/modules/product/repositories/product.repository.ts diff --git a/libs/common/src/modules/product/dtos/index.ts b/libs/common/src/modules/product/dtos/index.ts new file mode 100644 index 0000000..810efbd --- /dev/null +++ b/libs/common/src/modules/product/dtos/index.ts @@ -0,0 +1 @@ +export * from './product.dto'; diff --git a/libs/common/src/modules/product/dtos/product.dto.ts b/libs/common/src/modules/product/dtos/product.dto.ts new file mode 100644 index 0000000..2614d78 --- /dev/null +++ b/libs/common/src/modules/product/dtos/product.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ProductDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public catName: string; + + @IsString() + @IsNotEmpty() + public prodId: string; + + @IsString() + @IsNotEmpty() + public prodType: string; +} diff --git a/libs/common/src/modules/product/entities/index.ts b/libs/common/src/modules/product/entities/index.ts new file mode 100644 index 0000000..9120a7a --- /dev/null +++ b/libs/common/src/modules/product/entities/index.ts @@ -0,0 +1 @@ +export * from './product.entity'; diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts new file mode 100644 index 0000000..5f04d66 --- /dev/null +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity } from 'typeorm'; +import { ProductDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; + +@Entity({ name: 'product' }) +export class ProductEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + catName: string; + + @Column({ + nullable: false, + unique: true, + }) + public prodId: string; + + @Column({ + nullable: false, + }) + public prodType: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/product/product.repository.module.ts b/libs/common/src/modules/product/product.repository.module.ts new file mode 100644 index 0000000..6d92960 --- /dev/null +++ b/libs/common/src/modules/product/product.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ProductEntity } from './entities/product.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([ProductEntity])], +}) +export class ProductRepositoryModule {} diff --git a/libs/common/src/modules/product/repositories/index.ts b/libs/common/src/modules/product/repositories/index.ts new file mode 100644 index 0000000..4d7899c --- /dev/null +++ b/libs/common/src/modules/product/repositories/index.ts @@ -0,0 +1 @@ +export * from './product.repository'; diff --git a/libs/common/src/modules/product/repositories/product.repository.ts b/libs/common/src/modules/product/repositories/product.repository.ts new file mode 100644 index 0000000..3244f52 --- /dev/null +++ b/libs/common/src/modules/product/repositories/product.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { ProductEntity } from '../entities/product.entity'; + +@Injectable() +export class ProductRepository extends Repository { + constructor(private dataSource: DataSource) { + super(ProductEntity, dataSource.createEntityManager()); + } +} From 2cf45b170304c5d34098d791974c5c860105b9a7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:36:14 +0300 Subject: [PATCH 076/259] Refactor primary key generation in AbstractEntity --- .../abstract/entities/abstract.entity.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/libs/common/src/modules/abstract/entities/abstract.entity.ts b/libs/common/src/modules/abstract/entities/abstract.entity.ts index bc27899..7c54a31 100644 --- a/libs/common/src/modules/abstract/entities/abstract.entity.ts +++ b/libs/common/src/modules/abstract/entities/abstract.entity.ts @@ -1,11 +1,5 @@ import { Exclude } from 'class-transformer'; -import { - Column, - CreateDateColumn, - Generated, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm'; import { AbstractDto } from '../dtos'; import { Constructor } from '../../../../../common/src/util/types'; @@ -14,12 +8,11 @@ export abstract class AbstractEntity< T extends AbstractDto = AbstractDto, O = never, > { - @PrimaryGeneratedColumn('increment') + @PrimaryColumn({ + type: 'uuid', + generated: 'uuid', + }) @Exclude() - public id: number; - - @Column() - @Generated('uuid') public uuid: string; @CreateDateColumn({ type: 'timestamp' }) From 37316fd9e6e3f41ade686d45f1aa6ac552511236 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:36:34 +0300 Subject: [PATCH 077/259] Refactor OTP deletion in UserAuthService --- src/auth/services/user-auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index ae8c721..6c1e172 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -135,7 +135,7 @@ export class UserAuthService { } if (otp.expiryTime < new Date()) { - await this.otpRepository.delete(otp.id); + await this.otpRepository.delete(otp.uuid); throw new BadRequestException('OTP expired'); } From 37aa5191078a0e29e8ffb07c3ea6443a70dcd922 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:36:45 +0300 Subject: [PATCH 078/259] Add ProductRepository to DeviceModule imports and providers --- src/device/device.module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 8bac0cf..d72db05 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { DeviceService } from './services/device.service'; import { DeviceController } from './controllers/device.controller'; import { ConfigModule } from '@nestjs/config'; +import { ProductRepositoryModule } from '@app/common/modules/product/product.repository.module'; +import { ProductRepository } from '@app/common/modules/product/repositories'; @Module({ - imports: [ConfigModule], + imports: [ConfigModule, ProductRepositoryModule], controllers: [DeviceController], - providers: [DeviceService], + providers: [DeviceService, ProductRepository], exports: [DeviceService], }) export class DeviceModule {} From 2d6ce1842b684091041b5c6e53f484efc792f749 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:37:05 +0300 Subject: [PATCH 079/259] Add ProductEntity to entities list in database module --- libs/common/src/database/database.module.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index e263571..dd29899 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -6,6 +6,7 @@ import { UserEntity } from '../modules/user/entities/user.entity'; import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; import { HomeEntity } from '../modules/home/entities'; +import { ProductEntity } from '../modules/product/entities'; @Module({ imports: [ @@ -20,7 +21,13 @@ import { HomeEntity } from '../modules/home/entities'; username: configService.get('DB_USER'), password: configService.get('DB_PASSWORD'), database: configService.get('DB_NAME'), - entities: [UserEntity, UserSessionEntity, UserOtpEntity, HomeEntity], + entities: [ + UserEntity, + UserSessionEntity, + UserOtpEntity, + HomeEntity, + ProductEntity, + ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), logging: true, From fc387703ead729fcd04f9110034b0f80e7ce3801 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:27:01 +0300 Subject: [PATCH 080/259] Add camelCaseConverter helper function --- libs/common/src/helper/camelCaseConverter.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 libs/common/src/helper/camelCaseConverter.ts diff --git a/libs/common/src/helper/camelCaseConverter.ts b/libs/common/src/helper/camelCaseConverter.ts new file mode 100644 index 0000000..5cfc2ba --- /dev/null +++ b/libs/common/src/helper/camelCaseConverter.ts @@ -0,0 +1,20 @@ +export function convertKeysToCamelCase(obj: any): any { + if (!obj || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(convertKeysToCamelCase); + } + + const camelCaseObj: { [key: string]: any } = {}; + + for (const key of Object.keys(obj)) { + const camelCaseKey = key.replace(/_([a-z])/g, (_, letter) => + letter.toUpperCase(), + ); + camelCaseObj[camelCaseKey] = convertKeysToCamelCase(obj[key]); + } + + return camelCaseObj; +} From ca86e07981d722f373b302b2a5565d8a1d21bf6f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:27:31 +0300 Subject: [PATCH 081/259] Update GetDeviceDetailsInterface and add new interfaces --- src/device/interfaces/get.device.interface.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index e6f1361..d7c856b 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -1,5 +1,7 @@ export class GetDeviceDetailsInterface { - result: object; + result: { + productId: string; + }; success: boolean; msg: string; } @@ -42,3 +44,21 @@ export class GetDeviceDetailsFunctionsStatusInterface { success: boolean; msg: string; } +export interface GetProductInterface { + productType: string; + productId: string; +} + +export interface DeviceInstructionResponse { + success: boolean; + result: { + productId: string; + productType: string; + functions: { + code: string; + values: any[]; + dataType: string; + }[]; + }; + msg: string; +} From fba0063268cfd74e84ad0c0f8bc97f694e1b4d18 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:27:50 +0300 Subject: [PATCH 082/259] Refactor device service methods for better error handling and readability --- src/device/services/device.service.ts | 129 +++++++++++++++++++++----- 1 file changed, 105 insertions(+), 24 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ce13b30..649aadc 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -6,11 +6,13 @@ import { AddDeviceInRoomDto, } from '../dtos/add.device.dto'; import { + DeviceInstructionResponse, GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, GetDevicesByGroupIdInterface, GetDevicesByRoomIdInterface, + GetProductInterface, addDeviceInRoomInterface, controlDeviceInterface, } from '../interfaces/get.device.interface'; @@ -19,11 +21,16 @@ import { GetDeviceByRoomIdDto, } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { ProductRepository } from '@app/common/modules/product/repositories'; @Injectable() export class DeviceService { private tuya: TuyaContext; - constructor(private readonly configService: ConfigService) { + constructor( + private readonly configService: ConfigService, + private readonly productRepository: ProductRepository, + ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); // const clientId = this.configService.get('auth-config.CLIENT_ID'); @@ -45,7 +52,7 @@ export class DeviceService { }; } catch (error) { throw new HttpException( - 'Error fetching devices', + 'Error fetching devices by room', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -67,7 +74,7 @@ export class DeviceService { return response as GetDevicesByRoomIdInterface; } catch (error) { throw new HttpException( - 'Error fetching devices ', + 'Error fetching devices by room from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -86,7 +93,7 @@ export class DeviceService { }; } catch (error) { throw new HttpException( - 'Error fetching devices', + 'Error fetching devices by group', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -109,7 +116,7 @@ export class DeviceService { return response as GetDevicesByGroupIdInterface; } catch (error) { throw new HttpException( - 'Error fetching devices ', + 'Error fetching devices by group from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -147,7 +154,7 @@ export class DeviceService { return response as addDeviceInRoomInterface; } catch (error) { throw new HttpException( - 'Error adding device', + 'Error adding device in room from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -185,7 +192,7 @@ export class DeviceService { return response as addDeviceInRoomInterface; } catch (error) { throw new HttpException( - 'Error adding device', + 'Error adding device in group from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -221,15 +228,15 @@ export class DeviceService { return response as controlDeviceInterface; } catch (error) { throw new HttpException( - 'Error control device', + 'Error control device from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getDevicesByDeviceId(deviceId: string) { + async getDeviceDetailsByDeviceId(deviceId: string) { try { - const response = await this.getDevicesByDeviceIdTuya(deviceId); + const response = await this.getDeviceDetailsByDeviceIdTuya(deviceId); return { success: response.success, @@ -238,13 +245,12 @@ export class DeviceService { }; } catch (error) { throw new HttpException( - 'Error fetching device', + 'Error fetching device details', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - - async getDevicesByDeviceIdTuya( + async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { try { @@ -253,35 +259,110 @@ export class DeviceService { method: 'GET', path, }); - return response as GetDeviceDetailsInterface; + + // Convert keys to camel case + const camelCaseResponse = convertKeysToCamelCase(response); + const productType: string = await this.getProductTypeByProductId( + camelCaseResponse.result.productId, + ); + + return { + result: { + ...camelCaseResponse.result, + productType: productType, + }, + success: camelCaseResponse.success, + msg: camelCaseResponse.msg, + } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( - 'Error fetching device ', + 'Error fetching device details from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getDevicesInstructionByDeviceId(deviceId: string) { + async getProductIdByDeviceId(deviceId: string) { try { - const response = await this.getDevicesInstructionByDeviceIdTuya(deviceId); + const deviceDetails: GetDeviceDetailsInterface = + await this.getDeviceDetailsByDeviceId(deviceId); + + return deviceDetails.result.productId; + } catch (error) { + throw new HttpException( + 'Error fetching product id by device id', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getProductByProductId(productId: string): Promise { + try { + const product = await this.productRepository + .createQueryBuilder('product') + .where('product.prodId = :productId', { productId }) + .select(['product.prodId', 'product.prodType']) + .getOne(); + + if (product) { + return { + productType: product.prodType, + productId: product.prodId, + }; + } else { + throw new HttpException('Product not found', HttpStatus.NOT_FOUND); + } + } catch (error) { + throw new HttpException( + 'Error fetching product by product id from db', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getProductTypeByProductId(productId: string) { + try { + const product = await this.getProductByProductId(productId); + return product.productType; + } catch (error) { + throw new HttpException( + 'Error getting product type by product id', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDeviceInstructionByDeviceId( + deviceId: string, + ): Promise { + try { + const response = await this.getDeviceInstructionByDeviceIdTuya(deviceId); + + const productId: string = await this.getProductIdByDeviceId(deviceId); + const productType: string = + await this.getProductTypeByProductId(productId); return { success: response.success, result: { - category: response.result.category, - function: response.result.functions, + productId: productId, + productType: productType, + functions: response.result.functions.map((fun: any) => { + return { + code: fun.code, + values: fun.values, + dataType: fun.type, + }; + }), }, msg: response.msg, }; } catch (error) { throw new HttpException( - 'Error fetching device functions', + 'Error fetching device functions by device id', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getDevicesInstructionByDeviceIdTuya( + async getDeviceInstructionByDeviceIdTuya( deviceId: string, ): Promise { try { @@ -293,7 +374,7 @@ export class DeviceService { return response as GetDeviceDetailsFunctionsInterface; } catch (error) { throw new HttpException( - 'Error fetching device functions', + 'Error fetching device functions from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -309,7 +390,7 @@ export class DeviceService { }; } catch (error) { throw new HttpException( - 'Error fetching device functions', + 'Error fetching device functions status', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -330,7 +411,7 @@ export class DeviceService { return response as unknown as GetDeviceDetailsFunctionsStatusInterface; } catch (error) { throw new HttpException( - 'Error fetching device functions', + 'Error fetching device functions status from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } From a289b1645c98c736ce58ffc77931e13f808f52e4 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:28:13 +0300 Subject: [PATCH 083/259] Refactor device controller method names --- src/device/controllers/device.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 936b8e1..136e94f 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -57,9 +57,9 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceId') - async getDevicesByDeviceId(@Param('deviceId') deviceId: string) { + async getDeviceDetailsByDeviceId(@Param('deviceId') deviceId: string) { try { - return await this.deviceService.getDevicesByDeviceId(deviceId); + return await this.deviceService.getDeviceDetailsByDeviceId(deviceId); } catch (err) { throw new Error(err); } @@ -67,9 +67,9 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceId/functions') - async getDevicesInstructionByDeviceId(@Param('deviceId') deviceId: string) { + async getDeviceInstructionByDeviceId(@Param('deviceId') deviceId: string) { try { - return await this.deviceService.getDevicesInstructionByDeviceId(deviceId); + return await this.deviceService.getDeviceInstructionByDeviceId(deviceId); } catch (err) { throw new Error(err); } From 6c02f60f287dc290b62179171a1557e0b4743562 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:36:11 +0300 Subject: [PATCH 084/259] Refactor device interfaces to use interfaces instead of classes --- src/device/interfaces/get.device.interface.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index d7c856b..30f57f8 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -1,17 +1,17 @@ -export class GetDeviceDetailsInterface { +export interface GetDeviceDetailsInterface { result: { productId: string; }; success: boolean; msg: string; } -export class GetDevicesByRoomIdInterface { +export interface GetDevicesByRoomIdInterface { success: boolean; msg: string; result: []; } -export class GetDevicesByGroupIdInterface { +export interface GetDevicesByGroupIdInterface { success: boolean; msg: string; result: { @@ -20,18 +20,18 @@ export class GetDevicesByGroupIdInterface { }; } -export class addDeviceInRoomInterface { +export interface addDeviceInRoomInterface { success: boolean; msg: string; result: boolean; } -export class controlDeviceInterface { +export interface controlDeviceInterface { success: boolean; result: boolean; msg: string; } -export class GetDeviceDetailsFunctionsInterface { +export interface GetDeviceDetailsFunctionsInterface { result: { category: string; functions: []; @@ -39,7 +39,7 @@ export class GetDeviceDetailsFunctionsInterface { success: boolean; msg: string; } -export class GetDeviceDetailsFunctionsStatusInterface { +export interface GetDeviceDetailsFunctionsStatusInterface { result: [{ id: string; status: [] }]; success: boolean; msg: string; From 46e7fc426070c39066d024f5f12d30b28e18cfaa Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:36:31 +0300 Subject: [PATCH 085/259] Refactor getDevicesInstructionStatus method for improved readability --- src/device/services/device.service.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 649aadc..1a240d0 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -381,12 +381,18 @@ export class DeviceService { } async getDevicesInstructionStatus(deviceId: string) { try { - const response = await this.getDevicesInstructionStatusTuya(deviceId); - + const deviceStatus = await this.getDevicesInstructionStatusTuya(deviceId); + const productId: string = await this.getProductIdByDeviceId(deviceId); + const productType: string = + await this.getProductTypeByProductId(productId); return { - result: response.result, - success: response.success, - msg: response.msg, + result: { + productId: productId, + productType: productType, + status: deviceStatus.result[0].status, + }, + success: deviceStatus.success, + msg: deviceStatus.msg, }; } catch (error) { throw new HttpException( From e26c0475a33bf8976fd0c6df338c44c50e4f2f13 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:37:48 +0300 Subject: [PATCH 086/259] Remove commented out code --- src/device/services/device.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 1a240d0..b1c60c2 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -33,7 +33,6 @@ export class DeviceService { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); this.tuya = new TuyaContext({ baseUrl: 'https://openapi.tuyaeu.com', accessKey, @@ -259,7 +258,6 @@ export class DeviceService { method: 'GET', path, }); - // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); const productType: string = await this.getProductTypeByProductId( From da634167d01a5afe7c8808d7f51bb753ab4b5b67 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 12:20:00 +0300 Subject: [PATCH 087/259] fix build issue --- src/device/services/device.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index cd00e7b..0fa0be7 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -84,7 +84,9 @@ export class DeviceService { await this.getDevicesByGroupIdTuya(getDeviceByGroupIdDto); const devicesDetails = await Promise.all( devicesIds.result.data_list.map(async (device: any) => { - const deviceData = await this.getDevicesByDeviceId(device.dev_id); + const deviceData = await this.getDeviceDetailsByDeviceId( + device.dev_id, + ); return deviceData.result; }), ); From 93e8aabc4d15263ed6171ab266e390f7e6a75775 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 13:35:49 +0300 Subject: [PATCH 088/259] Add space DTO, entity, repository, and module --- libs/common/src/modules/space/dtos/index.ts | 1 + .../src/modules/space/dtos/space.dto.ts | 19 ++++++++++ .../src/modules/space/entities/index.ts | 1 + .../modules/space/entities/space.entity.ts | 36 +++++++++++++++++++ .../src/modules/space/repositories/index.ts | 1 + .../space/repositories/space.repository.ts | 10 ++++++ .../modules/space/space.repository.module.ts | 11 ++++++ 7 files changed, 79 insertions(+) create mode 100644 libs/common/src/modules/space/dtos/index.ts create mode 100644 libs/common/src/modules/space/dtos/space.dto.ts create mode 100644 libs/common/src/modules/space/entities/index.ts create mode 100644 libs/common/src/modules/space/entities/space.entity.ts create mode 100644 libs/common/src/modules/space/repositories/index.ts create mode 100644 libs/common/src/modules/space/repositories/space.repository.ts create mode 100644 libs/common/src/modules/space/space.repository.module.ts diff --git a/libs/common/src/modules/space/dtos/index.ts b/libs/common/src/modules/space/dtos/index.ts new file mode 100644 index 0000000..9144208 --- /dev/null +++ b/libs/common/src/modules/space/dtos/index.ts @@ -0,0 +1 @@ +export * from './space.dto'; diff --git a/libs/common/src/modules/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts new file mode 100644 index 0000000..cc5ad83 --- /dev/null +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SpaceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public parentUuid: string; + + @IsString() + @IsNotEmpty() + public spaceName: string; + + @IsString() + @IsNotEmpty() + public spaceTypeUuid: string; +} diff --git a/libs/common/src/modules/space/entities/index.ts b/libs/common/src/modules/space/entities/index.ts new file mode 100644 index 0000000..bce8032 --- /dev/null +++ b/libs/common/src/modules/space/entities/index.ts @@ -0,0 +1 @@ +export * from './space.entity'; diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts new file mode 100644 index 0000000..f546b62 --- /dev/null +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -0,0 +1,36 @@ +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { SpaceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceTypeEntity } from '../../space-type/entities'; + +@Entity({ name: 'space' }) +export class SpaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + parentUuid: string; + + @Column({ + nullable: false, + }) + public spaceName: string; + @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) + parent: SpaceEntity; + + @OneToMany(() => SpaceEntity, (space) => space.parent) + children: SpaceEntity[]; + @ManyToOne(() => SpaceTypeEntity, (spaceType) => spaceType.spaces) + spaceType: SpaceTypeEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/repositories/index.ts b/libs/common/src/modules/space/repositories/index.ts new file mode 100644 index 0000000..1e390d3 --- /dev/null +++ b/libs/common/src/modules/space/repositories/index.ts @@ -0,0 +1 @@ +export * from './space.repository'; diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts new file mode 100644 index 0000000..c939761 --- /dev/null +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SpaceEntity } from '../entities/space.entity'; + +@Injectable() +export class SpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts new file mode 100644 index 0000000..58a8ad0 --- /dev/null +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SpaceEntity } from './entities/space.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SpaceEntity])], +}) +export class SpaceRepositoryModule {} From 861aa788ae95163edbc6a2c7360bdb6a1229c103 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 13:36:19 +0300 Subject: [PATCH 089/259] Add space type DTO, entity, repository, and module --- .../src/modules/space-type/dtos/index.ts | 1 + .../modules/space-type/dtos/space.type.dto.ts | 11 ++++++++ .../src/modules/space-type/entities/index.ts | 1 + .../space-type/entities/space.type.entity.ts | 26 +++++++++++++++++++ .../modules/space-type/repositories/index.ts | 1 + .../repositories/space.type.repository.ts | 10 +++++++ .../space.type.repository.module.ts | 11 ++++++++ 7 files changed, 61 insertions(+) create mode 100644 libs/common/src/modules/space-type/dtos/index.ts create mode 100644 libs/common/src/modules/space-type/dtos/space.type.dto.ts create mode 100644 libs/common/src/modules/space-type/entities/index.ts create mode 100644 libs/common/src/modules/space-type/entities/space.type.entity.ts create mode 100644 libs/common/src/modules/space-type/repositories/index.ts create mode 100644 libs/common/src/modules/space-type/repositories/space.type.repository.ts create mode 100644 libs/common/src/modules/space-type/space.type.repository.module.ts diff --git a/libs/common/src/modules/space-type/dtos/index.ts b/libs/common/src/modules/space-type/dtos/index.ts new file mode 100644 index 0000000..e9824e7 --- /dev/null +++ b/libs/common/src/modules/space-type/dtos/index.ts @@ -0,0 +1 @@ +export * from './space.type.dto'; diff --git a/libs/common/src/modules/space-type/dtos/space.type.dto.ts b/libs/common/src/modules/space-type/dtos/space.type.dto.ts new file mode 100644 index 0000000..2f3d807 --- /dev/null +++ b/libs/common/src/modules/space-type/dtos/space.type.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SpaceTypeDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public type: string; +} diff --git a/libs/common/src/modules/space-type/entities/index.ts b/libs/common/src/modules/space-type/entities/index.ts new file mode 100644 index 0000000..71944ff --- /dev/null +++ b/libs/common/src/modules/space-type/entities/index.ts @@ -0,0 +1 @@ +export * from './space.type.entity'; diff --git a/libs/common/src/modules/space-type/entities/space.type.entity.ts b/libs/common/src/modules/space-type/entities/space.type.entity.ts new file mode 100644 index 0000000..f66ee86 --- /dev/null +++ b/libs/common/src/modules/space-type/entities/space.type.entity.ts @@ -0,0 +1,26 @@ +import { Column, Entity, OneToMany } from 'typeorm'; +import { SpaceTypeDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from '../../space/entities'; + +@Entity({ name: 'space-type' }) +export class SpaceTypeEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + type: string; + + @OneToMany(() => SpaceEntity, (space) => space.spaceType) + spaces: SpaceEntity[]; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space-type/repositories/index.ts b/libs/common/src/modules/space-type/repositories/index.ts new file mode 100644 index 0000000..26f69ae --- /dev/null +++ b/libs/common/src/modules/space-type/repositories/index.ts @@ -0,0 +1 @@ +export * from './space.type.repository'; diff --git a/libs/common/src/modules/space-type/repositories/space.type.repository.ts b/libs/common/src/modules/space-type/repositories/space.type.repository.ts new file mode 100644 index 0000000..0ea5b13 --- /dev/null +++ b/libs/common/src/modules/space-type/repositories/space.type.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SpaceTypeEntity } from '../entities/space.type.entity'; + +@Injectable() +export class SpaceTypeRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceTypeEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space-type/space.type.repository.module.ts b/libs/common/src/modules/space-type/space.type.repository.module.ts new file mode 100644 index 0000000..6787b67 --- /dev/null +++ b/libs/common/src/modules/space-type/space.type.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SpaceTypeEntity } from './entities/space.type.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SpaceTypeEntity])], +}) +export class SpaceTypeRepositoryModule {} From 4092c67f892a65268dd14c8e8ea24d110b042661 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:28:51 +0300 Subject: [PATCH 090/259] make spaceType nullable false --- libs/common/src/modules/space/entities/space.entity.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index f546b62..1f523f3 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -12,11 +12,6 @@ export class SpaceEntity extends AbstractEntity { }) public uuid: string; - @Column({ - nullable: false, - }) - parentUuid: string; - @Column({ nullable: false, }) @@ -26,7 +21,9 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => SpaceEntity, (space) => space.parent) children: SpaceEntity[]; - @ManyToOne(() => SpaceTypeEntity, (spaceType) => spaceType.spaces) + @ManyToOne(() => SpaceTypeEntity, (spaceType) => spaceType.spaces, { + nullable: false, + }) spaceType: SpaceTypeEntity; constructor(partial: Partial) { From 75197664fdca418014de00452611d27db3df06c1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:29:33 +0300 Subject: [PATCH 091/259] Add CommunityController --- .../controllers/community.controller.ts | 36 +++++++++++++++++++ src/community/controllers/index.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 src/community/controllers/community.controller.ts create mode 100644 src/community/controllers/index.ts diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts new file mode 100644 index 0000000..3fb10cd --- /dev/null +++ b/src/community/controllers/community.controller.ts @@ -0,0 +1,36 @@ +import { CommunityService } from '../services/community.service'; +import { + Body, + Controller, + HttpException, + HttpStatus, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddCommunityDto } from '../dtos/add.community.dto'; + +@ApiTags('Community Module') +@Controller({ + version: '1', + path: 'community', +}) +export class CommunityController { + constructor(private readonly communityService: CommunityService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addCommunity(@Body() addCommunityDto: AddCommunityDto) { + try { + await this.communityService.addCommunity(addCommunityDto); + return { message: 'Community added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/community/controllers/index.ts b/src/community/controllers/index.ts new file mode 100644 index 0000000..b78bbea --- /dev/null +++ b/src/community/controllers/index.ts @@ -0,0 +1 @@ +export * from './community.controller'; From 9654d58ea0eb99549b82a53586c1e628e266e04f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:30:05 +0300 Subject: [PATCH 092/259] Add AddCommunityDto class with properties --- src/community/dtos/add.community.dto.ts | 32 +++++++++++++++++++++++++ src/community/dtos/index.ts | 1 + 2 files changed, 33 insertions(+) create mode 100644 src/community/dtos/add.community.dto.ts create mode 100644 src/community/dtos/index.ts diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts new file mode 100644 index 0000000..d28af76 --- /dev/null +++ b/src/community/dtos/add.community.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsOptional } from 'class-validator'; + +export class AddCommunityDto { + @ApiProperty({ + description: 'spaceName', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceName: string; + + @ApiProperty({ + description: 'parentUuid', + required: false, + }) + @IsString() + @IsOptional() + public parentUuid?: string; + + @ApiProperty({ + description: 'spaceTypeUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceTypeUuid: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/community/dtos/index.ts b/src/community/dtos/index.ts new file mode 100644 index 0000000..7119b23 --- /dev/null +++ b/src/community/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.community.dto'; From 85f6dce85fa33a4fc488fb4ee631d25f6eaabe70 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:30:51 +0300 Subject: [PATCH 093/259] Add CommunityService implementation --- src/community/services/community.service.ts | 35 +++++++++++++++++++++ src/community/services/index.ts | 1 + 2 files changed, 36 insertions(+) create mode 100644 src/community/services/community.service.ts create mode 100644 src/community/services/index.ts diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts new file mode 100644 index 0000000..af76894 --- /dev/null +++ b/src/community/services/community.service.ts @@ -0,0 +1,35 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddCommunityDto } from '../dtos'; + +@Injectable() +export class CommunityService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly spaceRepository: SpaceRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + // const clientId = this.configService.get('auth-config.CLIENT_ID'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async addCommunity(addCommunityDto: AddCommunityDto) { + try { + await this.spaceRepository.save({ + spaceName: addCommunityDto.spaceName, + parent: { uuid: addCommunityDto.parentUuid }, + spaceType: { uuid: addCommunityDto.spaceTypeUuid }, + }); + } catch (err) { + throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/community/services/index.ts b/src/community/services/index.ts new file mode 100644 index 0000000..2408c7c --- /dev/null +++ b/src/community/services/index.ts @@ -0,0 +1 @@ +export * from './community.service'; From be880e47e79f304c2ee7d302c723cf515ac69bca Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:31:49 +0300 Subject: [PATCH 094/259] Add CommunityModule to app.module.ts --- src/app.module.ts | 2 ++ src/community/community.module.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/community/community.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index b3c0106..1ff3985 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { HomeModule } from './home/home.module'; import { RoomModule } from './room/room.module'; import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; +import { CommunityModule } from './community/community.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -15,6 +16,7 @@ import { DeviceModule } from './device/device.module'; }), AuthenticationModule, UserModule, + CommunityModule, HomeModule, RoomModule, GroupModule, diff --git a/src/community/community.module.ts b/src/community/community.module.ts new file mode 100644 index 0000000..cb83b4d --- /dev/null +++ b/src/community/community.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CommunityService } from './services/community.service'; +import { CommunityController } from './controllers/community.controller'; +import { ConfigModule } from '@nestjs/config'; +import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule], + controllers: [CommunityController], + providers: [CommunityService, SpaceRepository], + exports: [CommunityService], +}) +export class CommunityModule {} From d765d090eaf57a2563114279f2e970c1c6936421 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:39:26 +0300 Subject: [PATCH 095/259] Refactor CommunityService constructor for simplicity and readability --- src/community/services/community.service.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index af76894..e3d1560 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,25 +1,10 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConfigService } from '@nestjs/config'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; @Injectable() export class CommunityService { - private tuya: TuyaContext; - constructor( - private readonly configService: ConfigService, - private readonly spaceRepository: SpaceRepository, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); - this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', - accessKey, - secretKey, - }); - } + constructor(private readonly spaceRepository: SpaceRepository) {} async addCommunity(addCommunityDto: AddCommunityDto) { try { From ae8d909268e0704ab3b3bc78c53bf4c76ea3220c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:04:38 +0300 Subject: [PATCH 096/259] Add endpoint to get community by UUID --- .../controllers/community.controller.ts | 23 ++++++++++++++++++ src/community/dtos/get.community.dto.ts | 12 ++++++++++ .../interface/community.interface.ts | 12 ++++++++++ src/community/services/community.service.ts | 24 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 src/community/dtos/get.community.dto.ts create mode 100644 src/community/interface/community.interface.ts diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 3fb10cd..75b41fb 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -2,8 +2,10 @@ import { CommunityService } from '../services/community.service'; import { Body, Controller, + Get, HttpException, HttpStatus, + Param, Post, UseGuards, } from '@nestjs/common'; @@ -33,4 +35,25 @@ export class CommunityController { ); } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':communityUuid') + async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { + try { + const community = + await this.communityService.getCommunityByUuid(communityUuid); + + return community; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts new file mode 100644 index 0000000..e313125 --- /dev/null +++ b/src/community/dtos/get.community.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetCommunityDto { + @ApiProperty({ + description: 'communityUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public communityUuid: string; +} diff --git a/src/community/interface/community.interface.ts b/src/community/interface/community.interface.ts new file mode 100644 index 0000000..50fe78a --- /dev/null +++ b/src/community/interface/community.interface.ts @@ -0,0 +1,12 @@ +export interface GetCommunityByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + spaceName: string; + spaceType: { + uuid: string; + createdAt: Date; + updatedAt: Date; + type: string; + }; +} diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index e3d1560..1bfd12b 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,6 +1,7 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; +import { GetCommunityByUuidInterface } from '../interface/community.interface'; @Injectable() export class CommunityService { @@ -17,4 +18,27 @@ export class CommunityService { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } } + + async getCommunityByUuid( + communityUuid: string, + ): Promise { + try { + const community: GetCommunityByUuidInterface = + await this.spaceRepository.findOne({ + where: { + uuid: communityUuid, + }, + relations: ['spaceType'], + }); + if (!community) { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } + return community; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From badda01a05871f4784219d23a97a9d746d38cd9e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:30:44 +0300 Subject: [PATCH 097/259] Add SpaceTypeRepository to CommunityModule imports --- src/community/community.module.ts | 6 ++++-- .../controllers/community.controller.ts | 1 - src/community/dtos/add.community.dto.ts | 8 -------- src/community/services/community.service.ts | 17 +++++++++++++++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index cb83b4d..36b9da2 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -4,11 +4,13 @@ import { CommunityController } from './controllers/community.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 { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; +import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule], + imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], controllers: [CommunityController], - providers: [CommunityService, SpaceRepository], + providers: [CommunityService, SpaceRepository, SpaceTypeRepository], exports: [CommunityService], }) export class CommunityModule {} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 75b41fb..127ea72 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -43,7 +43,6 @@ export class CommunityController { try { const community = await this.communityService.getCommunityByUuid(communityUuid); - return community; } catch (error) { if (error.status === 404) { diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index d28af76..0409b3e 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -18,14 +18,6 @@ export class AddCommunityDto { @IsOptional() public parentUuid?: string; - @ApiProperty({ - description: 'spaceTypeUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public spaceTypeUuid: string; - constructor(dto: Partial) { Object.assign(this, dto); } diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 1bfd12b..ec964a9 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,3 +1,4 @@ +import { SpaceTypeRepository } from './../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; @@ -5,14 +6,23 @@ import { GetCommunityByUuidInterface } from '../interface/community.interface'; @Injectable() export class CommunityService { - constructor(private readonly spaceRepository: SpaceRepository) {} + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceTypeRepository: SpaceTypeRepository, + ) {} async addCommunity(addCommunityDto: AddCommunityDto) { try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'community', + }, + }); + await this.spaceRepository.save({ spaceName: addCommunityDto.spaceName, parent: { uuid: addCommunityDto.parentUuid }, - spaceType: { uuid: addCommunityDto.spaceTypeUuid }, + spaceType: { uuid: spaceType.uuid }, }); } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); @@ -27,6 +37,9 @@ export class CommunityService { await this.spaceRepository.findOne({ where: { uuid: communityUuid, + spaceType: { + type: 'community', + }, }, relations: ['spaceType'], }); From 4a033107da887ce41b44b7bb246b214828c64f34 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:29:33 +0300 Subject: [PATCH 098/259] Add SpaceEntity and SpaceTypeEntity to database module --- libs/common/src/database/database.module.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index dd29899..c604089 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -7,6 +7,8 @@ import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; import { HomeEntity } from '../modules/home/entities'; import { ProductEntity } from '../modules/product/entities'; +import { SpaceEntity } from '../modules/space/entities'; +import { SpaceTypeEntity } from '../modules/space-type/entities'; @Module({ imports: [ @@ -27,10 +29,12 @@ import { ProductEntity } from '../modules/product/entities'; UserOtpEntity, HomeEntity, ProductEntity, + SpaceEntity, + SpaceTypeEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), - logging: true, + logging: false, extra: { charset: 'utf8mb4', max: 20, // set pool max size From dcbeb92ce6773b74572285959afad5e9fd70e5b7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:30:11 +0300 Subject: [PATCH 099/259] Refactor class-validator imports in community DTOs --- src/community/dtos/add.community.dto.ts | 10 +----- src/community/dtos/get.community.dto.ts | 41 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index 0409b3e..f335d3d 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsOptional } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class AddCommunityDto { @ApiProperty({ @@ -10,14 +10,6 @@ export class AddCommunityDto { @IsNotEmpty() public spaceName: string; - @ApiProperty({ - description: 'parentUuid', - required: false, - }) - @IsString() - @IsOptional() - public parentUuid?: string; - constructor(dto: Partial) { Object.assign(this, dto); } diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts index e313125..be614e5 100644 --- a/src/community/dtos/get.community.dto.ts +++ b/src/community/dtos/get.community.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Min, +} from 'class-validator'; export class GetCommunityDto { @ApiProperty({ @@ -10,3 +18,34 @@ export class GetCommunityDto { @IsNotEmpty() public communityUuid: string; } + +export class GetCommunityChildDto { + @ApiProperty({ example: 1, description: 'Page number', required: true }) + @IsInt({ message: 'Page must be a number' }) + @Min(1, { message: 'Page must not be less than 1' }) + @IsNotEmpty() + public page: number; + + @ApiProperty({ + example: 10, + description: 'Number of items per page', + required: true, + }) + @IsInt({ message: 'Page size must be a number' }) + @Min(1, { message: 'Page size must not be less than 1' }) + @IsNotEmpty() + public pageSize: number; + + @ApiProperty({ + example: true, + description: 'Flag to determine whether to fetch full hierarchy', + required: false, + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform((value) => { + return value.obj.includeSubSpaces === 'true'; + }) + public includeSubSpaces: boolean = false; +} From 8a759aca354add9b887cbe0b4ac926bbee18d794 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:30:31 +0300 Subject: [PATCH 100/259] Refactor GetCommunityByUuidInterface --- src/community/interface/community.interface.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/community/interface/community.interface.ts b/src/community/interface/community.interface.ts index 50fe78a..b0f491e 100644 --- a/src/community/interface/community.interface.ts +++ b/src/community/interface/community.interface.ts @@ -2,11 +2,13 @@ export interface GetCommunityByUuidInterface { uuid: string; createdAt: Date; updatedAt: Date; - spaceName: string; - spaceType: { - uuid: string; - createdAt: Date; - updatedAt: Date; - type: string; - }; + name: string; + type: string; +} + +export interface CommunityChildInterface { + uuid: string; + name: string; + type: string; + children?: CommunityChildInterface[]; } From 893667c1b502c6d12d6e1707ef808134340bd9fa Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:30:40 +0300 Subject: [PATCH 101/259] Update global validation pipe configuration --- src/main.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 95f8ed1..129f319 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,14 @@ async function bootstrap() { setupSwaggerAuthentication(app); - app.useGlobalPipes(new ValidationPipe()); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, // Auto-transform payloads to their DTO instances. + transformOptions: { + enableImplicitConversion: true, // Convert incoming payloads to their DTO instances if possible. + }, + }), + ); await app.listen(process.env.PORT || 4000); } From ca5d67e291708ac837abce4591b1af28ef187549 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:30:54 +0300 Subject: [PATCH 102/259] Refactor community service to include child hierarchy retrieval --- src/community/services/community.service.ts | 93 ++++++++++++++++++--- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index ec964a9..81aab77 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,8 +1,13 @@ +import { GetCommunityChildDto } from './../dtos/get.community.dto'; import { SpaceTypeRepository } from './../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; -import { GetCommunityByUuidInterface } from '../interface/community.interface'; +import { + CommunityChildInterface, + GetCommunityByUuidInterface, +} from '../interface/community.interface'; +import { SpaceEntity } from '@app/common/modules/space/entities'; @Injectable() export class CommunityService { @@ -21,7 +26,6 @@ export class CommunityService { await this.spaceRepository.save({ spaceName: addCommunityDto.spaceName, - parent: { uuid: addCommunityDto.parentUuid }, spaceType: { uuid: spaceType.uuid }, }); } catch (err) { @@ -33,20 +37,25 @@ export class CommunityService { communityUuid: string, ): Promise { try { - const community: GetCommunityByUuidInterface = - await this.spaceRepository.findOne({ - where: { - uuid: communityUuid, - spaceType: { - type: 'community', - }, + const community = await this.spaceRepository.findOne({ + where: { + uuid: communityUuid, + spaceType: { + type: 'community', }, - relations: ['spaceType'], - }); + }, + relations: ['spaceType'], + }); if (!community) { throw new HttpException('Community not found', HttpStatus.NOT_FOUND); } - return community; + return { + uuid: community.uuid, + createdAt: community.createdAt, + updatedAt: community.updatedAt, + name: community.spaceName, + type: community.spaceType.type, + }; } catch (err) { throw new HttpException( err.message, @@ -54,4 +63,64 @@ export class CommunityService { ); } } + async getCommunityChildByUuid( + communityUuid: string, + getCommunityChildDto: GetCommunityChildDto, + ): Promise { + const { includeSubSpaces, page, pageSize } = getCommunityChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: communityUuid }, + relations: ['children', 'spaceType'], + }); + + if (space.spaceType.type !== 'community') { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + children: await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ), + }; + } + + private async buildHierarchy( + space: SpaceEntity, + includeSubSpaces: boolean, + page: number, + pageSize: number, + ): Promise { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['spaceType'], + skip: (page - 1) * pageSize, + take: pageSize, + }); + + if (!children || children.length === 0 || !includeSubSpaces) { + return children.map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); + } + + const childHierarchies = await Promise.all( + children.map(async (child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + children: await this.buildHierarchy(child, true, 1, pageSize), + })), + ); + + return childHierarchies; + } } From 72756e4e0801f34a2f499cdc64175110700850ab Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:31:20 +0300 Subject: [PATCH 103/259] Add endpoint to get community child by UUID --- .../controllers/community.controller.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 127ea72..6b1a5a7 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -7,11 +7,13 @@ import { HttpStatus, Param, Post, + Query, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddCommunityDto } from '../dtos/add.community.dto'; +import { GetCommunityChildDto } from '../dtos/get.community.dto'; @ApiTags('Community Module') @Controller({ @@ -55,4 +57,29 @@ export class CommunityController { } } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('child/:communityUuid') + async getCommunityChildByUuid( + @Param('communityUuid') communityUuid: string, + @Query() query: GetCommunityChildDto, + ) { + try { + const community = await this.communityService.getCommunityChildByUuid( + communityUuid, + query, + ); + return community; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } From 7cf26f3146ae98f8eeff95912e4158e2e0acd560 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:12:48 +0300 Subject: [PATCH 104/259] Update property name to communityName --- src/community/dtos/add.community.dto.ts | 4 ++-- src/community/services/community.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index f335d3d..96dce06 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -3,12 +3,12 @@ import { IsNotEmpty, IsString } from 'class-validator'; export class AddCommunityDto { @ApiProperty({ - description: 'spaceName', + description: 'communityName', required: true, }) @IsString() @IsNotEmpty() - public spaceName: string; + public communityName: string; constructor(dto: Partial) { Object.assign(this, dto); diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 81aab77..2861fe2 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -25,7 +25,7 @@ export class CommunityService { }); await this.spaceRepository.save({ - spaceName: addCommunityDto.spaceName, + spaceName: addCommunityDto.communityName, spaceType: { uuid: spaceType.uuid }, }); } catch (err) { From 499603f3c0bc7f8e7e71fa33e8cbff2a02960701 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:51:40 +0300 Subject: [PATCH 105/259] Add BuildingController with CRUD operations --- .../controllers/building.controller.ts | 104 ++++++++++++++++++ src/building/controllers/index.ts | 1 + 2 files changed, 105 insertions(+) create mode 100644 src/building/controllers/building.controller.ts create mode 100644 src/building/controllers/index.ts diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts new file mode 100644 index 0000000..17e01d4 --- /dev/null +++ b/src/building/controllers/building.controller.ts @@ -0,0 +1,104 @@ +import { BuildingService } from '../services/building.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddBuildingDto } from '../dtos/add.building.dto'; +import { GetBuildingChildDto } from '../dtos/get.building.dto'; + +@ApiTags('Building Module') +@Controller({ + version: '1', + path: 'building', +}) +export class BuildingController { + constructor(private readonly buildingService: BuildingService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addBuilding(@Body() addBuildingDto: AddBuildingDto) { + try { + await this.buildingService.addBuilding(addBuildingDto); + return { message: 'Building added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':buildingUuid') + async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { + try { + const building = + await this.buildingService.getBuildingByUuid(buildingUuid); + return building; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('child/:buildingUuid') + async getBuildingChildByUuid( + @Param('buildingUuid') buildingUuid: string, + @Query() query: GetBuildingChildDto, + ) { + try { + const building = await this.buildingService.getBuildingChildByUuid( + buildingUuid, + query, + ); + return building; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('parent/:buildingUuid') + async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { + try { + const building = + await this.buildingService.getBuildingParentByUuid(buildingUuid); + return building; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } +} diff --git a/src/building/controllers/index.ts b/src/building/controllers/index.ts new file mode 100644 index 0000000..b5ec3c2 --- /dev/null +++ b/src/building/controllers/index.ts @@ -0,0 +1 @@ +export * from './building.controller'; From 6fedcb6c3bec235c7adf1510210c257822691a99 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:51:53 +0300 Subject: [PATCH 106/259] Add building and get building DTOs --- src/building/dtos/add.building.dto.ts | 23 ++++++++++++ src/building/dtos/get.building.dto.ts | 51 +++++++++++++++++++++++++++ src/building/dtos/index.ts | 1 + 3 files changed, 75 insertions(+) create mode 100644 src/building/dtos/add.building.dto.ts create mode 100644 src/building/dtos/get.building.dto.ts create mode 100644 src/building/dtos/index.ts diff --git a/src/building/dtos/add.building.dto.ts b/src/building/dtos/add.building.dto.ts new file mode 100644 index 0000000..e9268c0 --- /dev/null +++ b/src/building/dtos/add.building.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddBuildingDto { + @ApiProperty({ + description: 'buildingName', + required: true, + }) + @IsString() + @IsNotEmpty() + public buildingName: string; + + @ApiProperty({ + description: 'communityUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public communityUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/building/dtos/get.building.dto.ts b/src/building/dtos/get.building.dto.ts new file mode 100644 index 0000000..f762469 --- /dev/null +++ b/src/building/dtos/get.building.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Min, +} from 'class-validator'; + +export class GetBuildingDto { + @ApiProperty({ + description: 'buildingUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public buildingUuid: string; +} + +export class GetBuildingChildDto { + @ApiProperty({ example: 1, description: 'Page number', required: true }) + @IsInt({ message: 'Page must be a number' }) + @Min(1, { message: 'Page must not be less than 1' }) + @IsNotEmpty() + public page: number; + + @ApiProperty({ + example: 10, + description: 'Number of items per page', + required: true, + }) + @IsInt({ message: 'Page size must be a number' }) + @Min(1, { message: 'Page size must not be less than 1' }) + @IsNotEmpty() + public pageSize: number; + + @ApiProperty({ + example: true, + description: 'Flag to determine whether to fetch full hierarchy', + required: false, + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform((value) => { + return value.obj.includeSubSpaces === 'true'; + }) + public includeSubSpaces: boolean = false; +} diff --git a/src/building/dtos/index.ts b/src/building/dtos/index.ts new file mode 100644 index 0000000..93e7c6f --- /dev/null +++ b/src/building/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.building.dto'; From 296457310ab859d73f5248accf8291c637e81ba7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:52:07 +0300 Subject: [PATCH 107/259] Add building interfaces for parent and child relationships --- src/building/interface/building.interface.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/building/interface/building.interface.ts diff --git a/src/building/interface/building.interface.ts b/src/building/interface/building.interface.ts new file mode 100644 index 0000000..92852a7 --- /dev/null +++ b/src/building/interface/building.interface.ts @@ -0,0 +1,21 @@ +export interface GetBuildingByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface BuildingChildInterface { + uuid: string; + name: string; + type: string; + totalCount?: number; + children?: BuildingChildInterface[]; +} +export interface BuildingParentInterface { + uuid: string; + name: string; + type: string; + parent?: BuildingParentInterface; +} From 526bf4b6618bd7763e4665e25e50ffdd576a4caf Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:52:30 +0300 Subject: [PATCH 108/259] Add building service and check building middleware --- src/building/services/building.service.ts | 182 ++++++++++++++++++++++ src/building/services/index.ts | 1 + src/middleware/CheckBuildingMiddleware.ts | 74 +++++++++ 3 files changed, 257 insertions(+) create mode 100644 src/building/services/building.service.ts create mode 100644 src/building/services/index.ts create mode 100644 src/middleware/CheckBuildingMiddleware.ts diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts new file mode 100644 index 0000000..fbaf8cb --- /dev/null +++ b/src/building/services/building.service.ts @@ -0,0 +1,182 @@ +import { GetBuildingChildDto } from '../dtos/get.building.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddBuildingDto } from '../dtos'; +import { + BuildingChildInterface, + BuildingParentInterface, + GetBuildingByUuidInterface, +} from '../interface/building.interface'; +import { SpaceEntity } from '@app/common/modules/space/entities'; + +@Injectable() +export class BuildingService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceTypeRepository: SpaceTypeRepository, + ) {} + + async addBuilding(addBuildingDto: AddBuildingDto) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'building', + }, + }); + + await this.spaceRepository.save({ + spaceName: addBuildingDto.buildingName, + parent: { uuid: addBuildingDto.communityUuid }, + spaceType: { uuid: spaceType.uuid }, + }); + } catch (err) { + throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getBuildingByUuid( + buildingUuid: string, + ): Promise { + try { + const building = await this.spaceRepository.findOne({ + where: { + uuid: buildingUuid, + spaceType: { + type: 'building', + }, + }, + relations: ['spaceType'], + }); + if (!building) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + return { + uuid: building.uuid, + createdAt: building.createdAt, + updatedAt: building.updatedAt, + name: building.spaceName, + type: building.spaceType.type, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getBuildingChildByUuid( + buildingUuid: string, + getBuildingChildDto: GetBuildingChildDto, + ): Promise { + const { includeSubSpaces, page, pageSize } = getBuildingChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: buildingUuid }, + relations: ['children', 'spaceType'], + }); + + if (space.spaceType.type !== 'building') { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } + + private async buildHierarchy( + space: SpaceEntity, + includeSubSpaces: boolean, + page: number, + pageSize: number, + ): Promise { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['spaceType'], + skip: (page - 1) * pageSize, + take: pageSize, + }); + + if (!children || children.length === 0 || !includeSubSpaces) { + return children + .filter( + (child) => + child.spaceType.type !== 'building' && + child.spaceType.type !== 'community', + ) // Filter remaining building and community types + .map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); + } + + const childHierarchies = await Promise.all( + children + .filter( + (child) => + child.spaceType.type !== 'building' && + child.spaceType.type !== 'community', + ) // Filter remaining building and community types + .map(async (child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + children: await this.buildHierarchy(child, true, 1, pageSize), + })), + ); + + return childHierarchies; + } + + async getBuildingParentByUuid( + buildingUuid: string, + ): Promise { + try { + const building = await this.spaceRepository.findOne({ + where: { + uuid: buildingUuid, + spaceType: { + type: 'building', + }, + }, + relations: ['spaceType', 'parent', 'parent.spaceType'], + }); + if (!building) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + + return { + uuid: building.uuid, + name: building.spaceName, + type: building.spaceType.type, + parent: { + uuid: building.parent.uuid, + name: building.parent.spaceName, + type: building.parent.spaceType.type, + }, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/building/services/index.ts b/src/building/services/index.ts new file mode 100644 index 0000000..7b260d2 --- /dev/null +++ b/src/building/services/index.ts @@ -0,0 +1 @@ +export * from './building.service'; diff --git a/src/middleware/CheckBuildingMiddleware.ts b/src/middleware/CheckBuildingMiddleware.ts new file mode 100644 index 0000000..92a5aba --- /dev/null +++ b/src/middleware/CheckBuildingMiddleware.ts @@ -0,0 +1,74 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + NestMiddleware, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CheckBuildingMiddleware implements NestMiddleware { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async use(req: Request, res: Response, next: NextFunction) { + try { + // Destructure request body for cleaner code + const { buildingName, communityUuid } = req.body; + + // Guard clauses for early return + if (!buildingName) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'buildingName is required', + }); + } + + if (!communityUuid) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'communityUuid is required', + }); + } + + // Call function to check if community is a building + await this.checkCommunityIsBuilding(communityUuid); + + // Call next middleware + next(); + } catch (error) { + // Handle errors + this.handleMiddlewareError(error, res); + } + } + + async checkCommunityIsBuilding(communityUuid: string) { + const communityData = await this.spaceRepository.findOne({ + where: { uuid: communityUuid }, + relations: ['spaceType'], + }); + + // Throw error if community not found + if (!communityData) { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } + + // Throw error if community is not of type 'community' + if (communityData.spaceType.type !== 'community') { + throw new HttpException( + "communityUuid is not of type 'community'", + HttpStatus.BAD_REQUEST, + ); + } + } + + // Function to handle middleware errors + private handleMiddlewareError(error: Error, res: Response) { + const status = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + const message = error.message || 'Internal server error'; + res.status(status).json({ statusCode: status, message }); + } +} From 033e09a44e973e72138a751d2f9c53b28a01f7dd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:55:08 +0300 Subject: [PATCH 109/259] Add BuildingModule and CheckCommunityTypeMiddleware --- src/building/building.module.ts | 29 +++++++++++++++++++ ...are.ts => CheckCommunityTypeMiddleware.ts} | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/building/building.module.ts rename src/middleware/{CheckBuildingMiddleware.ts => CheckCommunityTypeMiddleware.ts} (96%) diff --git a/src/building/building.module.ts b/src/building/building.module.ts new file mode 100644 index 0000000..2bdeb7e --- /dev/null +++ b/src/building/building.module.ts @@ -0,0 +1,29 @@ +import { + MiddlewareConsumer, + Module, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { BuildingService } from './services/building.service'; +import { BuildingController } from './controllers/building.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 { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; +import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { CheckCommunityTypeMiddleware } from 'src/middleware/CheckCommunityTypeMiddleware'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + controllers: [BuildingController], + providers: [BuildingService, SpaceRepository, SpaceTypeRepository], + exports: [BuildingService], +}) +export class BuildingModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CheckCommunityTypeMiddleware).forRoutes({ + path: '/building', + method: RequestMethod.POST, + }); + } +} diff --git a/src/middleware/CheckBuildingMiddleware.ts b/src/middleware/CheckCommunityTypeMiddleware.ts similarity index 96% rename from src/middleware/CheckBuildingMiddleware.ts rename to src/middleware/CheckCommunityTypeMiddleware.ts index 92a5aba..314b954 100644 --- a/src/middleware/CheckBuildingMiddleware.ts +++ b/src/middleware/CheckCommunityTypeMiddleware.ts @@ -8,7 +8,7 @@ import { import { Request, Response, NextFunction } from 'express'; @Injectable() -export class CheckBuildingMiddleware implements NestMiddleware { +export class CheckCommunityTypeMiddleware implements NestMiddleware { constructor(private readonly spaceRepository: SpaceRepository) {} async use(req: Request, res: Response, next: NextFunction) { From cb6ec0e278e14ee27d401e03a4e3c618b0cfde4a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:55:50 +0300 Subject: [PATCH 110/259] Add BuildingModule to imports in app.module.ts --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 1ff3985..6fcf2c1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import { RoomModule } from './room/room.module'; import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; import { CommunityModule } from './community/community.module'; +import { BuildingModule } from './building/building.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -17,6 +18,7 @@ import { CommunityModule } from './community/community.module'; AuthenticationModule, UserModule, CommunityModule, + BuildingModule, HomeModule, RoomModule, GroupModule, From 249dd675918a2b59e68dfc4588ddf1325dd4a57f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:07:00 +0300 Subject: [PATCH 111/259] Add totalCount field to CommunityChildInterface --- src/community/interface/community.interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/community/interface/community.interface.ts b/src/community/interface/community.interface.ts index b0f491e..7f769d3 100644 --- a/src/community/interface/community.interface.ts +++ b/src/community/interface/community.interface.ts @@ -10,5 +10,6 @@ export interface CommunityChildInterface { uuid: string; name: string; type: string; + totalCount?: number; children?: CommunityChildInterface[]; } From 9b837bd15a6e20c91329bd8569b88b2079b21540 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:07:11 +0300 Subject: [PATCH 112/259] Refactor buildHierarchy method for improved performance --- src/community/services/community.service.ts | 44 ++++++++++++--------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 2861fe2..a917d94 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -77,17 +77,21 @@ export class CommunityService { if (space.spaceType.type !== 'community') { throw new HttpException('Community not found', HttpStatus.NOT_FOUND); } - + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); return { uuid: space.uuid, name: space.spaceName, type: space.spaceType.type, - children: await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ), + totalCount, + children, }; } @@ -105,20 +109,24 @@ export class CommunityService { }); if (!children || children.length === 0 || !includeSubSpaces) { - return children.map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); + return children + .filter((child) => child.spaceType.type !== 'community') // Filter remaining community type + .map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); } const childHierarchies = await Promise.all( - children.map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), + children + .filter((child) => child.spaceType.type !== 'community') // Filter remaining community type + .map(async (child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + children: await this.buildHierarchy(child, true, 1, pageSize), + })), ); return childHierarchies; From d5f396e4f52cda01a53f6dfae27b7715a008f908 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 13 Apr 2024 17:20:20 +0300 Subject: [PATCH 113/259] Add floor module with controller, service, DTOs, and interfaces --- src/app.module.ts | 2 + src/floor/controllers/floor.controller.ts | 102 ++++++++++ src/floor/controllers/index.ts | 1 + src/floor/dtos/add.floor.dto.ts | 23 +++ src/floor/dtos/get.floor.dto.ts | 51 +++++ src/floor/dtos/index.ts | 1 + src/floor/floor.module.ts | 29 +++ src/floor/interface/floor.interface.ts | 21 +++ src/floor/services/floor.service.ts | 178 ++++++++++++++++++ src/floor/services/index.ts | 1 + src/middleware/CheckBuildingTypeMiddleware.ts | 74 ++++++++ 11 files changed, 483 insertions(+) create mode 100644 src/floor/controllers/floor.controller.ts create mode 100644 src/floor/controllers/index.ts create mode 100644 src/floor/dtos/add.floor.dto.ts create mode 100644 src/floor/dtos/get.floor.dto.ts create mode 100644 src/floor/dtos/index.ts create mode 100644 src/floor/floor.module.ts create mode 100644 src/floor/interface/floor.interface.ts create mode 100644 src/floor/services/floor.service.ts create mode 100644 src/floor/services/index.ts create mode 100644 src/middleware/CheckBuildingTypeMiddleware.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6fcf2c1..ff3bc78 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; import { CommunityModule } from './community/community.module'; import { BuildingModule } from './building/building.module'; +import { FloorModule } from './floor/floor.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -19,6 +20,7 @@ import { BuildingModule } from './building/building.module'; UserModule, CommunityModule, BuildingModule, + FloorModule, HomeModule, RoomModule, GroupModule, diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts new file mode 100644 index 0000000..6e3de22 --- /dev/null +++ b/src/floor/controllers/floor.controller.ts @@ -0,0 +1,102 @@ +import { FloorService } from '../services/floor.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddFloorDto } from '../dtos/add.floor.dto'; +import { GetFloorChildDto } from '../dtos/get.floor.dto'; + +@ApiTags('Floor Module') +@Controller({ + version: '1', + path: 'floor', +}) +export class FloorController { + constructor(private readonly floorService: FloorService) {} + + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Post() + async addFloor(@Body() addFloorDto: AddFloorDto) { + try { + await this.floorService.addFloor(addFloorDto); + return { message: 'Floor added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':floorUuid') + async getFloorByUuid(@Param('floorUuid') floorUuid: string) { + try { + const floor = await this.floorService.getFloorByUuid(floorUuid); + return floor; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('child/:floorUuid') + async getFloorChildByUuid( + @Param('floorUuid') floorUuid: string, + @Query() query: GetFloorChildDto, + ) { + try { + const floor = await this.floorService.getFloorChildByUuid( + floorUuid, + query, + ); + return floor; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('parent/:floorUuid') + async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { + try { + const floor = await this.floorService.getFloorParentByUuid(floorUuid); + return floor; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } +} diff --git a/src/floor/controllers/index.ts b/src/floor/controllers/index.ts new file mode 100644 index 0000000..99eb600 --- /dev/null +++ b/src/floor/controllers/index.ts @@ -0,0 +1 @@ +export * from './floor.controller'; diff --git a/src/floor/dtos/add.floor.dto.ts b/src/floor/dtos/add.floor.dto.ts new file mode 100644 index 0000000..9f2de58 --- /dev/null +++ b/src/floor/dtos/add.floor.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddFloorDto { + @ApiProperty({ + description: 'floorName', + required: true, + }) + @IsString() + @IsNotEmpty() + public floorName: string; + + @ApiProperty({ + description: 'buildingUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public buildingUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/floor/dtos/get.floor.dto.ts b/src/floor/dtos/get.floor.dto.ts new file mode 100644 index 0000000..23a8e56 --- /dev/null +++ b/src/floor/dtos/get.floor.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Min, +} from 'class-validator'; + +export class GetFloorDto { + @ApiProperty({ + description: 'floorUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public floorUuid: string; +} + +export class GetFloorChildDto { + @ApiProperty({ example: 1, description: 'Page number', required: true }) + @IsInt({ message: 'Page must be a number' }) + @Min(1, { message: 'Page must not be less than 1' }) + @IsNotEmpty() + public page: number; + + @ApiProperty({ + example: 10, + description: 'Number of items per page', + required: true, + }) + @IsInt({ message: 'Page size must be a number' }) + @Min(1, { message: 'Page size must not be less than 1' }) + @IsNotEmpty() + public pageSize: number; + + @ApiProperty({ + example: true, + description: 'Flag to determine whether to fetch full hierarchy', + required: false, + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform((value) => { + return value.obj.includeSubSpaces === 'true'; + }) + public includeSubSpaces: boolean = false; +} diff --git a/src/floor/dtos/index.ts b/src/floor/dtos/index.ts new file mode 100644 index 0000000..9c08a9f --- /dev/null +++ b/src/floor/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.floor.dto'; diff --git a/src/floor/floor.module.ts b/src/floor/floor.module.ts new file mode 100644 index 0000000..199f876 --- /dev/null +++ b/src/floor/floor.module.ts @@ -0,0 +1,29 @@ +import { + MiddlewareConsumer, + Module, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { FloorService } from './services/floor.service'; +import { FloorController } from './controllers/floor.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 { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; +import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { CheckBuildingTypeMiddleware } from 'src/middleware/CheckBuildingTypeMiddleware'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + controllers: [FloorController], + providers: [FloorService, SpaceRepository, SpaceTypeRepository], + exports: [FloorService], +}) +export class FloorModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CheckBuildingTypeMiddleware).forRoutes({ + path: '/floor', + method: RequestMethod.POST, + }); + } +} diff --git a/src/floor/interface/floor.interface.ts b/src/floor/interface/floor.interface.ts new file mode 100644 index 0000000..b70d504 --- /dev/null +++ b/src/floor/interface/floor.interface.ts @@ -0,0 +1,21 @@ +export interface GetFloorByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface FloorChildInterface { + uuid: string; + name: string; + type: string; + totalCount?: number; + children?: FloorChildInterface[]; +} +export interface FloorParentInterface { + uuid: string; + name: string; + type: string; + parent?: FloorParentInterface; +} diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts new file mode 100644 index 0000000..089f17e --- /dev/null +++ b/src/floor/services/floor.service.ts @@ -0,0 +1,178 @@ +import { GetFloorChildDto } from '../dtos/get.floor.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddFloorDto } from '../dtos'; +import { + FloorChildInterface, + FloorParentInterface, + GetFloorByUuidInterface, +} from '../interface/floor.interface'; +import { SpaceEntity } from '@app/common/modules/space/entities'; + +@Injectable() +export class FloorService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceTypeRepository: SpaceTypeRepository, + ) {} + + async addFloor(addFloorDto: AddFloorDto) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'floor', + }, + }); + + await this.spaceRepository.save({ + spaceName: addFloorDto.floorName, + parent: { uuid: addFloorDto.buildingUuid }, + spaceType: { uuid: spaceType.uuid }, + }); + } catch (err) { + throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getFloorByUuid(floorUuid: string): Promise { + try { + const floor = await this.spaceRepository.findOne({ + where: { + uuid: floorUuid, + spaceType: { + type: 'floor', + }, + }, + relations: ['spaceType'], + }); + if (!floor) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + return { + uuid: floor.uuid, + createdAt: floor.createdAt, + updatedAt: floor.updatedAt, + name: floor.spaceName, + type: floor.spaceType.type, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getFloorChildByUuid( + floorUuid: string, + getFloorChildDto: GetFloorChildDto, + ): Promise { + const { includeSubSpaces, page, pageSize } = getFloorChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: floorUuid }, + relations: ['children', 'spaceType'], + }); + + if (space.spaceType.type !== 'floor') { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } + + private async buildHierarchy( + space: SpaceEntity, + includeSubSpaces: boolean, + page: number, + pageSize: number, + ): Promise { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['spaceType'], + skip: (page - 1) * pageSize, + take: pageSize, + }); + + if (!children || children.length === 0 || !includeSubSpaces) { + return children + .filter( + (child) => + child.spaceType.type !== 'floor' && + child.spaceType.type !== 'building', + ) // Filter remaining floor and building types + .map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); + } + + const childHierarchies = await Promise.all( + children + .filter( + (child) => + child.spaceType.type !== 'floor' && + child.spaceType.type !== 'building', + ) // Filter remaining floor and building types + .map(async (child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + children: await this.buildHierarchy(child, true, 1, pageSize), + })), + ); + + return childHierarchies; + } + + async getFloorParentByUuid(floorUuid: string): Promise { + try { + const floor = await this.spaceRepository.findOne({ + where: { + uuid: floorUuid, + spaceType: { + type: 'floor', + }, + }, + relations: ['spaceType', 'parent', 'parent.spaceType'], + }); + if (!floor) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + + return { + uuid: floor.uuid, + name: floor.spaceName, + type: floor.spaceType.type, + parent: { + uuid: floor.parent.uuid, + name: floor.parent.spaceName, + type: floor.parent.spaceType.type, + }, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/floor/services/index.ts b/src/floor/services/index.ts new file mode 100644 index 0000000..e6f7946 --- /dev/null +++ b/src/floor/services/index.ts @@ -0,0 +1 @@ +export * from './floor.service'; diff --git a/src/middleware/CheckBuildingTypeMiddleware.ts b/src/middleware/CheckBuildingTypeMiddleware.ts new file mode 100644 index 0000000..dab04c1 --- /dev/null +++ b/src/middleware/CheckBuildingTypeMiddleware.ts @@ -0,0 +1,74 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + NestMiddleware, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CheckBuildingTypeMiddleware implements NestMiddleware { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async use(req: Request, res: Response, next: NextFunction) { + try { + // Destructure request body for cleaner code + const { floorName, buildingUuid } = req.body; + + // Guard clauses for early return + if (!floorName) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'floorName is required', + }); + } + + if (!buildingUuid) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'buildingUuid is required', + }); + } + + // Call function to check if building is a building + await this.checkBuildingIsBuilding(buildingUuid); + + // Call next middleware + next(); + } catch (error) { + // Handle errors + this.handleMiddlewareError(error, res); + } + } + + async checkBuildingIsBuilding(buildingUuid: string) { + const buildingData = await this.spaceRepository.findOne({ + where: { uuid: buildingUuid }, + relations: ['spaceType'], + }); + + // Throw error if building not found + if (!buildingData) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + + // Throw error if building is not of type 'building' + if (buildingData.spaceType.type !== 'building') { + throw new HttpException( + "buildingUuid is not of type 'building'", + HttpStatus.BAD_REQUEST, + ); + } + } + + // Function to handle middleware errors + private handleMiddlewareError(error: Error, res: Response) { + const status = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + const message = error.message || 'Internal server error'; + res.status(status).json({ statusCode: status, message }); + } +} From 9e3544756e2c531c740ec531e7c77966b8e55e6b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 13 Apr 2024 17:23:56 +0300 Subject: [PATCH 114/259] Refactor filtering logic in FloorService --- src/floor/services/floor.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index 089f17e..3485b88 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -116,8 +116,9 @@ export class FloorService { .filter( (child) => child.spaceType.type !== 'floor' && - child.spaceType.type !== 'building', - ) // Filter remaining floor and building types + child.spaceType.type !== 'building' && + child.spaceType.type !== 'community', + ) // Filter remaining floor and building and community types .map((child) => ({ uuid: child.uuid, name: child.spaceName, @@ -130,8 +131,9 @@ export class FloorService { .filter( (child) => child.spaceType.type !== 'floor' && - child.spaceType.type !== 'building', - ) // Filter remaining floor and building types + child.spaceType.type !== 'building' && + child.spaceType.type !== 'community', + ) // Filter remaining floor and building and community types .map(async (child) => ({ uuid: child.uuid, name: child.spaceName, From 120939b4701722fc3f962d65b6dc0fb5c205408d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 13 Apr 2024 17:25:48 +0300 Subject: [PATCH 115/259] Refactor middleware function names for clarity --- src/middleware/CheckBuildingTypeMiddleware.ts | 4 ++-- src/middleware/CheckCommunityTypeMiddleware.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/middleware/CheckBuildingTypeMiddleware.ts b/src/middleware/CheckBuildingTypeMiddleware.ts index dab04c1..170cb9b 100644 --- a/src/middleware/CheckBuildingTypeMiddleware.ts +++ b/src/middleware/CheckBuildingTypeMiddleware.ts @@ -32,7 +32,7 @@ export class CheckBuildingTypeMiddleware implements NestMiddleware { } // Call function to check if building is a building - await this.checkBuildingIsBuilding(buildingUuid); + await this.checkBuildingIsBuildingType(buildingUuid); // Call next middleware next(); @@ -42,7 +42,7 @@ export class CheckBuildingTypeMiddleware implements NestMiddleware { } } - async checkBuildingIsBuilding(buildingUuid: string) { + async checkBuildingIsBuildingType(buildingUuid: string) { const buildingData = await this.spaceRepository.findOne({ where: { uuid: buildingUuid }, relations: ['spaceType'], diff --git a/src/middleware/CheckCommunityTypeMiddleware.ts b/src/middleware/CheckCommunityTypeMiddleware.ts index 314b954..3eef679 100644 --- a/src/middleware/CheckCommunityTypeMiddleware.ts +++ b/src/middleware/CheckCommunityTypeMiddleware.ts @@ -32,7 +32,7 @@ export class CheckCommunityTypeMiddleware implements NestMiddleware { } // Call function to check if community is a building - await this.checkCommunityIsBuilding(communityUuid); + await this.checkCommunityIsCommunityType(communityUuid); // Call next middleware next(); @@ -42,7 +42,7 @@ export class CheckCommunityTypeMiddleware implements NestMiddleware { } } - async checkCommunityIsBuilding(communityUuid: string) { + async checkCommunityIsCommunityType(communityUuid: string) { const communityData = await this.spaceRepository.findOne({ where: { uuid: communityUuid }, relations: ['spaceType'], From c0e84d80d437c3a38dea54a6c9a480abb485d913 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 13 Apr 2024 17:55:28 +0300 Subject: [PATCH 116/259] Add unit module with controller, service, dtos, and middleware --- src/app.module.ts | 2 + src/middleware/CheckFloorTypeMiddleware.ts | 74 +++++++++ src/unit/controllers/index.ts | 1 + src/unit/controllers/unit.controller.ts | 99 ++++++++++++ src/unit/dtos/add.unit.dto.ts | 23 +++ src/unit/dtos/get.unit.dto.ts | 51 ++++++ src/unit/dtos/index.ts | 1 + src/unit/interface/unit.interface.ts | 21 +++ src/unit/services/index.ts | 1 + src/unit/services/unit.service.ts | 180 +++++++++++++++++++++ src/unit/unit.module.ts | 29 ++++ 11 files changed, 482 insertions(+) create mode 100644 src/middleware/CheckFloorTypeMiddleware.ts create mode 100644 src/unit/controllers/index.ts create mode 100644 src/unit/controllers/unit.controller.ts create mode 100644 src/unit/dtos/add.unit.dto.ts create mode 100644 src/unit/dtos/get.unit.dto.ts create mode 100644 src/unit/dtos/index.ts create mode 100644 src/unit/interface/unit.interface.ts create mode 100644 src/unit/services/index.ts create mode 100644 src/unit/services/unit.service.ts create mode 100644 src/unit/unit.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index ff3bc78..1322e04 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { DeviceModule } from './device/device.module'; import { CommunityModule } from './community/community.module'; import { BuildingModule } from './building/building.module'; import { FloorModule } from './floor/floor.module'; +import { UnitModule } from './unit/unit.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -21,6 +22,7 @@ import { FloorModule } from './floor/floor.module'; CommunityModule, BuildingModule, FloorModule, + UnitModule, HomeModule, RoomModule, GroupModule, diff --git a/src/middleware/CheckFloorTypeMiddleware.ts b/src/middleware/CheckFloorTypeMiddleware.ts new file mode 100644 index 0000000..e096750 --- /dev/null +++ b/src/middleware/CheckFloorTypeMiddleware.ts @@ -0,0 +1,74 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + NestMiddleware, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CheckFloorTypeMiddleware implements NestMiddleware { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async use(req: Request, res: Response, next: NextFunction) { + try { + // Destructure request body for cleaner code + const { unitName, floorUuid } = req.body; + + // Guard clauses for early return + if (!unitName) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'unitName is required', + }); + } + + if (!floorUuid) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'floorUuid is required', + }); + } + + // Call function to check if floor is a floor + await this.checkFloorIsFloorType(floorUuid); + + // Call next middleware + next(); + } catch (error) { + // Handle errors + this.handleMiddlewareError(error, res); + } + } + + async checkFloorIsFloorType(floorUuid: string) { + const floorData = await this.spaceRepository.findOne({ + where: { uuid: floorUuid }, + relations: ['spaceType'], + }); + + // Throw error if floor not found + if (!floorData) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + + // Throw error if floor is not of type 'floor' + if (floorData.spaceType.type !== 'floor') { + throw new HttpException( + "floorUuid is not of type 'floor'", + HttpStatus.BAD_REQUEST, + ); + } + } + + // Function to handle middleware errors + private handleMiddlewareError(error: Error, res: Response) { + const status = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + const message = error.message || 'Internal server error'; + res.status(status).json({ statusCode: status, message }); + } +} diff --git a/src/unit/controllers/index.ts b/src/unit/controllers/index.ts new file mode 100644 index 0000000..c8d7271 --- /dev/null +++ b/src/unit/controllers/index.ts @@ -0,0 +1 @@ +export * from './unit.controller'; diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts new file mode 100644 index 0000000..40f35a1 --- /dev/null +++ b/src/unit/controllers/unit.controller.ts @@ -0,0 +1,99 @@ +import { UnitService } from '../services/unit.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AddUnitDto } from '../dtos/add.unit.dto'; +import { GetUnitChildDto } from '../dtos/get.unit.dto'; + +@ApiTags('Unit Module') +@Controller({ + version: '1', + path: 'unit', +}) +export class UnitController { + constructor(private readonly unitService: UnitService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addUnit(@Body() addUnitDto: AddUnitDto) { + try { + await this.unitService.addUnit(addUnitDto); + return { message: 'Unit added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':unitUuid') + async getUnitByUuid(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitByUuid(unitUuid); + return unit; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('child/:unitUuid') + async getUnitChildByUuid( + @Param('unitUuid') unitUuid: string, + @Query() query: GetUnitChildDto, + ) { + try { + const unit = await this.unitService.getUnitChildByUuid(unitUuid, query); + return unit; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('parent/:unitUuid') + async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitParentByUuid(unitUuid); + return unit; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } +} diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts new file mode 100644 index 0000000..40f164a --- /dev/null +++ b/src/unit/dtos/add.unit.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddUnitDto { + @ApiProperty({ + description: 'unitName', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitName: string; + + @ApiProperty({ + description: 'floorUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public floorUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/dtos/get.unit.dto.ts b/src/unit/dtos/get.unit.dto.ts new file mode 100644 index 0000000..b96147b --- /dev/null +++ b/src/unit/dtos/get.unit.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Min, +} from 'class-validator'; + +export class GetUnitDto { + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; +} + +export class GetUnitChildDto { + @ApiProperty({ example: 1, description: 'Page number', required: true }) + @IsInt({ message: 'Page must be a number' }) + @Min(1, { message: 'Page must not be less than 1' }) + @IsNotEmpty() + public page: number; + + @ApiProperty({ + example: 10, + description: 'Number of items per page', + required: true, + }) + @IsInt({ message: 'Page size must be a number' }) + @Min(1, { message: 'Page size must not be less than 1' }) + @IsNotEmpty() + public pageSize: number; + + @ApiProperty({ + example: true, + description: 'Flag to determine whether to fetch full hierarchy', + required: false, + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform((value) => { + return value.obj.includeSubSpaces === 'true'; + }) + public includeSubSpaces: boolean = false; +} diff --git a/src/unit/dtos/index.ts b/src/unit/dtos/index.ts new file mode 100644 index 0000000..970d13d --- /dev/null +++ b/src/unit/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.unit.dto'; diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts new file mode 100644 index 0000000..8db6bfa --- /dev/null +++ b/src/unit/interface/unit.interface.ts @@ -0,0 +1,21 @@ +export interface GetUnitByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface UnitChildInterface { + uuid: string; + name: string; + type: string; + totalCount?: number; + children?: UnitChildInterface[]; +} +export interface UnitParentInterface { + uuid: string; + name: string; + type: string; + parent?: UnitParentInterface; +} diff --git a/src/unit/services/index.ts b/src/unit/services/index.ts new file mode 100644 index 0000000..0540c40 --- /dev/null +++ b/src/unit/services/index.ts @@ -0,0 +1 @@ +export * from './unit.service'; diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts new file mode 100644 index 0000000..4906f7d --- /dev/null +++ b/src/unit/services/unit.service.ts @@ -0,0 +1,180 @@ +import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddUnitDto } from '../dtos'; +import { + UnitChildInterface, + UnitParentInterface, + GetUnitByUuidInterface, +} from '../interface/unit.interface'; +import { SpaceEntity } from '@app/common/modules/space/entities'; + +@Injectable() +export class UnitService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceTypeRepository: SpaceTypeRepository, + ) {} + + async addUnit(addUnitDto: AddUnitDto) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'unit', + }, + }); + + await this.spaceRepository.save({ + spaceName: addUnitDto.unitName, + parent: { uuid: addUnitDto.floorUuid }, + spaceType: { uuid: spaceType.uuid }, + }); + } catch (err) { + throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getUnitByUuid(unitUuid: string): Promise { + try { + const unit = await this.spaceRepository.findOne({ + where: { + uuid: unitUuid, + spaceType: { + type: 'unit', + }, + }, + relations: ['spaceType'], + }); + if (!unit) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + return { + uuid: unit.uuid, + createdAt: unit.createdAt, + updatedAt: unit.updatedAt, + name: unit.spaceName, + type: unit.spaceType.type, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getUnitChildByUuid( + unitUuid: string, + getUnitChildDto: GetUnitChildDto, + ): Promise { + const { includeSubSpaces, page, pageSize } = getUnitChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['children', 'spaceType'], + }); + + if (space.spaceType.type !== 'unit') { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } + + private async buildHierarchy( + space: SpaceEntity, + includeSubSpaces: boolean, + page: number, + pageSize: number, + ): Promise { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['spaceType'], + skip: (page - 1) * pageSize, + take: pageSize, + }); + + if (!children || children.length === 0 || !includeSubSpaces) { + return children + .filter( + (child) => + child.spaceType.type !== 'unit' && + child.spaceType.type !== 'floor' && + child.spaceType.type !== 'community', + ) // Filter remaining unit and floor and community types + .map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); + } + + const childHierarchies = await Promise.all( + children + .filter( + (child) => + child.spaceType.type !== 'unit' && + child.spaceType.type !== 'floor' && + child.spaceType.type !== 'community', + ) // Filter remaining unit and floor and community types + .map(async (child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + children: await this.buildHierarchy(child, true, 1, pageSize), + })), + ); + + return childHierarchies; + } + + async getUnitParentByUuid(unitUuid: string): Promise { + try { + const unit = await this.spaceRepository.findOne({ + where: { + uuid: unitUuid, + spaceType: { + type: 'unit', + }, + }, + relations: ['spaceType', 'parent', 'parent.spaceType'], + }); + if (!unit) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + + return { + uuid: unit.uuid, + name: unit.spaceName, + type: unit.spaceType.type, + parent: { + uuid: unit.parent.uuid, + name: unit.parent.spaceName, + type: unit.parent.spaceType.type, + }, + }; + } catch (err) { + throw new HttpException( + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts new file mode 100644 index 0000000..0ebee41 --- /dev/null +++ b/src/unit/unit.module.ts @@ -0,0 +1,29 @@ +import { + MiddlewareConsumer, + Module, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { UnitService } from './services/unit.service'; +import { UnitController } from './controllers/unit.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 { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; +import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { CheckFloorTypeMiddleware } from 'src/middleware/CheckFloorTypeMiddleware'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + controllers: [UnitController], + providers: [UnitService, SpaceRepository, SpaceTypeRepository], + exports: [UnitService], +}) +export class UnitModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CheckFloorTypeMiddleware).forRoutes({ + path: '/unit', + method: RequestMethod.POST, + }); + } +} From 4401b4358c331739db6d81a065834108ff195e10 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:07:54 +0300 Subject: [PATCH 117/259] Refactor filtering logic in UnitService --- src/unit/services/unit.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 4906f7d..6969717 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -117,8 +117,9 @@ export class UnitService { (child) => child.spaceType.type !== 'unit' && child.spaceType.type !== 'floor' && - child.spaceType.type !== 'community', - ) // Filter remaining unit and floor and community types + child.spaceType.type !== 'community' && + child.spaceType.type !== 'unit', + ) // Filter remaining unit and floor and community and unit types .map((child) => ({ uuid: child.uuid, name: child.spaceName, @@ -132,8 +133,9 @@ export class UnitService { (child) => child.spaceType.type !== 'unit' && child.spaceType.type !== 'floor' && - child.spaceType.type !== 'community', - ) // Filter remaining unit and floor and community types + child.spaceType.type !== 'community' && + child.spaceType.type !== 'unit', + ) // Filter remaining unit and floor and community and unit types .map(async (child) => ({ uuid: child.uuid, name: child.spaceName, From dffe347adf7fbb56a1263ce597e0096269f0a4a9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:54:44 +0300 Subject: [PATCH 118/259] Add CheckUnitTypeMiddleware and update room module --- src/app.module.ts | 1 + src/middleware/CheckUnitTypeMiddleware.ts | 74 ++++++++++ src/room/controllers/room.controller.ts | 73 ++++++---- src/room/dtos/add.room.dto.ts | 11 +- src/room/interface/room.interface.ts | 14 ++ src/room/interfaces/get.room.interface.ts | 12 -- src/room/room.module.ts | 26 +++- src/room/services/room.service.ts | 159 ++++++++++------------ 8 files changed, 236 insertions(+), 134 deletions(-) create mode 100644 src/middleware/CheckUnitTypeMiddleware.ts create mode 100644 src/room/interface/room.interface.ts delete mode 100644 src/room/interfaces/get.room.interface.ts diff --git a/src/app.module.ts b/src/app.module.ts index 1322e04..7050ec6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { UnitModule } from './unit/unit.module'; BuildingModule, FloorModule, UnitModule, + RoomModule, HomeModule, RoomModule, GroupModule, diff --git a/src/middleware/CheckUnitTypeMiddleware.ts b/src/middleware/CheckUnitTypeMiddleware.ts new file mode 100644 index 0000000..7f5ba6a --- /dev/null +++ b/src/middleware/CheckUnitTypeMiddleware.ts @@ -0,0 +1,74 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + NestMiddleware, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CheckUnitTypeMiddleware implements NestMiddleware { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async use(req: Request, res: Response, next: NextFunction) { + try { + // Destructure request body for cleaner code + const { roomName, unitUuid } = req.body; + + // Guard clauses for early return + if (!roomName) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'roomName is required', + }); + } + + if (!unitUuid) { + return res.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'unitUuid is required', + }); + } + + // Call function to check if unit is a unit + await this.checkFloorIsFloorType(unitUuid); + + // Call next middleware + next(); + } catch (error) { + // Handle errors + this.handleMiddlewareError(error, res); + } + } + + async checkFloorIsFloorType(unitUuid: string) { + const unitData = await this.spaceRepository.findOne({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + // Throw error if unit not found + if (!unitData) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + + // Throw error if unit is not of type 'unit' + if (unitData.spaceType.type !== 'unit') { + throw new HttpException( + "unitUuid is not of type 'unit'", + HttpStatus.BAD_REQUEST, + ); + } + } + + // Function to handle middleware errors + private handleMiddlewareError(error: Error, res: Response) { + const status = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + const message = error.message || 'Internal server error'; + res.status(status).json({ statusCode: status, message }); + } +} diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index b90210c..a998077 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -3,10 +3,11 @@ import { Body, Controller, Get, + HttpException, + HttpStatus, + Param, Post, UseGuards, - Query, - Param, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; @@ -20,34 +21,56 @@ import { AddRoomDto } from '../dtos/add.room.dto'; export class RoomController { constructor(private readonly roomService: RoomService) {} - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get() - async getRoomsByHomeId(@Query('homeId') homeId: string) { - try { - return await this.roomService.getRoomsByHomeId(homeId); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get(':roomId') - async getRoomsByRoomId(@Param('roomId') roomId: string) { - try { - return await this.roomService.getRoomsByRoomId(roomId); - } catch (err) { - throw new Error(err); - } - } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { - return await this.roomService.addRoom(addRoomDto); - } catch (err) { - throw new Error(err); + await this.roomService.addRoom(addRoomDto); + return { message: 'Room added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':roomUuid') + async getRoomByUuid(@Param('roomUuid') roomUuid: string) { + try { + const room = await this.roomService.getRoomByUuid(roomUuid); + return room; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('parent/:roomUuid') + async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { + try { + const room = await this.roomService.getRoomParentByUuid(roomUuid); + return room; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } } diff --git a/src/room/dtos/add.room.dto.ts b/src/room/dtos/add.room.dto.ts index 3d39559..69425b1 100644 --- a/src/room/dtos/add.room.dto.ts +++ b/src/room/dtos/add.room.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class AddRoomDto { @ApiProperty({ @@ -11,10 +11,13 @@ export class AddRoomDto { public roomName: string; @ApiProperty({ - description: 'homeId', + description: 'unitUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public homeId: string; + public unitUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } } diff --git a/src/room/interface/room.interface.ts b/src/room/interface/room.interface.ts new file mode 100644 index 0000000..f98a8c9 --- /dev/null +++ b/src/room/interface/room.interface.ts @@ -0,0 +1,14 @@ +export interface GetRoomByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface RoomParentInterface { + uuid: string; + name: string; + type: string; + parent?: RoomParentInterface; +} diff --git a/src/room/interfaces/get.room.interface.ts b/src/room/interfaces/get.room.interface.ts deleted file mode 100644 index 56c0d49..0000000 --- a/src/room/interfaces/get.room.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class GetRoomDetailsInterface { - result: { - id: string; - name: string; - root_id: string; - }; -} -export class GetRoomsIdsInterface { - result: { - data: []; - }; -} diff --git a/src/room/room.module.ts b/src/room/room.module.ts index cd520c6..660fbec 100644 --- a/src/room/room.module.ts +++ b/src/room/room.module.ts @@ -1,11 +1,29 @@ -import { Module } from '@nestjs/common'; +import { + MiddlewareConsumer, + Module, + NestModule, + RequestMethod, +} from '@nestjs/common'; import { RoomService } from './services/room.service'; import { RoomController } from './controllers/room.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 { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; +import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { CheckUnitTypeMiddleware } from 'src/middleware/CheckUnitTypeMiddleware'; + @Module({ - imports: [ConfigModule], + imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], controllers: [RoomController], - providers: [RoomService], + providers: [RoomService, SpaceRepository, SpaceTypeRepository], exports: [RoomService], }) -export class RoomModule {} +export class RoomModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CheckUnitTypeMiddleware).forRoutes({ + path: '/room', + method: RequestMethod.POST, + }); + } +} diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 095e8df..ae0d403 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -1,114 +1,95 @@ +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConfigService } from '@nestjs/config'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddRoomDto } from '../dtos'; import { - GetRoomDetailsInterface, - GetRoomsIdsInterface, -} from '../interfaces/get.room.interface'; + RoomParentInterface, + GetRoomByUuidInterface, +} from '../interface/room.interface'; @Injectable() export class RoomService { - private tuya: TuyaContext; - constructor(private readonly configService: ConfigService) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); - this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', - accessKey, - secretKey, - }); - } + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceTypeRepository: SpaceTypeRepository, + ) {} - async getRoomsByHomeId(homeId: string) { - try { - const roomsIds = await this.getRoomsIds(homeId); - - const roomsDetails = await Promise.all( - roomsIds.result.data.map(async (roomId) => { - const roomData = await this.getRoomDetails(roomId); - return { - roomId: roomData?.result?.id, - roomName: roomData ? roomData.result.name : null, - }; - }), - ); - - return roomsDetails; - } catch (error) { - throw new HttpException( - 'Error fetching rooms', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getRoomsIds(homeId: string): Promise { - try { - const path = `/v2.0/cloud/space/child`; - const response = await this.tuya.request({ - method: 'GET', - path, - query: { space_id: homeId }, - }); - return response as GetRoomsIdsInterface; - } catch (error) { - throw new HttpException( - 'Error fetching rooms ids', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getRoomDetails(roomId: string): Promise { - // Added return type - try { - const path = `/v2.0/cloud/space/${roomId}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - - return response as GetRoomDetailsInterface; // Cast response to RoomData - } catch (error) { - throw new HttpException( - 'Error fetching rooms details', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async addRoom(addRoomDto: AddRoomDto) { try { - const path = `/v2.0/cloud/space/creation`; - const data = await this.tuya.request({ - method: 'POST', - path, - body: { name: addRoomDto.roomName, parent_id: addRoomDto.homeId }, + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'room', + }, }); + await this.spaceRepository.save({ + spaceName: addRoomDto.roomName, + parent: { uuid: addRoomDto.unitUuid }, + spaceType: { uuid: spaceType.uuid }, + }); + } catch (err) { + throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getRoomByUuid(roomUuid: string): Promise { + try { + const room = await this.spaceRepository.findOne({ + where: { + uuid: roomUuid, + spaceType: { + type: 'room', + }, + }, + relations: ['spaceType'], + }); + if (!room) { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } return { - success: data.success, - roomId: data.result, + uuid: room.uuid, + createdAt: room.createdAt, + updatedAt: room.updatedAt, + name: room.spaceName, + type: room.spaceType.type, }; - } catch (error) { + } catch (err) { throw new HttpException( - 'Error adding room', - HttpStatus.INTERNAL_SERVER_ERROR, + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getRoomsByRoomId(roomId: string) { + + async getRoomParentByUuid(roomUuid: string): Promise { try { - const response = await this.getRoomDetails(roomId); + const room = await this.spaceRepository.findOne({ + where: { + uuid: roomUuid, + spaceType: { + type: 'room', + }, + }, + relations: ['spaceType', 'parent', 'parent.spaceType'], + }); + if (!room) { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } return { - homeId: response.result.root_id, - roomId: response.result.id, - roomName: response.result.name, + uuid: room.uuid, + name: room.spaceName, + type: room.spaceType.type, + parent: { + uuid: room.parent.uuid, + name: room.parent.spaceName, + type: room.parent.spaceType.type, + }, }; - } catch (error) { + } catch (err) { throw new HttpException( - 'Error fetching rooms', - HttpStatus.INTERNAL_SERVER_ERROR, + err.message, + err.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } From 42a4d0cc11dbd009c17935515d67cab5af7f0a43 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:17:11 +0300 Subject: [PATCH 119/259] Add endpoint to rename community by UUID --- .../controllers/community.controller.ts | 27 ++++++++++ src/community/dtos/update.community.dto.ts | 16 ++++++ .../interface/community.interface.ts | 5 ++ src/community/services/community.service.ts | 51 ++++++++++++++++++- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/community/dtos/update.community.dto.ts diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 6b1a5a7..1a07791 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -7,6 +7,7 @@ import { HttpStatus, Param, Post, + Put, Query, UseGuards, } from '@nestjs/common'; @@ -14,6 +15,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddCommunityDto } from '../dtos/add.community.dto'; import { GetCommunityChildDto } from '../dtos/get.community.dto'; +import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @ApiTags('Community Module') @Controller({ @@ -82,4 +84,29 @@ export class CommunityController { } } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename/:communityUuid') + async renameCommunityByUuid( + @Param('communityUuid') communityUuid: string, + @Body() updateCommunityDto: UpdateCommunityNameDto, + ) { + try { + const community = await this.communityService.renameCommunityByUuid( + communityUuid, + updateCommunityDto, + ); + return community; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/community/dtos/update.community.dto.ts b/src/community/dtos/update.community.dto.ts new file mode 100644 index 0000000..6f15d43 --- /dev/null +++ b/src/community/dtos/update.community.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateCommunityNameDto { + @ApiProperty({ + description: 'communityName', + required: true, + }) + @IsString() + @IsNotEmpty() + public communityName: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/community/interface/community.interface.ts b/src/community/interface/community.interface.ts index 7f769d3..ca18aca 100644 --- a/src/community/interface/community.interface.ts +++ b/src/community/interface/community.interface.ts @@ -13,3 +13,8 @@ export interface CommunityChildInterface { totalCount?: number; children?: CommunityChildInterface[]; } +export interface RenameCommunityByUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index a917d94..33cab98 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,13 +1,20 @@ import { GetCommunityChildDto } from './../dtos/get.community.dto'; import { SpaceTypeRepository } from './../../../libs/common/src/modules/space-type/repositories/space.type.repository'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; import { CommunityChildInterface, GetCommunityByUuidInterface, + RenameCommunityByUuidInterface, } from '../interface/community.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; +import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @Injectable() export class CommunityService { @@ -131,4 +138,46 @@ export class CommunityService { return childHierarchies; } + async renameCommunityByUuid( + communityUuid: string, + updateCommunityDto: UpdateCommunityNameDto, + ): Promise { + try { + const community = await this.spaceRepository.findOneOrFail({ + where: { uuid: communityUuid }, + relations: ['spaceType'], + }); + + if ( + !community || + !community.spaceType || + community.spaceType.type !== 'community' + ) { + throw new BadRequestException('Invalid community UUID'); + } + + await this.spaceRepository.update( + { uuid: communityUuid }, + { spaceName: updateCommunityDto.communityName }, + ); + + // Fetch the updated community + const updatedCommunity = await this.spaceRepository.findOneOrFail({ + where: { uuid: communityUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedCommunity.uuid, + name: updatedCommunity.spaceName, + type: updatedCommunity.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } + } + } } From a9be4d315a4a233d5d7cee7bf86330733fa6e155 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:31:49 +0300 Subject: [PATCH 120/259] Add endpoint to rename building by UUID --- .../controllers/building.controller.ts | 26 ++++++++++ src/building/dtos/update.building.dto.ts | 16 ++++++ src/building/interface/building.interface.ts | 5 ++ src/building/services/building.service.ts | 52 ++++++++++++++++++- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/building/dtos/update.building.dto.ts diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 17e01d4..01d278c 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -7,6 +7,7 @@ import { HttpStatus, Param, Post, + Put, Query, UseGuards, } from '@nestjs/common'; @@ -14,6 +15,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddBuildingDto } from '../dtos/add.building.dto'; import { GetBuildingChildDto } from '../dtos/get.building.dto'; +import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; @ApiTags('Building Module') @Controller({ @@ -101,4 +103,28 @@ export class BuildingController { } } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename/:buildingUuid') + async renameBuildingByUuid( + @Param('buildingUuid') buildingUuid: string, + @Body() updateBuildingDto: UpdateBuildingNameDto, + ) { + try { + const building = await this.buildingService.renameBuildingByUuid( + buildingUuid, + updateBuildingDto, + ); + return building; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/building/dtos/update.building.dto.ts b/src/building/dtos/update.building.dto.ts new file mode 100644 index 0000000..0f07cbe --- /dev/null +++ b/src/building/dtos/update.building.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateBuildingNameDto { + @ApiProperty({ + description: 'buildingName', + required: true, + }) + @IsString() + @IsNotEmpty() + public buildingName: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/building/interface/building.interface.ts b/src/building/interface/building.interface.ts index 92852a7..4bdf760 100644 --- a/src/building/interface/building.interface.ts +++ b/src/building/interface/building.interface.ts @@ -19,3 +19,8 @@ export interface BuildingParentInterface { type: string; parent?: BuildingParentInterface; } +export interface RenameBuildingByUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index fbaf8cb..528d7e4 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -1,14 +1,21 @@ import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddBuildingDto } from '../dtos'; import { BuildingChildInterface, BuildingParentInterface, GetBuildingByUuidInterface, + RenameBuildingByUuidInterface, } from '../interface/building.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; +import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; @Injectable() export class BuildingService { @@ -179,4 +186,47 @@ export class BuildingService { ); } } + + async renameBuildingByUuid( + buildingUuid: string, + updateBuildingNameDto: UpdateBuildingNameDto, + ): Promise { + try { + const building = await this.spaceRepository.findOneOrFail({ + where: { uuid: buildingUuid }, + relations: ['spaceType'], + }); + + if ( + !building || + !building.spaceType || + building.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); + } + + await this.spaceRepository.update( + { uuid: buildingUuid }, + { spaceName: updateBuildingNameDto.buildingName }, + ); + + // Fetch the updated building + const updatedBuilding = await this.spaceRepository.findOneOrFail({ + where: { uuid: buildingUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedBuilding.uuid, + name: updatedBuilding.spaceName, + type: updatedBuilding.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + } + } } From 8dca50b24d35be2af16270276411d604beb04d91 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:40:03 +0300 Subject: [PATCH 121/259] Add endpoint to rename floor by UUID --- src/floor/controllers/floor.controller.ts | 27 +++++++++++++ src/floor/dtos/update.floor.dto.ts | 16 ++++++++ src/floor/interface/floor.interface.ts | 5 +++ src/floor/services/floor.service.ts | 48 ++++++++++++++++++++++- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/floor/dtos/update.floor.dto.ts diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 6e3de22..67c7b44 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -7,6 +7,7 @@ import { HttpStatus, Param, Post, + Put, Query, UseGuards, } from '@nestjs/common'; @@ -14,6 +15,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddFloorDto } from '../dtos/add.floor.dto'; import { GetFloorChildDto } from '../dtos/get.floor.dto'; +import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; @ApiTags('Floor Module') @Controller({ @@ -99,4 +101,29 @@ export class FloorController { } } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename/:floorUuid') + async renameFloorByUuid( + @Param('floorUuid') floorUuid: string, + @Body() updateFloorNameDto: UpdateFloorNameDto, + ) { + try { + const floor = await this.floorService.renameFloorByUuid( + floorUuid, + updateFloorNameDto, + ); + return floor; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/floor/dtos/update.floor.dto.ts b/src/floor/dtos/update.floor.dto.ts new file mode 100644 index 0000000..11c97b0 --- /dev/null +++ b/src/floor/dtos/update.floor.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateFloorNameDto { + @ApiProperty({ + description: 'floorName', + required: true, + }) + @IsString() + @IsNotEmpty() + public floorName: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/floor/interface/floor.interface.ts b/src/floor/interface/floor.interface.ts index b70d504..eb6a5ec 100644 --- a/src/floor/interface/floor.interface.ts +++ b/src/floor/interface/floor.interface.ts @@ -19,3 +19,8 @@ export interface FloorParentInterface { type: string; parent?: FloorParentInterface; } +export interface RenameFloorByUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index 3485b88..bbf97c4 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -1,14 +1,21 @@ import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddFloorDto } from '../dtos'; import { FloorChildInterface, FloorParentInterface, GetFloorByUuidInterface, + RenameFloorByUuidInterface, } from '../interface/floor.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; +import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; @Injectable() export class FloorService { @@ -177,4 +184,43 @@ export class FloorService { ); } } + + async renameFloorByUuid( + floorUuid: string, + updateFloorDto: UpdateFloorNameDto, + ): Promise { + try { + const floor = await this.spaceRepository.findOneOrFail({ + where: { uuid: floorUuid }, + relations: ['spaceType'], + }); + + if (!floor || !floor.spaceType || floor.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); + } + + await this.spaceRepository.update( + { uuid: floorUuid }, + { spaceName: updateFloorDto.floorName }, + ); + + // Fetch the updated floor + const updatedFloor = await this.spaceRepository.findOneOrFail({ + where: { uuid: floorUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedFloor.uuid, + name: updatedFloor.spaceName, + type: updatedFloor.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + } + } } From bf60303ddc88ad9ede708874e70a9b8293af3340 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:47:54 +0300 Subject: [PATCH 122/259] Add endpoint to rename unit by UUID --- src/unit/controllers/unit.controller.ts | 27 ++++++++++++++ src/unit/dtos/update.unit.dto.ts | 16 +++++++++ src/unit/interface/unit.interface.ts | 5 +++ src/unit/services/unit.service.ts | 48 ++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/unit/dtos/update.unit.dto.ts diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 40f35a1..cc746c8 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -7,6 +7,7 @@ import { HttpStatus, Param, Post, + Put, Query, UseGuards, } from '@nestjs/common'; @@ -14,6 +15,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddUnitDto } from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; @ApiTags('Unit Module') @Controller({ @@ -96,4 +98,29 @@ export class UnitController { } } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename/:unitUuid') + async renameUnitByUuid( + @Param('unitUuid') unitUuid: string, + @Body() updateUnitNameDto: UpdateUnitNameDto, + ) { + try { + const unit = await this.unitService.renameUnitByUuid( + unitUuid, + updateUnitNameDto, + ); + return unit; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/unit/dtos/update.unit.dto.ts b/src/unit/dtos/update.unit.dto.ts new file mode 100644 index 0000000..2d69902 --- /dev/null +++ b/src/unit/dtos/update.unit.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateUnitNameDto { + @ApiProperty({ + description: 'unitName', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitName: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts index 8db6bfa..8502a86 100644 --- a/src/unit/interface/unit.interface.ts +++ b/src/unit/interface/unit.interface.ts @@ -19,3 +19,8 @@ export interface UnitParentInterface { type: string; parent?: UnitParentInterface; } +export interface RenameUnitByUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 6969717..1bff975 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -1,14 +1,21 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddUnitDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, GetUnitByUuidInterface, + RenameUnitByUuidInterface, } from '../interface/unit.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; +import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; @Injectable() export class UnitService { @@ -179,4 +186,43 @@ export class UnitService { ); } } + + async renameUnitByUuid( + unitUuid: string, + updateUnitNameDto: UpdateUnitNameDto, + ): Promise { + try { + const unit = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); + } + + await this.spaceRepository.update( + { uuid: unitUuid }, + { spaceName: updateUnitNameDto.unitName }, + ); + + // Fetch the updated unit + const updatedUnit = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedUnit.uuid, + name: updatedUnit.spaceName, + type: updatedUnit.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } } From 53628236a63c6156f471ac7f379e441b3b5a0086 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:57:13 +0300 Subject: [PATCH 123/259] Add endpoint to rename room by UUID --- src/room/controllers/room.controller.ts | 27 ++++++++++++++ src/room/dtos/update.room.dto.ts | 16 +++++++++ src/room/interface/room.interface.ts | 5 +++ src/room/services/room.service.ts | 47 ++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/room/dtos/update.room.dto.ts diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index a998077..b5ffb15 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -7,11 +7,13 @@ import { HttpStatus, Param, Post, + Put, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto } from '../dtos/add.room.dto'; +import { UpdateRoomNameDto } from '../dtos/update.room.dto'; @ApiTags('Room Module') @Controller({ @@ -73,4 +75,29 @@ export class RoomController { } } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('rename/:roomUuid') + async renameRoomByUuid( + @Param('roomUuid') roomUuid: string, + @Body() updateRoomNameDto: UpdateRoomNameDto, + ) { + try { + const room = await this.roomService.renameRoomByUuid( + roomUuid, + updateRoomNameDto, + ); + return room; + } catch (error) { + if (error.status === 404) { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } } diff --git a/src/room/dtos/update.room.dto.ts b/src/room/dtos/update.room.dto.ts new file mode 100644 index 0000000..8f54092 --- /dev/null +++ b/src/room/dtos/update.room.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateRoomNameDto { + @ApiProperty({ + description: 'roomName', + required: true, + }) + @IsString() + @IsNotEmpty() + public roomName: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/room/interface/room.interface.ts b/src/room/interface/room.interface.ts index f98a8c9..3f1d817 100644 --- a/src/room/interface/room.interface.ts +++ b/src/room/interface/room.interface.ts @@ -12,3 +12,8 @@ export interface RoomParentInterface { type: string; parent?: RoomParentInterface; } +export interface RenameRoomByUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index ae0d403..24d5301 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -1,11 +1,18 @@ import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddRoomDto } from '../dtos'; import { RoomParentInterface, GetRoomByUuidInterface, + RenameRoomByUuidInterface, } from '../interface/room.interface'; +import { UpdateRoomNameDto } from '../dtos/update.room.dto'; @Injectable() export class RoomService { @@ -93,4 +100,42 @@ export class RoomService { ); } } + async renameRoomByUuid( + roomUuid: string, + updateRoomNameDto: UpdateRoomNameDto, + ): Promise { + try { + const room = await this.spaceRepository.findOneOrFail({ + where: { uuid: roomUuid }, + relations: ['spaceType'], + }); + + if (!room || !room.spaceType || room.spaceType.type !== 'room') { + throw new BadRequestException('Invalid room UUID'); + } + + await this.spaceRepository.update( + { uuid: roomUuid }, + { spaceName: updateRoomNameDto.roomName }, + ); + + // Fetch the updated room + const updateRoom = await this.spaceRepository.findOneOrFail({ + where: { uuid: roomUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updateRoom.uuid, + name: updateRoom.spaceName, + type: updateRoom.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } + } + } } From d92ae03eac04515913c7c2de2387dd7695d97c61 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 11:28:52 +0300 Subject: [PATCH 124/259] Refactor error handling in controllers and services --- .../controllers/building.controller.ts | 50 ++++------ src/building/services/building.service.ts | 94 +++++++++++-------- .../controllers/community.controller.ts | 38 +++----- src/community/services/community.service.ts | 71 ++++++++------ src/floor/controllers/floor.controller.ts | 54 ++++------- src/floor/services/floor.service.ts | 86 +++++++++-------- src/room/controllers/room.controller.ts | 38 +++----- src/room/services/room.service.ts | 27 +++--- src/unit/controllers/unit.controller.ts | 50 ++++------ src/unit/services/unit.service.ts | 87 +++++++++-------- 10 files changed, 287 insertions(+), 308 deletions(-) diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 01d278c..81365fb 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -35,7 +35,7 @@ export class BuildingController { } catch (error) { throw new HttpException( error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -49,14 +49,10 @@ export class BuildingController { await this.buildingService.getBuildingByUuid(buildingUuid); return building; } catch (error) { - if (error.status === 404) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -74,14 +70,10 @@ export class BuildingController { ); return building; } catch (error) { - if (error.status === 404) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @@ -93,14 +85,10 @@ export class BuildingController { await this.buildingService.getBuildingParentByUuid(buildingUuid); return building; } catch (error) { - if (error.status === 404) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @@ -117,14 +105,10 @@ export class BuildingController { ); return building; } catch (error) { - if (error.status === 404) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index 528d7e4..62081e8 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -55,8 +55,12 @@ export class BuildingService { }, relations: ['spaceType'], }); - if (!building) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + if ( + !building || + !building.spaceType || + building.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); } return { uuid: building.uuid, @@ -66,45 +70,53 @@ export class BuildingService { type: building.spaceType.type, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } } } async getBuildingChildByUuid( buildingUuid: string, getBuildingChildDto: GetBuildingChildDto, ): Promise { - const { includeSubSpaces, page, pageSize } = getBuildingChildDto; + try { + const { includeSubSpaces, page, pageSize } = getBuildingChildDto; - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: buildingUuid }, - relations: ['children', 'spaceType'], - }); + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: buildingUuid }, + relations: ['children', 'spaceType'], + }); + if (!space || !space.spaceType || space.spaceType.type !== 'building') { + throw new BadRequestException('Invalid building UUID'); + } - if (space.spaceType.type !== 'building') { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } } - - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; } private async buildHierarchy( @@ -165,10 +177,13 @@ export class BuildingService { }, relations: ['spaceType', 'parent', 'parent.spaceType'], }); - if (!building) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + if ( + !building || + !building.spaceType || + building.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); } - return { uuid: building.uuid, name: building.spaceName, @@ -180,10 +195,11 @@ export class BuildingService { }, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } } } diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 1a07791..f889e13 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -35,7 +35,7 @@ export class CommunityController { } catch (error) { throw new HttpException( error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -49,14 +49,10 @@ export class CommunityController { await this.communityService.getCommunityByUuid(communityUuid); return community; } catch (error) { - if (error.status === 404) { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -74,14 +70,10 @@ export class CommunityController { ); return community; } catch (error) { - if (error.status === 404) { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -99,14 +91,10 @@ export class CommunityController { ); return community; } catch (error) { - if (error.status === 404) { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 33cab98..5e9b7ea 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -53,8 +53,12 @@ export class CommunityService { }, relations: ['spaceType'], }); - if (!community) { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + if ( + !community || + !community.spaceType || + community.spaceType.type !== 'community' + ) { + throw new BadRequestException('Invalid community UUID'); } return { uuid: community.uuid, @@ -64,42 +68,51 @@ export class CommunityService { type: community.spaceType.type, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } } } async getCommunityChildByUuid( communityUuid: string, getCommunityChildDto: GetCommunityChildDto, ): Promise { - const { includeSubSpaces, page, pageSize } = getCommunityChildDto; + try { + const { includeSubSpaces, page, pageSize } = getCommunityChildDto; - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['children', 'spaceType'], - }); + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: communityUuid }, + relations: ['children', 'spaceType'], + }); - if (space.spaceType.type !== 'community') { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + if (!space || !space.spaceType || space.spaceType.type !== 'community') { + throw new BadRequestException('Invalid community UUID'); + } + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } } - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; } private async buildHierarchy( diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 67c7b44..87ac04a 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -25,8 +25,8 @@ import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; export class FloorController { constructor(private readonly floorService: FloorService) {} - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { @@ -35,7 +35,7 @@ export class FloorController { } catch (error) { throw new HttpException( error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -48,14 +48,10 @@ export class FloorController { const floor = await this.floorService.getFloorByUuid(floorUuid); return floor; } catch (error) { - if (error.status === 404) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -73,14 +69,10 @@ export class FloorController { ); return floor; } catch (error) { - if (error.status === 404) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @@ -91,14 +83,10 @@ export class FloorController { const floor = await this.floorService.getFloorParentByUuid(floorUuid); return floor; } catch (error) { - if (error.status === 404) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -116,14 +104,10 @@ export class FloorController { ); return floor; } catch (error) { - if (error.status === 404) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index bbf97c4..f4f0759 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -53,9 +53,10 @@ export class FloorService { }, relations: ['spaceType'], }); - if (!floor) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + if (!floor || !floor.spaceType || floor.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); } + return { uuid: floor.uuid, createdAt: floor.createdAt, @@ -64,45 +65,53 @@ export class FloorService { type: floor.spaceType.type, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } } } async getFloorChildByUuid( floorUuid: string, getFloorChildDto: GetFloorChildDto, ): Promise { - const { includeSubSpaces, page, pageSize } = getFloorChildDto; + try { + const { includeSubSpaces, page, pageSize } = getFloorChildDto; - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: floorUuid }, - relations: ['children', 'spaceType'], - }); + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: floorUuid }, + relations: ['children', 'spaceType'], + }); - if (space.spaceType.type !== 'floor') { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + if (!space || !space.spaceType || space.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); + } + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } } - - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; } private async buildHierarchy( @@ -163,8 +172,8 @@ export class FloorService { }, relations: ['spaceType', 'parent', 'parent.spaceType'], }); - if (!floor) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + if (!floor || !floor.spaceType || floor.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); } return { @@ -178,10 +187,11 @@ export class FloorService { }, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } } } diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index b5ffb15..43f6bd8 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -33,7 +33,7 @@ export class RoomController { } catch (error) { throw new HttpException( error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -46,14 +46,10 @@ export class RoomController { const room = await this.roomService.getRoomByUuid(roomUuid); return room; } catch (error) { - if (error.status === 404) { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -65,14 +61,10 @@ export class RoomController { const room = await this.roomService.getRoomParentByUuid(roomUuid); return room; } catch (error) { - if (error.status === 404) { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -90,14 +82,10 @@ export class RoomController { ); return room; } catch (error) { - if (error.status === 404) { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 24d5301..d6c8afa 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -50,9 +50,10 @@ export class RoomService { }, relations: ['spaceType'], }); - if (!room) { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + if (!room || !room.spaceType || room.spaceType.type !== 'room') { + throw new BadRequestException('Invalid room UUID'); } + return { uuid: room.uuid, createdAt: room.createdAt, @@ -61,10 +62,11 @@ export class RoomService { type: room.spaceType.type, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } } } @@ -79,8 +81,8 @@ export class RoomService { }, relations: ['spaceType', 'parent', 'parent.spaceType'], }); - if (!room) { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + if (!room || !room.spaceType || room.spaceType.type !== 'room') { + throw new BadRequestException('Invalid room UUID'); } return { @@ -94,10 +96,11 @@ export class RoomService { }, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } } } async renameRoomByUuid( diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index cc746c8..f56b9c7 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -35,7 +35,7 @@ export class UnitController { } catch (error) { throw new HttpException( error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -48,14 +48,10 @@ export class UnitController { const unit = await this.unitService.getUnitByUuid(unitUuid); return unit; } catch (error) { - if (error.status === 404) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -70,14 +66,10 @@ export class UnitController { const unit = await this.unitService.getUnitChildByUuid(unitUuid, query); return unit; } catch (error) { - if (error.status === 404) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @@ -88,14 +80,10 @@ export class UnitController { const unit = await this.unitService.getUnitParentByUuid(unitUuid); return unit; } catch (error) { - if (error.status === 404) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -113,14 +101,10 @@ export class UnitController { ); return unit; } catch (error) { - if (error.status === 404) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } else { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 1bff975..e84bc0f 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -53,8 +53,8 @@ export class UnitService { }, relations: ['spaceType'], }); - if (!unit) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); } return { uuid: unit.uuid, @@ -64,45 +64,54 @@ export class UnitService { type: unit.spaceType.type, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } } } async getUnitChildByUuid( unitUuid: string, getUnitChildDto: GetUnitChildDto, ): Promise { - const { includeSubSpaces, page, pageSize } = getUnitChildDto; + try { + const { includeSubSpaces, page, pageSize } = getUnitChildDto; - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: unitUuid }, - relations: ['children', 'spaceType'], - }); + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['children', 'spaceType'], + }); - if (space.spaceType.type !== 'unit') { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + if (!space || !space.spaceType || space.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); + } + + const totalCount = await this.spaceRepository.count({ + where: { parent: { uuid: space.uuid } }, + }); + + const children = await this.buildHierarchy( + space, + includeSubSpaces, + page, + pageSize, + ); + + return { + uuid: space.uuid, + name: space.spaceName, + type: space.spaceType.type, + totalCount, + children, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } } - - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; } private async buildHierarchy( @@ -165,10 +174,9 @@ export class UnitService { }, relations: ['spaceType', 'parent', 'parent.spaceType'], }); - if (!unit) { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); } - return { uuid: unit.uuid, name: unit.spaceName, @@ -180,10 +188,11 @@ export class UnitService { }, }; } catch (err) { - throw new HttpException( - err.message, - err.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } } } From 99aca8cf01a830f8e9fbc2925a8deb9b9ca127d1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:13:56 +0300 Subject: [PATCH 125/259] Refactor middleware to guards for type checking --- src/building/building.module.ts | 17 +---- .../controllers/building.controller.ts | 3 +- src/building/services/building.service.ts | 9 ++- src/floor/controllers/floor.controller.ts | 3 +- src/floor/floor.module.ts | 17 +---- src/guards/building.type.guard.ts | 66 +++++++++++++++++ src/guards/community.type.guard.ts | 67 +++++++++++++++++ src/guards/floor.type.guard.ts | 66 +++++++++++++++++ src/guards/unit.type.guard.ts | 66 +++++++++++++++++ src/middleware/CheckBuildingTypeMiddleware.ts | 74 ------------------- .../CheckCommunityTypeMiddleware.ts | 74 ------------------- src/middleware/CheckFloorTypeMiddleware.ts | 74 ------------------- src/middleware/CheckUnitTypeMiddleware.ts | 74 ------------------- src/room/controllers/room.controller.ts | 3 +- src/room/room.module.ts | 17 +---- src/unit/controllers/unit.controller.ts | 3 +- src/unit/unit.module.ts | 17 +---- 17 files changed, 289 insertions(+), 361 deletions(-) create mode 100644 src/guards/building.type.guard.ts create mode 100644 src/guards/community.type.guard.ts create mode 100644 src/guards/floor.type.guard.ts create mode 100644 src/guards/unit.type.guard.ts delete mode 100644 src/middleware/CheckBuildingTypeMiddleware.ts delete mode 100644 src/middleware/CheckCommunityTypeMiddleware.ts delete mode 100644 src/middleware/CheckFloorTypeMiddleware.ts delete mode 100644 src/middleware/CheckUnitTypeMiddleware.ts diff --git a/src/building/building.module.ts b/src/building/building.module.ts index 2bdeb7e..7574f81 100644 --- a/src/building/building.module.ts +++ b/src/building/building.module.ts @@ -1,9 +1,4 @@ -import { - MiddlewareConsumer, - Module, - NestModule, - RequestMethod, -} from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { BuildingService } from './services/building.service'; import { BuildingController } from './controllers/building.controller'; import { ConfigModule } from '@nestjs/config'; @@ -11,7 +6,6 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; -import { CheckCommunityTypeMiddleware } from 'src/middleware/CheckCommunityTypeMiddleware'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], @@ -19,11 +13,4 @@ import { CheckCommunityTypeMiddleware } from 'src/middleware/CheckCommunityTypeM providers: [BuildingService, SpaceRepository, SpaceTypeRepository], exports: [BuildingService], }) -export class BuildingModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(CheckCommunityTypeMiddleware).forRoutes({ - path: '/building', - method: RequestMethod.POST, - }); - } -} +export class BuildingModule {} diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 81365fb..ec4a616 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -16,6 +16,7 @@ import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddBuildingDto } from '../dtos/add.building.dto'; import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; +import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; @ApiTags('Building Module') @Controller({ @@ -26,7 +27,7 @@ export class BuildingController { constructor(private readonly buildingService: BuildingService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index 62081e8..7bfd33c 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -32,13 +32,20 @@ export class BuildingService { }, }); + if (!spaceType) { + throw new BadRequestException('Invalid building UUID'); + } await this.spaceRepository.save({ spaceName: addBuildingDto.buildingName, parent: { uuid: addBuildingDto.communityUuid }, spaceType: { uuid: spaceType.uuid }, }); } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } } } diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 87ac04a..5c1021a 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -16,6 +16,7 @@ import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddFloorDto } from '../dtos/add.floor.dto'; import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; +import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; @ApiTags('Floor Module') @Controller({ @@ -26,7 +27,7 @@ export class FloorController { constructor(private readonly floorService: FloorService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { diff --git a/src/floor/floor.module.ts b/src/floor/floor.module.ts index 199f876..7085659 100644 --- a/src/floor/floor.module.ts +++ b/src/floor/floor.module.ts @@ -1,9 +1,4 @@ -import { - MiddlewareConsumer, - Module, - NestModule, - RequestMethod, -} from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { FloorService } from './services/floor.service'; import { FloorController } from './controllers/floor.controller'; import { ConfigModule } from '@nestjs/config'; @@ -11,7 +6,6 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; -import { CheckBuildingTypeMiddleware } from 'src/middleware/CheckBuildingTypeMiddleware'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], @@ -19,11 +13,4 @@ import { CheckBuildingTypeMiddleware } from 'src/middleware/CheckBuildingTypeMid providers: [FloorService, SpaceRepository, SpaceTypeRepository], exports: [FloorService], }) -export class FloorModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(CheckBuildingTypeMiddleware).forRoutes({ - path: '/floor', - method: RequestMethod.POST, - }); - } -} +export class FloorModule {} diff --git a/src/guards/building.type.guard.ts b/src/guards/building.type.guard.ts new file mode 100644 index 0000000..1e36d9f --- /dev/null +++ b/src/guards/building.type.guard.ts @@ -0,0 +1,66 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + BadRequestException, + ExecutionContext, +} from '@nestjs/common'; + +@Injectable() +export class CheckBuildingTypeGuard implements CanActivate { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { floorName, buildingUuid } = req.body; + + if (!floorName) { + throw new BadRequestException('floorName is required'); + } + + if (!buildingUuid) { + throw new BadRequestException('buildingUuid is required'); + } + + await this.checkBuildingIsBuildingType(buildingUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkBuildingIsBuildingType(buildingUuid: string) { + const buildingData = await this.spaceRepository.findOne({ + where: { uuid: buildingUuid }, + relations: ['spaceType'], + }); + if ( + !buildingData || + !buildingData.spaceType || + buildingData.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Building not found', + }); + } + } +} diff --git a/src/guards/community.type.guard.ts b/src/guards/community.type.guard.ts new file mode 100644 index 0000000..e8212f4 --- /dev/null +++ b/src/guards/community.type.guard.ts @@ -0,0 +1,67 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException } from '@nestjs/common'; + +@Injectable() +export class CheckCommunityTypeGuard implements CanActivate { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { buildingName, communityUuid } = req.body; + + if (!buildingName) { + throw new BadRequestException('buildingName is required'); + } + + if (!communityUuid) { + throw new BadRequestException('communityUuid is required'); + } + + await this.checkCommunityIsCommunityType(communityUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkCommunityIsCommunityType(communityUuid: string) { + const communityData = await this.spaceRepository.findOne({ + where: { uuid: communityUuid }, + relations: ['spaceType'], + }); + + if ( + !communityData || + !communityData.spaceType || + communityData.spaceType.type !== 'community' + ) { + throw new BadRequestException('Invalid community UUID'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Community not found', + }); + } + } +} diff --git a/src/guards/floor.type.guard.ts b/src/guards/floor.type.guard.ts new file mode 100644 index 0000000..8064cb2 --- /dev/null +++ b/src/guards/floor.type.guard.ts @@ -0,0 +1,66 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; + +@Injectable() +export class CheckFloorTypeGuard implements CanActivate { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + try { + const { unitName, floorUuid } = req.body; + + if (!unitName) { + throw new BadRequestException('unitName is required'); + } + + if (!floorUuid) { + throw new BadRequestException('floorUuid is required'); + } + + await this.checkFloorIsFloorType(floorUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkFloorIsFloorType(floorUuid: string) { + const floorData = await this.spaceRepository.findOne({ + where: { uuid: floorUuid }, + relations: ['spaceType'], + }); + + if ( + !floorData || + !floorData.spaceType || + floorData.spaceType.type !== 'floor' + ) { + throw new BadRequestException('Invalid floor UUID'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Floor not found', + }); + } + } +} diff --git a/src/guards/unit.type.guard.ts b/src/guards/unit.type.guard.ts new file mode 100644 index 0000000..f1e292f --- /dev/null +++ b/src/guards/unit.type.guard.ts @@ -0,0 +1,66 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + BadRequestException, + ExecutionContext, +} from '@nestjs/common'; + +@Injectable() +export class CheckUnitTypeGuard implements CanActivate { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + try { + const { roomName, unitUuid } = req.body; + + if (!roomName) { + throw new BadRequestException('roomName is required'); + } + + if (!unitUuid) { + throw new BadRequestException('unitUuid is required'); + } + + await this.checkFloorIsFloorType(unitUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkFloorIsFloorType(unitUuid: string) { + const unitData = await this.spaceRepository.findOne({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + if ( + !unitData || + !unitData.spaceType || + unitData.spaceType.type !== 'unit' + ) { + throw new BadRequestException('Invalid unit UUID'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Unit not found', + }); + } + } +} diff --git a/src/middleware/CheckBuildingTypeMiddleware.ts b/src/middleware/CheckBuildingTypeMiddleware.ts deleted file mode 100644 index 170cb9b..0000000 --- a/src/middleware/CheckBuildingTypeMiddleware.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { - Injectable, - NestMiddleware, - HttpStatus, - HttpException, -} from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; - -@Injectable() -export class CheckBuildingTypeMiddleware implements NestMiddleware { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async use(req: Request, res: Response, next: NextFunction) { - try { - // Destructure request body for cleaner code - const { floorName, buildingUuid } = req.body; - - // Guard clauses for early return - if (!floorName) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'floorName is required', - }); - } - - if (!buildingUuid) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'buildingUuid is required', - }); - } - - // Call function to check if building is a building - await this.checkBuildingIsBuildingType(buildingUuid); - - // Call next middleware - next(); - } catch (error) { - // Handle errors - this.handleMiddlewareError(error, res); - } - } - - async checkBuildingIsBuildingType(buildingUuid: string) { - const buildingData = await this.spaceRepository.findOne({ - where: { uuid: buildingUuid }, - relations: ['spaceType'], - }); - - // Throw error if building not found - if (!buildingData) { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - - // Throw error if building is not of type 'building' - if (buildingData.spaceType.type !== 'building') { - throw new HttpException( - "buildingUuid is not of type 'building'", - HttpStatus.BAD_REQUEST, - ); - } - } - - // Function to handle middleware errors - private handleMiddlewareError(error: Error, res: Response) { - const status = - error instanceof HttpException - ? error.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - const message = error.message || 'Internal server error'; - res.status(status).json({ statusCode: status, message }); - } -} diff --git a/src/middleware/CheckCommunityTypeMiddleware.ts b/src/middleware/CheckCommunityTypeMiddleware.ts deleted file mode 100644 index 3eef679..0000000 --- a/src/middleware/CheckCommunityTypeMiddleware.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { - Injectable, - NestMiddleware, - HttpStatus, - HttpException, -} from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; - -@Injectable() -export class CheckCommunityTypeMiddleware implements NestMiddleware { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async use(req: Request, res: Response, next: NextFunction) { - try { - // Destructure request body for cleaner code - const { buildingName, communityUuid } = req.body; - - // Guard clauses for early return - if (!buildingName) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'buildingName is required', - }); - } - - if (!communityUuid) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'communityUuid is required', - }); - } - - // Call function to check if community is a building - await this.checkCommunityIsCommunityType(communityUuid); - - // Call next middleware - next(); - } catch (error) { - // Handle errors - this.handleMiddlewareError(error, res); - } - } - - async checkCommunityIsCommunityType(communityUuid: string) { - const communityData = await this.spaceRepository.findOne({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - // Throw error if community not found - if (!communityData) { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } - - // Throw error if community is not of type 'community' - if (communityData.spaceType.type !== 'community') { - throw new HttpException( - "communityUuid is not of type 'community'", - HttpStatus.BAD_REQUEST, - ); - } - } - - // Function to handle middleware errors - private handleMiddlewareError(error: Error, res: Response) { - const status = - error instanceof HttpException - ? error.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - const message = error.message || 'Internal server error'; - res.status(status).json({ statusCode: status, message }); - } -} diff --git a/src/middleware/CheckFloorTypeMiddleware.ts b/src/middleware/CheckFloorTypeMiddleware.ts deleted file mode 100644 index e096750..0000000 --- a/src/middleware/CheckFloorTypeMiddleware.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { - Injectable, - NestMiddleware, - HttpStatus, - HttpException, -} from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; - -@Injectable() -export class CheckFloorTypeMiddleware implements NestMiddleware { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async use(req: Request, res: Response, next: NextFunction) { - try { - // Destructure request body for cleaner code - const { unitName, floorUuid } = req.body; - - // Guard clauses for early return - if (!unitName) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'unitName is required', - }); - } - - if (!floorUuid) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'floorUuid is required', - }); - } - - // Call function to check if floor is a floor - await this.checkFloorIsFloorType(floorUuid); - - // Call next middleware - next(); - } catch (error) { - // Handle errors - this.handleMiddlewareError(error, res); - } - } - - async checkFloorIsFloorType(floorUuid: string) { - const floorData = await this.spaceRepository.findOne({ - where: { uuid: floorUuid }, - relations: ['spaceType'], - }); - - // Throw error if floor not found - if (!floorData) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - - // Throw error if floor is not of type 'floor' - if (floorData.spaceType.type !== 'floor') { - throw new HttpException( - "floorUuid is not of type 'floor'", - HttpStatus.BAD_REQUEST, - ); - } - } - - // Function to handle middleware errors - private handleMiddlewareError(error: Error, res: Response) { - const status = - error instanceof HttpException - ? error.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - const message = error.message || 'Internal server error'; - res.status(status).json({ statusCode: status, message }); - } -} diff --git a/src/middleware/CheckUnitTypeMiddleware.ts b/src/middleware/CheckUnitTypeMiddleware.ts deleted file mode 100644 index 7f5ba6a..0000000 --- a/src/middleware/CheckUnitTypeMiddleware.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { - Injectable, - NestMiddleware, - HttpStatus, - HttpException, -} from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; - -@Injectable() -export class CheckUnitTypeMiddleware implements NestMiddleware { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async use(req: Request, res: Response, next: NextFunction) { - try { - // Destructure request body for cleaner code - const { roomName, unitUuid } = req.body; - - // Guard clauses for early return - if (!roomName) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'roomName is required', - }); - } - - if (!unitUuid) { - return res.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'unitUuid is required', - }); - } - - // Call function to check if unit is a unit - await this.checkFloorIsFloorType(unitUuid); - - // Call next middleware - next(); - } catch (error) { - // Handle errors - this.handleMiddlewareError(error, res); - } - } - - async checkFloorIsFloorType(unitUuid: string) { - const unitData = await this.spaceRepository.findOne({ - where: { uuid: unitUuid }, - relations: ['spaceType'], - }); - - // Throw error if unit not found - if (!unitData) { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - - // Throw error if unit is not of type 'unit' - if (unitData.spaceType.type !== 'unit') { - throw new HttpException( - "unitUuid is not of type 'unit'", - HttpStatus.BAD_REQUEST, - ); - } - } - - // Function to handle middleware errors - private handleMiddlewareError(error: Error, res: Response) { - const status = - error instanceof HttpException - ? error.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - const message = error.message || 'Internal server error'; - res.status(status).json({ statusCode: status, message }); - } -} diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 43f6bd8..b0e4e3f 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -14,6 +14,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; +import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; @ApiTags('Room Module') @Controller({ @@ -24,7 +25,7 @@ export class RoomController { constructor(private readonly roomService: RoomService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { diff --git a/src/room/room.module.ts b/src/room/room.module.ts index 660fbec..8411cd4 100644 --- a/src/room/room.module.ts +++ b/src/room/room.module.ts @@ -1,9 +1,4 @@ -import { - MiddlewareConsumer, - Module, - NestModule, - RequestMethod, -} from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { RoomService } from './services/room.service'; import { RoomController } from './controllers/room.controller'; import { ConfigModule } from '@nestjs/config'; @@ -11,7 +6,6 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; -import { CheckUnitTypeMiddleware } from 'src/middleware/CheckUnitTypeMiddleware'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], @@ -19,11 +13,4 @@ import { CheckUnitTypeMiddleware } from 'src/middleware/CheckUnitTypeMiddleware' providers: [RoomService, SpaceRepository, SpaceTypeRepository], exports: [RoomService], }) -export class RoomModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(CheckUnitTypeMiddleware).forRoutes({ - path: '/room', - method: RequestMethod.POST, - }); - } -} +export class RoomModule {} diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index f56b9c7..f36a510 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -16,6 +16,7 @@ import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddUnitDto } from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; +import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @ApiTags('Unit Module') @Controller({ @@ -26,7 +27,7 @@ export class UnitController { constructor(private readonly unitService: UnitService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts index 0ebee41..eaf571f 100644 --- a/src/unit/unit.module.ts +++ b/src/unit/unit.module.ts @@ -1,9 +1,4 @@ -import { - MiddlewareConsumer, - Module, - NestModule, - RequestMethod, -} from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { UnitService } from './services/unit.service'; import { UnitController } from './controllers/unit.controller'; import { ConfigModule } from '@nestjs/config'; @@ -11,7 +6,6 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; -import { CheckFloorTypeMiddleware } from 'src/middleware/CheckFloorTypeMiddleware'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], @@ -19,11 +13,4 @@ import { CheckFloorTypeMiddleware } from 'src/middleware/CheckFloorTypeMiddlewar providers: [UnitService, SpaceRepository, SpaceTypeRepository], exports: [UnitService], }) -export class UnitModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(CheckFloorTypeMiddleware).forRoutes({ - path: '/unit', - method: RequestMethod.POST, - }); - } -} +export class UnitModule {} From 9837227fc572ba4dd7ba15b8db2c2232fb383aed Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:11:18 +0300 Subject: [PATCH 126/259] Add user space DTO, entity, repository, and module --- .../src/modules/user-space/dtos/index.ts | 1 + .../modules/user-space/dtos/user.space.dto.ts | 15 ++++++++++ .../src/modules/user-space/entities/index.ts | 1 + .../user-space/entities/user.space.entity.ts | 28 +++++++++++++++++++ .../modules/user-space/repositories/index.ts | 1 + .../repositories/user.space.repository.ts | 10 +++++++ .../user.space.repository.module.ts | 11 ++++++++ 7 files changed, 67 insertions(+) create mode 100644 libs/common/src/modules/user-space/dtos/index.ts create mode 100644 libs/common/src/modules/user-space/dtos/user.space.dto.ts create mode 100644 libs/common/src/modules/user-space/entities/index.ts create mode 100644 libs/common/src/modules/user-space/entities/user.space.entity.ts create mode 100644 libs/common/src/modules/user-space/repositories/index.ts create mode 100644 libs/common/src/modules/user-space/repositories/user.space.repository.ts create mode 100644 libs/common/src/modules/user-space/user.space.repository.module.ts diff --git a/libs/common/src/modules/user-space/dtos/index.ts b/libs/common/src/modules/user-space/dtos/index.ts new file mode 100644 index 0000000..41572f5 --- /dev/null +++ b/libs/common/src/modules/user-space/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.space.dto'; diff --git a/libs/common/src/modules/user-space/dtos/user.space.dto.ts b/libs/common/src/modules/user-space/dtos/user.space.dto.ts new file mode 100644 index 0000000..b9ef4d0 --- /dev/null +++ b/libs/common/src/modules/user-space/dtos/user.space.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserSpaceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public spaceUuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/libs/common/src/modules/user-space/entities/index.ts b/libs/common/src/modules/user-space/entities/index.ts new file mode 100644 index 0000000..ef6849a --- /dev/null +++ b/libs/common/src/modules/user-space/entities/index.ts @@ -0,0 +1 @@ +export * from './user.space.entity'; diff --git a/libs/common/src/modules/user-space/entities/user.space.entity.ts b/libs/common/src/modules/user-space/entities/user.space.entity.ts new file mode 100644 index 0000000..c41dfc0 --- /dev/null +++ b/libs/common/src/modules/user-space/entities/user.space.entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { UserSpaceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from '../../space/entities'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'user-space' }) +export class UserSpaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) + user: UserEntity; + + @ManyToOne(() => SpaceEntity, (space) => space.userSpaces, { + nullable: false, + }) + space: SpaceEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user-space/repositories/index.ts b/libs/common/src/modules/user-space/repositories/index.ts new file mode 100644 index 0000000..3ad6d48 --- /dev/null +++ b/libs/common/src/modules/user-space/repositories/index.ts @@ -0,0 +1 @@ +export * from './user.space.repository'; diff --git a/libs/common/src/modules/user-space/repositories/user.space.repository.ts b/libs/common/src/modules/user-space/repositories/user.space.repository.ts new file mode 100644 index 0000000..b4b7507 --- /dev/null +++ b/libs/common/src/modules/user-space/repositories/user.space.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserSpaceEntity } from '../entities/user.space.entity'; + +@Injectable() +export class UserSpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserSpaceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user-space/user.space.repository.module.ts b/libs/common/src/modules/user-space/user.space.repository.module.ts new file mode 100644 index 0000000..9655252 --- /dev/null +++ b/libs/common/src/modules/user-space/user.space.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserSpaceEntity } from './entities/user.space.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserSpaceEntity])], +}) +export class UserSpaceRepositoryModule {} From 6673bcd913c25edc99db54fe9b3f5c157b857540 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:11:36 +0300 Subject: [PATCH 127/259] Add UserSpaceEntity relation to SpaceEntity and UserEntity --- libs/common/src/modules/space/entities/space.entity.ts | 4 ++++ libs/common/src/modules/user/entities/index.ts | 1 + libs/common/src/modules/user/entities/user.entity.ts | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 1f523f3..af6922e 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceTypeEntity } from '../../space-type/entities'; +import { UserSpaceEntity } from '../../user-space/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -26,6 +27,9 @@ export class SpaceEntity extends AbstractEntity { }) spaceType: SpaceTypeEntity; + @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.space) + userSpaces: UserSpaceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/user/entities/index.ts b/libs/common/src/modules/user/entities/index.ts index e69de29..e4aa507 100644 --- a/libs/common/src/modules/user/entities/index.ts +++ b/libs/common/src/modules/user/entities/index.ts @@ -0,0 +1 @@ +export * from './user.entity'; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 63f2185..3212662 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserSpaceEntity } from '../../user-space/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -36,6 +37,9 @@ export class UserEntity extends AbstractEntity { }) public isUserVerified: boolean; + @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) + userSpaces: UserSpaceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); From 35b41e1be189ededf2d9607d2b82bb5c3e4bf9a7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:11:47 +0300 Subject: [PATCH 128/259] Add UserSpaceEntity to database module imports --- libs/common/src/database/database.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index c604089..aa4f781 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -9,6 +9,7 @@ import { HomeEntity } from '../modules/home/entities'; import { ProductEntity } from '../modules/product/entities'; import { SpaceEntity } from '../modules/space/entities'; import { SpaceTypeEntity } from '../modules/space-type/entities'; +import { UserSpaceEntity } from '../modules/user-space/entities'; @Module({ imports: [ @@ -31,6 +32,7 @@ import { SpaceTypeEntity } from '../modules/space-type/entities'; ProductEntity, SpaceEntity, SpaceTypeEntity, + UserSpaceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), From c12b11b81d922c7fb88cf1b6b26d7e5033e8c26f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:33:28 +0300 Subject: [PATCH 129/259] Add UserSpaceRepositoryModule and UserSpaceRepository to BuildingModule --- src/building/building.module.ts | 16 +++++++- .../controllers/building.controller.ts | 23 +++++++++++- src/building/dtos/get.building.dto.ts | 10 +++++ src/building/interface/building.interface.ts | 5 +++ src/building/services/building.service.ts | 37 +++++++++++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/building/building.module.ts b/src/building/building.module.ts index 7574f81..5507453 100644 --- a/src/building/building.module.ts +++ b/src/building/building.module.ts @@ -6,11 +6,23 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + imports: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + ], controllers: [BuildingController], - providers: [BuildingService, SpaceRepository, SpaceTypeRepository], + providers: [ + BuildingService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + ], exports: [BuildingService], }) export class BuildingModule {} diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index ec4a616..9e7a873 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -10,11 +10,15 @@ import { Put, Query, UseGuards, + ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddBuildingDto } from '../dtos/add.building.dto'; -import { GetBuildingChildDto } from '../dtos/get.building.dto'; +import { + GetBuildingByUserIdDto, + GetBuildingChildDto, +} from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; @@ -92,6 +96,23 @@ export class BuildingController { ); } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async getBuildingsByUserId( + @Query(ValidationPipe) dto: GetBuildingByUserIdDto, + ) { + try { + return await this.buildingService.getBuildingsByUserId(dto.userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:buildingUuid') diff --git a/src/building/dtos/get.building.dto.ts b/src/building/dtos/get.building.dto.ts index f762469..720201a 100644 --- a/src/building/dtos/get.building.dto.ts +++ b/src/building/dtos/get.building.dto.ts @@ -49,3 +49,13 @@ export class GetBuildingChildDto { }) public includeSubSpaces: boolean = false; } + +export class GetBuildingByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/src/building/interface/building.interface.ts b/src/building/interface/building.interface.ts index 4bdf760..1127456 100644 --- a/src/building/interface/building.interface.ts +++ b/src/building/interface/building.interface.ts @@ -24,3 +24,8 @@ export interface RenameBuildingByUuidInterface { name: string; type: string; } +export interface GetBuildingByUserUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index 7bfd33c..726697b 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -11,17 +11,20 @@ import { AddBuildingDto } from '../dtos'; import { BuildingChildInterface, BuildingParentInterface, + GetBuildingByUserUuidInterface, GetBuildingByUuidInterface, RenameBuildingByUuidInterface, } from '../interface/building.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Injectable() export class BuildingService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async addBuilding(addBuildingDto: AddBuildingDto) { @@ -210,6 +213,40 @@ export class BuildingService { } } + async getBuildingsByUserId( + userUuid: string, + ): Promise { + try { + const buildings = await this.userSpaceRepository.find({ + relations: ['space', 'space.spaceType'], + where: { + user: { uuid: userUuid }, + space: { spaceType: { type: 'building' } }, + }, + }); + + if (buildings.length === 0) { + throw new HttpException( + 'this user has no buildings', + HttpStatus.NOT_FOUND, + ); + } + const spaces = buildings.map((building) => ({ + uuid: building.space.uuid, + name: building.space.spaceName, + type: building.space.spaceType.type, + })); + + return spaces; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException('user not found', HttpStatus.NOT_FOUND); + } + } + } + async renameBuildingByUuid( buildingUuid: string, updateBuildingNameDto: UpdateBuildingNameDto, From 230ed4eac1f69118811abb2202c00ff5eae7f825 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:33:37 +0300 Subject: [PATCH 130/259] Add UserSpaceRepositoryModule and UserSpaceRepository to CommunityModule --- src/community/community.module.ts | 16 +++++++- .../controllers/community.controller.ts | 22 ++++++++++- src/community/dtos/get.community.dto.ts | 10 +++++ .../interface/community.interface.ts | 6 +++ src/community/services/community.service.ts | 37 +++++++++++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index 36b9da2..de5a7c0 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -6,11 +6,23 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + imports: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + ], controllers: [CommunityController], - providers: [CommunityService, SpaceRepository, SpaceTypeRepository], + providers: [ + CommunityService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + ], exports: [CommunityService], }) export class CommunityModule {} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index f889e13..ea9805c 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -10,11 +10,15 @@ import { Put, Query, UseGuards, + ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddCommunityDto } from '../dtos/add.community.dto'; -import { GetCommunityChildDto } from '../dtos/get.community.dto'; +import { + GetCommunitiesByUserIdDto, + GetCommunityChildDto, +} from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @ApiTags('Community Module') @@ -77,6 +81,22 @@ export class CommunityController { } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async getCommunitiesByUserId( + @Query(ValidationPipe) dto: GetCommunitiesByUserIdDto, + ) { + try { + return await this.communityService.getCommunitiesByUserId(dto.userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:communityUuid') diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts index be614e5..5d0e08c 100644 --- a/src/community/dtos/get.community.dto.ts +++ b/src/community/dtos/get.community.dto.ts @@ -49,3 +49,13 @@ export class GetCommunityChildDto { }) public includeSubSpaces: boolean = false; } + +export class GetCommunitiesByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/src/community/interface/community.interface.ts b/src/community/interface/community.interface.ts index ca18aca..31c0579 100644 --- a/src/community/interface/community.interface.ts +++ b/src/community/interface/community.interface.ts @@ -18,3 +18,9 @@ export interface RenameCommunityByUuidInterface { name: string; type: string; } + +export interface GetCommunityByUserUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 5e9b7ea..1c3109a 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -10,17 +10,20 @@ import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto } from '../dtos'; import { CommunityChildInterface, + GetCommunityByUserUuidInterface, GetCommunityByUuidInterface, RenameCommunityByUuidInterface, } from '../interface/community.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Injectable() export class CommunityService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async addCommunity(addCommunityDto: AddCommunityDto) { @@ -151,6 +154,40 @@ export class CommunityService { return childHierarchies; } + + async getCommunitiesByUserId( + userUuid: string, + ): Promise { + try { + const communities = await this.userSpaceRepository.find({ + relations: ['space', 'space.spaceType'], + where: { + user: { uuid: userUuid }, + space: { spaceType: { type: 'community' } }, + }, + }); + + if (communities.length === 0) { + throw new HttpException( + 'this user has no communities', + HttpStatus.NOT_FOUND, + ); + } + const spaces = communities.map((community) => ({ + uuid: community.space.uuid, + name: community.space.spaceName, + type: community.space.spaceType.type, + })); + + return spaces; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException('user not found', HttpStatus.NOT_FOUND); + } + } + } async renameCommunityByUuid( communityUuid: string, updateCommunityDto: UpdateCommunityNameDto, From c03bb1ef3677f4b7e9c983a23705045380673c10 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:33:53 +0300 Subject: [PATCH 131/259] Add GetFloorsByUserId endpoint and related functionality --- src/floor/controllers/floor.controller.ts | 17 ++++++++++- src/floor/dtos/get.floor.dto.ts | 10 ++++++ src/floor/floor.module.ts | 16 ++++++++-- src/floor/interface/floor.interface.ts | 6 ++++ src/floor/services/floor.service.ts | 37 +++++++++++++++++++++++ 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 5c1021a..c1fa66f 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -10,11 +10,12 @@ import { Put, Query, UseGuards, + ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddFloorDto } from '../dtos/add.floor.dto'; -import { GetFloorChildDto } from '../dtos/get.floor.dto'; +import { GetFloorByUserIdDto, GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; @@ -91,6 +92,20 @@ export class FloorController { } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async getFloorsByUserId(@Query(ValidationPipe) dto: GetFloorByUserIdDto) { + try { + return await this.floorService.getFloorsByUserId(dto.userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:floorUuid') diff --git a/src/floor/dtos/get.floor.dto.ts b/src/floor/dtos/get.floor.dto.ts index 23a8e56..40d0aed 100644 --- a/src/floor/dtos/get.floor.dto.ts +++ b/src/floor/dtos/get.floor.dto.ts @@ -49,3 +49,13 @@ export class GetFloorChildDto { }) public includeSubSpaces: boolean = false; } + +export class GetFloorByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/src/floor/floor.module.ts b/src/floor/floor.module.ts index 7085659..f247fea 100644 --- a/src/floor/floor.module.ts +++ b/src/floor/floor.module.ts @@ -6,11 +6,23 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + imports: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + ], controllers: [FloorController], - providers: [FloorService, SpaceRepository, SpaceTypeRepository], + providers: [ + FloorService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + ], exports: [FloorService], }) export class FloorModule {} diff --git a/src/floor/interface/floor.interface.ts b/src/floor/interface/floor.interface.ts index eb6a5ec..37f35c4 100644 --- a/src/floor/interface/floor.interface.ts +++ b/src/floor/interface/floor.interface.ts @@ -24,3 +24,9 @@ export interface RenameFloorByUuidInterface { name: string; type: string; } + +export interface GetFloorByUserUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index f4f0759..0cf19da 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -11,17 +11,20 @@ import { AddFloorDto } from '../dtos'; import { FloorChildInterface, FloorParentInterface, + GetFloorByUserUuidInterface, GetFloorByUuidInterface, RenameFloorByUuidInterface, } from '../interface/floor.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Injectable() export class FloorService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async addFloor(addFloorDto: AddFloorDto) { @@ -195,6 +198,40 @@ export class FloorService { } } + async getFloorsByUserId( + userUuid: string, + ): Promise { + try { + const floors = await this.userSpaceRepository.find({ + relations: ['space', 'space.spaceType'], + where: { + user: { uuid: userUuid }, + space: { spaceType: { type: 'floor' } }, + }, + }); + + if (floors.length === 0) { + throw new HttpException( + 'this user has no floors', + HttpStatus.NOT_FOUND, + ); + } + const spaces = floors.map((floor) => ({ + uuid: floor.space.uuid, + name: floor.space.spaceName, + type: floor.space.spaceType.type, + })); + + return spaces; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException('user not found', HttpStatus.NOT_FOUND); + } + } + } + async renameFloorByUuid( floorUuid: string, updateFloorDto: UpdateFloorNameDto, From 5c3edb0bab5a464c76aa1ae0abe4f37de69b2441 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:34:03 +0300 Subject: [PATCH 132/259] Add endpoint to get rooms by user ID --- src/room/controllers/room.controller.ts | 17 ++++++++++++ src/room/dtos/get.room.dto.ts | 12 +++++++++ src/room/interface/room.interface.ts | 5 ++++ src/room/room.module.ts | 16 +++++++++-- src/room/services/room.service.ts | 35 +++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/room/dtos/get.room.dto.ts diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index b0e4e3f..80ef7cf 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -8,13 +8,16 @@ import { Param, Post, Put, + Query, UseGuards, + ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; +import { GetRoomByUserIdDto } from '../dtos/get.room.dto'; @ApiTags('Room Module') @Controller({ @@ -69,6 +72,20 @@ export class RoomController { } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async getRoomsByUserId(@Query(ValidationPipe) dto: GetRoomByUserIdDto) { + try { + return await this.roomService.getRoomsByUserId(dto.userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:roomUuid') diff --git a/src/room/dtos/get.room.dto.ts b/src/room/dtos/get.room.dto.ts new file mode 100644 index 0000000..f919b85 --- /dev/null +++ b/src/room/dtos/get.room.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetRoomByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/src/room/interface/room.interface.ts b/src/room/interface/room.interface.ts index 3f1d817..49473a3 100644 --- a/src/room/interface/room.interface.ts +++ b/src/room/interface/room.interface.ts @@ -17,3 +17,8 @@ export interface RenameRoomByUuidInterface { name: string; type: string; } +export interface GetRoomByUserUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/room/room.module.ts b/src/room/room.module.ts index 8411cd4..d83d6fb 100644 --- a/src/room/room.module.ts +++ b/src/room/room.module.ts @@ -6,11 +6,23 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + imports: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + ], controllers: [RoomController], - providers: [RoomService, SpaceRepository, SpaceTypeRepository], + providers: [ + RoomService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + ], exports: [RoomService], }) export class RoomModule {} diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index d6c8afa..7b0b7f5 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -11,14 +11,17 @@ import { RoomParentInterface, GetRoomByUuidInterface, RenameRoomByUuidInterface, + GetRoomByUserUuidInterface, } from '../interface/room.interface'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Injectable() export class RoomService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async addRoom(addRoomDto: AddRoomDto) { @@ -103,6 +106,38 @@ export class RoomService { } } } + + async getRoomsByUserId( + userUuid: string, + ): Promise { + try { + const rooms = await this.userSpaceRepository.find({ + relations: ['space', 'space.spaceType'], + where: { + user: { uuid: userUuid }, + space: { spaceType: { type: 'room' } }, + }, + }); + + if (rooms.length === 0) { + throw new HttpException('this user has no rooms', HttpStatus.NOT_FOUND); + } + const spaces = rooms.map((room) => ({ + uuid: room.space.uuid, + name: room.space.spaceName, + type: room.space.spaceType.type, + })); + + return spaces; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException('user not found', HttpStatus.NOT_FOUND); + } + } + } + async renameRoomByUuid( roomUuid: string, updateRoomNameDto: UpdateRoomNameDto, From f30dd755a201549151422cb4bd5d93e7c53ba97d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:34:15 +0300 Subject: [PATCH 133/259] Add endpoint to get units by user ID --- src/unit/controllers/unit.controller.ts | 17 ++++++++++++- src/unit/dtos/get.unit.dto.ts | 9 +++++++ src/unit/interface/unit.interface.ts | 5 ++++ src/unit/services/unit.service.ts | 32 +++++++++++++++++++++++++ src/unit/unit.module.ts | 16 +++++++++++-- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index f36a510..245dd85 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -10,11 +10,12 @@ import { Put, Query, UseGuards, + ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddUnitDto } from '../dtos/add.unit.dto'; -import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { GetUnitByUserIdDto, GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @@ -88,6 +89,20 @@ export class UnitController { } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async getUnitsByUserId(@Query(ValidationPipe) dto: GetUnitByUserIdDto) { + try { + return await this.unitService.getUnitsByUserId(dto.userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:unitUuid') diff --git a/src/unit/dtos/get.unit.dto.ts b/src/unit/dtos/get.unit.dto.ts index b96147b..82837aa 100644 --- a/src/unit/dtos/get.unit.dto.ts +++ b/src/unit/dtos/get.unit.dto.ts @@ -49,3 +49,12 @@ export class GetUnitChildDto { }) public includeSubSpaces: boolean = false; } +export class GetUnitByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts index 8502a86..39fac9a 100644 --- a/src/unit/interface/unit.interface.ts +++ b/src/unit/interface/unit.interface.ts @@ -24,3 +24,8 @@ export interface RenameUnitByUuidInterface { name: string; type: string; } +export interface GetUnitByUserUuidInterface { + uuid: string; + name: string; + type: string; +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index e84bc0f..27bebbc 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -13,15 +13,18 @@ import { UnitParentInterface, GetUnitByUuidInterface, RenameUnitByUuidInterface, + GetUnitByUserUuidInterface, } from '../interface/unit.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Injectable() export class UnitService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async addUnit(addUnitDto: AddUnitDto) { @@ -195,7 +198,36 @@ export class UnitService { } } } + async getUnitsByUserId( + userUuid: string, + ): Promise { + try { + const units = await this.userSpaceRepository.find({ + relations: ['space', 'space.spaceType'], + where: { + user: { uuid: userUuid }, + space: { spaceType: { type: 'unit' } }, + }, + }); + if (units.length === 0) { + throw new HttpException('this user has no units', HttpStatus.NOT_FOUND); + } + const spaces = units.map((unit) => ({ + uuid: unit.space.uuid, + name: unit.space.spaceName, + type: unit.space.spaceType.type, + })); + + return spaces; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException('user not found', HttpStatus.NOT_FOUND); + } + } + } async renameUnitByUuid( unitUuid: string, updateUnitNameDto: UpdateUnitNameDto, diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts index eaf571f..2f0234e 100644 --- a/src/unit/unit.module.ts +++ b/src/unit/unit.module.ts @@ -6,11 +6,23 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space.type.repository.module'; import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; +import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; +import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, SpaceTypeRepositoryModule], + imports: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + ], controllers: [UnitController], - providers: [UnitService, SpaceRepository, SpaceTypeRepository], + providers: [ + UnitService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + ], exports: [UnitService], }) export class UnitModule {} From 74b747110693e500c2a0ca0eed51095111b5cc09 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:11:43 +0300 Subject: [PATCH 134/259] Refactor controllers to use userUuid as route param --- src/building/controllers/building.controller.ts | 14 ++++---------- src/building/dtos/get.building.dto.ts | 10 ---------- src/community/controllers/community.controller.ts | 14 ++++---------- src/community/dtos/get.community.dto.ts | 10 ---------- src/floor/controllers/floor.controller.ts | 9 ++++----- src/floor/dtos/get.floor.dto.ts | 10 ---------- src/room/controllers/room.controller.ts | 9 +++------ src/room/dtos/get.room.dto.ts | 12 ------------ src/unit/controllers/unit.controller.ts | 9 ++++----- 9 files changed, 19 insertions(+), 78 deletions(-) delete mode 100644 src/room/dtos/get.room.dto.ts diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 9e7a873..bac6d18 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -10,15 +10,11 @@ import { Put, Query, UseGuards, - ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddBuildingDto } from '../dtos/add.building.dto'; -import { - GetBuildingByUserIdDto, - GetBuildingChildDto, -} from '../dtos/get.building.dto'; +import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; @@ -99,12 +95,10 @@ export class BuildingController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get() - async getBuildingsByUserId( - @Query(ValidationPipe) dto: GetBuildingByUserIdDto, - ) { + @Get('user/:userUuid') + async getBuildingsByUserId(@Param('userUuid') userUuid: string) { try { - return await this.buildingService.getBuildingsByUserId(dto.userUuid); + return await this.buildingService.getBuildingsByUserId(userUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/building/dtos/get.building.dto.ts b/src/building/dtos/get.building.dto.ts index 720201a..f762469 100644 --- a/src/building/dtos/get.building.dto.ts +++ b/src/building/dtos/get.building.dto.ts @@ -49,13 +49,3 @@ export class GetBuildingChildDto { }) public includeSubSpaces: boolean = false; } - -export class GetBuildingByUserIdDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; -} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index ea9805c..ab46154 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -10,15 +10,11 @@ import { Put, Query, UseGuards, - ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddCommunityDto } from '../dtos/add.community.dto'; -import { - GetCommunitiesByUserIdDto, - GetCommunityChildDto, -} from '../dtos/get.community.dto'; +import { GetCommunityChildDto } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @ApiTags('Community Module') @@ -83,12 +79,10 @@ export class CommunityController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get() - async getCommunitiesByUserId( - @Query(ValidationPipe) dto: GetCommunitiesByUserIdDto, - ) { + @Get('user/:userUuid') + async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { try { - return await this.communityService.getCommunitiesByUserId(dto.userUuid); + return await this.communityService.getCommunitiesByUserId(userUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts index 5d0e08c..be614e5 100644 --- a/src/community/dtos/get.community.dto.ts +++ b/src/community/dtos/get.community.dto.ts @@ -49,13 +49,3 @@ export class GetCommunityChildDto { }) public includeSubSpaces: boolean = false; } - -export class GetCommunitiesByUserIdDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; -} diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index c1fa66f..fd22325 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -10,12 +10,11 @@ import { Put, Query, UseGuards, - ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddFloorDto } from '../dtos/add.floor.dto'; -import { GetFloorByUserIdDto, GetFloorChildDto } from '../dtos/get.floor.dto'; +import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; @@ -94,10 +93,10 @@ export class FloorController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get() - async getFloorsByUserId(@Query(ValidationPipe) dto: GetFloorByUserIdDto) { + @Get('user/:userUuid') + async getFloorsByUserId(@Param('userUuid') userUuid: string) { try { - return await this.floorService.getFloorsByUserId(dto.userUuid); + return await this.floorService.getFloorsByUserId(userUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/floor/dtos/get.floor.dto.ts b/src/floor/dtos/get.floor.dto.ts index 40d0aed..23a8e56 100644 --- a/src/floor/dtos/get.floor.dto.ts +++ b/src/floor/dtos/get.floor.dto.ts @@ -49,13 +49,3 @@ export class GetFloorChildDto { }) public includeSubSpaces: boolean = false; } - -export class GetFloorByUserIdDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; -} diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 80ef7cf..dee131f 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -8,16 +8,13 @@ import { Param, Post, Put, - Query, UseGuards, - ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; -import { GetRoomByUserIdDto } from '../dtos/get.room.dto'; @ApiTags('Room Module') @Controller({ @@ -74,10 +71,10 @@ export class RoomController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get() - async getRoomsByUserId(@Query(ValidationPipe) dto: GetRoomByUserIdDto) { + @Get('user/:userUuid') + async getRoomsByUserId(@Param('userUuid') userUuid: string) { try { - return await this.roomService.getRoomsByUserId(dto.userUuid); + return await this.roomService.getRoomsByUserId(userUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/room/dtos/get.room.dto.ts b/src/room/dtos/get.room.dto.ts deleted file mode 100644 index f919b85..0000000 --- a/src/room/dtos/get.room.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class GetRoomByUserIdDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; -} diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 245dd85..5289200 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -10,12 +10,11 @@ import { Put, Query, UseGuards, - ValidationPipe, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddUnitDto } from '../dtos/add.unit.dto'; -import { GetUnitByUserIdDto, GetUnitChildDto } from '../dtos/get.unit.dto'; +import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @@ -91,10 +90,10 @@ export class UnitController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get() - async getUnitsByUserId(@Query(ValidationPipe) dto: GetUnitByUserIdDto) { + @Get('user/:userUuid') + async getUnitsByUserId(@Param('userUuid') userUuid: string) { try { - return await this.unitService.getUnitsByUserId(dto.userUuid); + return await this.unitService.getUnitsByUserId(userUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', From 1d9bb96a87ad7a0f9aa6d9ea552f35205d2b43f6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:22:02 +0300 Subject: [PATCH 135/259] Add UserRepository and User Building functionality --- src/building/building.module.ts | 4 ++++ .../controllers/building.controller.ts | 18 +++++++++++++-- src/building/dtos/add.building.dto.ts | 19 ++++++++++++++++ src/building/services/building.service.ts | 22 +++++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/building/building.module.ts b/src/building/building.module.ts index 5507453..80391fe 100644 --- a/src/building/building.module.ts +++ b/src/building/building.module.ts @@ -8,6 +8,8 @@ import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space. import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; @Module({ imports: [ @@ -15,6 +17,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepositoryModule, SpaceTypeRepositoryModule, UserSpaceRepositoryModule, + UserRepositoryModule, ], controllers: [BuildingController], providers: [ @@ -22,6 +25,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepository, SpaceTypeRepository, UserSpaceRepository, + UserRepository, ], exports: [BuildingService], }) diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index bac6d18..8517ee1 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -13,10 +13,11 @@ import { } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddBuildingDto } from '../dtos/add.building.dto'; +import { AddBuildingDto, AddUserBuildingDto } from '../dtos/add.building.dto'; import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; +import { CheckUserBuildingGuard } from 'src/guards/user.building.guard'; @ApiTags('Building Module') @Controller({ @@ -92,7 +93,20 @@ export class BuildingController { ); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserBuildingGuard) + @Post('user') + async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) { + try { + await this.buildingService.addUserBuilding(addUserBuildingDto); + return { message: 'user building added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') diff --git a/src/building/dtos/add.building.dto.ts b/src/building/dtos/add.building.dto.ts index e9268c0..5d79231 100644 --- a/src/building/dtos/add.building.dto.ts +++ b/src/building/dtos/add.building.dto.ts @@ -21,3 +21,22 @@ export class AddBuildingDto { Object.assign(this, dto); } } +export class AddUserBuildingDto { + @ApiProperty({ + description: 'buildingUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public buildingUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index 726697b..fcb803b 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -7,7 +7,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddBuildingDto } from '../dtos'; +import { AddBuildingDto, AddUserBuildingDto } from '../dtos'; import { BuildingChildInterface, BuildingParentInterface, @@ -246,7 +246,25 @@ export class BuildingService { } } } - + async addUserBuilding(addUserBuildingDto: AddUserBuildingDto) { + try { + await this.userSpaceRepository.save({ + user: { uuid: addUserBuildingDto.userUuid }, + space: { uuid: addUserBuildingDto.buildingUuid }, + }); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this building', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async renameBuildingByUuid( buildingUuid: string, updateBuildingNameDto: UpdateBuildingNameDto, From ae63ac67177ba5701ca53bb022bf96be2d9433be Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:22:12 +0300 Subject: [PATCH 136/259] Add UserRepository and addUserCommunity endpoint --- src/community/community.module.ts | 4 ++++ .../controllers/community.controller.ts | 21 ++++++++++++++++-- src/community/dtos/add.community.dto.ts | 19 ++++++++++++++++ src/community/services/community.service.ts | 22 ++++++++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index de5a7c0..742e3ad 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -8,6 +8,8 @@ import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space. import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; +import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ imports: [ @@ -15,6 +17,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepositoryModule, SpaceTypeRepositoryModule, UserSpaceRepositoryModule, + UserRepositoryModule, ], controllers: [CommunityController], providers: [ @@ -22,6 +25,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepository, SpaceTypeRepository, UserSpaceRepository, + UserRepository, ], exports: [CommunityService], }) diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index ab46154..f20aa3d 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -13,9 +13,13 @@ import { } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddCommunityDto } from '../dtos/add.community.dto'; +import { + AddCommunityDto, + AddUserCommunityDto, +} 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'; @ApiTags('Community Module') @Controller({ @@ -90,7 +94,20 @@ export class CommunityController { ); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserCommunityGuard) + @Post('user') + async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { + try { + await this.communityService.addUserCommunity(addUserCommunityDto); + return { message: 'user community added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('rename/:communityUuid') diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index 96dce06..06aec7c 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -14,3 +14,22 @@ export class AddCommunityDto { Object.assign(this, dto); } } +export class AddUserCommunityDto { + @ApiProperty({ + description: 'communityUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public communityUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 1c3109a..e88290f 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -7,7 +7,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddCommunityDto } from '../dtos'; +import { AddCommunityDto, AddUserCommunityDto } from '../dtos'; import { CommunityChildInterface, GetCommunityByUserUuidInterface, @@ -188,6 +188,26 @@ export class CommunityService { } } } + + async addUserCommunity(addUserCommunityDto: AddUserCommunityDto) { + try { + await this.userSpaceRepository.save({ + user: { uuid: addUserCommunityDto.userUuid }, + space: { uuid: addUserCommunityDto.communityUuid }, + }); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this community', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async renameCommunityByUuid( communityUuid: string, updateCommunityDto: UpdateCommunityNameDto, From 5bf3215c6be90096f8f6d4fabf995a2acf617b6e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:22:26 +0300 Subject: [PATCH 137/259] Add CheckUserFloorGuard and implement addUserFloor functionality --- src/floor/controllers/floor.controller.ts | 18 +++++++++++++++++- src/floor/dtos/add.floor.dto.ts | 19 +++++++++++++++++++ src/floor/floor.module.ts | 4 ++++ src/floor/services/floor.service.ts | 22 ++++++++++++++++++++-- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index fd22325..2033918 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -13,10 +13,11 @@ import { } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddFloorDto } from '../dtos/add.floor.dto'; +import { AddFloorDto, AddUserFloorDto } from '../dtos/add.floor.dto'; import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; +import { CheckUserFloorGuard } from 'src/guards/user.floor.guard'; @ApiTags('Floor Module') @Controller({ @@ -91,6 +92,21 @@ export class FloorController { } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserFloorGuard) + @Post('user') + async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) { + try { + await this.floorService.addUserFloor(addUserFloorDto); + return { message: 'user floor added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') diff --git a/src/floor/dtos/add.floor.dto.ts b/src/floor/dtos/add.floor.dto.ts index 9f2de58..3d1655a 100644 --- a/src/floor/dtos/add.floor.dto.ts +++ b/src/floor/dtos/add.floor.dto.ts @@ -21,3 +21,22 @@ export class AddFloorDto { Object.assign(this, dto); } } +export class AddUserFloorDto { + @ApiProperty({ + description: 'floorUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public floorUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/floor/floor.module.ts b/src/floor/floor.module.ts index f247fea..71a6c67 100644 --- a/src/floor/floor.module.ts +++ b/src/floor/floor.module.ts @@ -8,6 +8,8 @@ import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space. import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; +import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ imports: [ @@ -15,6 +17,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepositoryModule, SpaceTypeRepositoryModule, UserSpaceRepositoryModule, + UserRepositoryModule, ], controllers: [FloorController], providers: [ @@ -22,6 +25,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepository, SpaceTypeRepository, UserSpaceRepository, + UserRepository, ], exports: [FloorService], }) diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index 0cf19da..91de93b 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -7,7 +7,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddFloorDto } from '../dtos'; +import { AddFloorDto, AddUserFloorDto } from '../dtos'; import { FloorChildInterface, FloorParentInterface, @@ -231,7 +231,25 @@ export class FloorService { } } } - + async addUserFloor(addUserFloorDto: AddUserFloorDto) { + try { + await this.userSpaceRepository.save({ + user: { uuid: addUserFloorDto.userUuid }, + space: { uuid: addUserFloorDto.floorUuid }, + }); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this floor', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async renameFloorByUuid( floorUuid: string, updateFloorDto: UpdateFloorNameDto, From 3afdd4922bbf205aab4ffdc51fa91c0bfab7262a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:22:38 +0300 Subject: [PATCH 138/259] Add AddUserRoomDto and endpoint to add user to room --- src/room/controllers/room.controller.ts | 18 ++++++++++++++++-- src/room/dtos/add.room.dto.ts | 19 +++++++++++++++++++ src/room/room.module.ts | 4 ++++ src/room/services/room.service.ts | 22 ++++++++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index dee131f..054bedf 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -12,9 +12,10 @@ import { } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddRoomDto } from '../dtos/add.room.dto'; +import { AddRoomDto, AddUserRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; +import { CheckUserRoomGuard } from 'src/guards/user.room.guard'; @ApiTags('Room Module') @Controller({ @@ -68,7 +69,20 @@ export class RoomController { ); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserRoomGuard) + @Post('user') + async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) { + try { + await this.roomService.addUserRoom(addUserRoomDto); + return { message: 'user room added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') diff --git a/src/room/dtos/add.room.dto.ts b/src/room/dtos/add.room.dto.ts index 69425b1..2718a29 100644 --- a/src/room/dtos/add.room.dto.ts +++ b/src/room/dtos/add.room.dto.ts @@ -21,3 +21,22 @@ export class AddRoomDto { Object.assign(this, dto); } } +export class AddUserRoomDto { + @ApiProperty({ + description: 'roomUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public roomUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/room/room.module.ts b/src/room/room.module.ts index d83d6fb..2d6d98c 100644 --- a/src/room/room.module.ts +++ b/src/room/room.module.ts @@ -8,6 +8,8 @@ import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space. import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; +import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ imports: [ @@ -15,6 +17,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepositoryModule, SpaceTypeRepositoryModule, UserSpaceRepositoryModule, + UserRepositoryModule, ], controllers: [RoomController], providers: [ @@ -22,6 +25,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepository, SpaceTypeRepository, UserSpaceRepository, + UserRepository, ], exports: [RoomService], }) diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 7b0b7f5..4771c25 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -6,7 +6,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddRoomDto } from '../dtos'; +import { AddRoomDto, AddUserRoomDto } from '../dtos'; import { RoomParentInterface, GetRoomByUuidInterface, @@ -137,7 +137,25 @@ export class RoomService { } } } - + async addUserRoom(addUserRoomDto: AddUserRoomDto) { + try { + await this.userSpaceRepository.save({ + user: { uuid: addUserRoomDto.userUuid }, + space: { uuid: addUserRoomDto.roomUuid }, + }); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this room', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async renameRoomByUuid( roomUuid: string, updateRoomNameDto: UpdateRoomNameDto, From d9782b3218e60f195ae68fb1d972ff18c1329f98 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:22:47 +0300 Subject: [PATCH 139/259] Add functionality to add user to a unit --- src/unit/controllers/unit.controller.ts | 18 ++++++++++++++++-- src/unit/dtos/add.unit.dto.ts | 19 +++++++++++++++++++ src/unit/services/unit.service.ts | 22 +++++++++++++++++++++- src/unit/unit.module.ts | 4 ++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 5289200..a3e59e9 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -13,10 +13,11 @@ import { } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddUnitDto } from '../dtos/add.unit.dto'; +import { AddUnitDto, AddUserUnitDto } from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; +import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; @ApiTags('Unit Module') @Controller({ @@ -87,7 +88,20 @@ export class UnitController { ); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserUnitGuard) + @Post('user') + async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { + try { + await this.unitService.addUserUnit(addUserUnitDto); + return { message: 'user unit added successfully' }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts index 40f164a..e42d1bb 100644 --- a/src/unit/dtos/add.unit.dto.ts +++ b/src/unit/dtos/add.unit.dto.ts @@ -21,3 +21,22 @@ export class AddUnitDto { Object.assign(this, dto); } } +export class AddUserUnitDto { + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 27bebbc..867300c 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -7,7 +7,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddUnitDto } from '../dtos'; +import { AddUnitDto, AddUserUnitDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, @@ -228,6 +228,26 @@ export class UnitService { } } } + + async addUserUnit(addUserUnitDto: AddUserUnitDto) { + try { + await this.userSpaceRepository.save({ + user: { uuid: addUserUnitDto.userUuid }, + space: { uuid: addUserUnitDto.unitUuid }, + }); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this unit', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async renameUnitByUuid( unitUuid: string, updateUnitNameDto: UpdateUnitNameDto, diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts index 2f0234e..569ef39 100644 --- a/src/unit/unit.module.ts +++ b/src/unit/unit.module.ts @@ -8,6 +8,8 @@ import { SpaceTypeRepositoryModule } from '@app/common/modules/space-type/space. import { SpaceTypeRepository } from '@app/common/modules/space-type/repositories'; import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.space.repository.module'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; +import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ imports: [ @@ -15,6 +17,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepositoryModule, SpaceTypeRepositoryModule, UserSpaceRepositoryModule, + UserRepositoryModule, ], controllers: [UnitController], providers: [ @@ -22,6 +25,7 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories SpaceRepository, SpaceTypeRepository, UserSpaceRepository, + UserRepository, ], exports: [UnitService], }) From c2d6c7ffd551d3591dfc98a910c05f6c2edb5d1e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:23:01 +0300 Subject: [PATCH 140/259] Add user space type guards --- src/guards/user.building.guard.ts | 70 ++++++++++++++++++++++++++++++ src/guards/user.community.guard.ts | 70 ++++++++++++++++++++++++++++++ src/guards/user.floor.guard.ts | 70 ++++++++++++++++++++++++++++++ src/guards/user.room.guard.ts | 70 ++++++++++++++++++++++++++++++ src/guards/user.unit.guard.ts | 70 ++++++++++++++++++++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 src/guards/user.building.guard.ts create mode 100644 src/guards/user.community.guard.ts create mode 100644 src/guards/user.floor.guard.ts create mode 100644 src/guards/user.room.guard.ts create mode 100644 src/guards/user.unit.guard.ts diff --git a/src/guards/user.building.guard.ts b/src/guards/user.building.guard.ts new file mode 100644 index 0000000..b47124d --- /dev/null +++ b/src/guards/user.building.guard.ts @@ -0,0 +1,70 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckUserBuildingGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { userUuid, buildingUuid } = req.body; + + await this.checkUserIsFound(userUuid); + + await this.checkBuildingIsFound(buildingUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!userData) { + throw new NotFoundException('User not found'); + } + } + + private async checkBuildingIsFound(spaceUuid: string) { + const spaceData = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, spaceType: { type: 'building' } }, + relations: ['spaceType'], + }); + if (!spaceData) { + throw new NotFoundException('Building not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if ( + error instanceof BadRequestException || + error instanceof NotFoundException + ) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'invalid userUuid or buildingUuid', + }); + } + } +} diff --git a/src/guards/user.community.guard.ts b/src/guards/user.community.guard.ts new file mode 100644 index 0000000..04e08b4 --- /dev/null +++ b/src/guards/user.community.guard.ts @@ -0,0 +1,70 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckUserCommunityGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { userUuid, communityUuid } = req.body; + + await this.checkUserIsFound(userUuid); + + await this.checkCommunityIsFound(communityUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!userData) { + throw new NotFoundException('User not found'); + } + } + + private async checkCommunityIsFound(spaceUuid: string) { + const spaceData = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, spaceType: { type: 'community' } }, + relations: ['spaceType'], + }); + if (!spaceData) { + throw new NotFoundException('Community not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if ( + error instanceof BadRequestException || + error instanceof NotFoundException + ) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'invalid userUuid or communityUuid', + }); + } + } +} diff --git a/src/guards/user.floor.guard.ts b/src/guards/user.floor.guard.ts new file mode 100644 index 0000000..9235fb8 --- /dev/null +++ b/src/guards/user.floor.guard.ts @@ -0,0 +1,70 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckUserFloorGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { userUuid, floorUuid } = req.body; + + await this.checkUserIsFound(userUuid); + + await this.checkFloorIsFound(floorUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!userData) { + throw new NotFoundException('User not found'); + } + } + + private async checkFloorIsFound(spaceUuid: string) { + const spaceData = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, spaceType: { type: 'floor' } }, + relations: ['spaceType'], + }); + if (!spaceData) { + throw new NotFoundException('Floor not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if ( + error instanceof BadRequestException || + error instanceof NotFoundException + ) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'invalid userUuid or floorUuid', + }); + } + } +} diff --git a/src/guards/user.room.guard.ts b/src/guards/user.room.guard.ts new file mode 100644 index 0000000..0ecf817 --- /dev/null +++ b/src/guards/user.room.guard.ts @@ -0,0 +1,70 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckUserRoomGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { userUuid, roomUuid } = req.body; + + await this.checkUserIsFound(userUuid); + + await this.checkRoomIsFound(roomUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!userData) { + throw new NotFoundException('User not found'); + } + } + + private async checkRoomIsFound(spaceUuid: string) { + const spaceData = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, spaceType: { type: 'room' } }, + relations: ['spaceType'], + }); + if (!spaceData) { + throw new NotFoundException('Room not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if ( + error instanceof BadRequestException || + error instanceof NotFoundException + ) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'invalid userUuid or roomUuid', + }); + } + } +} diff --git a/src/guards/user.unit.guard.ts b/src/guards/user.unit.guard.ts new file mode 100644 index 0000000..4d7b1ab --- /dev/null +++ b/src/guards/user.unit.guard.ts @@ -0,0 +1,70 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckUserUnitGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { userUuid, unitUuid } = req.body; + + await this.checkUserIsFound(userUuid); + + await this.checkUnitIsFound(unitUuid); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!userData) { + throw new NotFoundException('User not found'); + } + } + + private async checkUnitIsFound(spaceUuid: string) { + const spaceData = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, spaceType: { type: 'unit' } }, + relations: ['spaceType'], + }); + if (!spaceData) { + throw new NotFoundException('Unit not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if ( + error instanceof BadRequestException || + error instanceof NotFoundException + ) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'invalid userUuid or unitUuid', + }); + } + } +} From 0bfb47bc331edb3071443443d8317c7cbcd7d57c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:23:11 +0300 Subject: [PATCH 141/259] Add unique constraint for user and space in UserSpaceEntity --- .../src/modules/user-space/entities/user.space.entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/user-space/entities/user.space.entity.ts b/libs/common/src/modules/user-space/entities/user.space.entity.ts index c41dfc0..a6caaa5 100644 --- a/libs/common/src/modules/user-space/entities/user.space.entity.ts +++ b/libs/common/src/modules/user-space/entities/user.space.entity.ts @@ -1,10 +1,11 @@ -import { Column, Entity, ManyToOne } from 'typeorm'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { UserSpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceEntity } from '../../space/entities'; import { UserEntity } from '../../user/entities'; @Entity({ name: 'user-space' }) +@Unique(['user', 'space']) export class UserSpaceEntity extends AbstractEntity { @Column({ type: 'uuid', From fbdf187ee17719aee1f01459dca4bfc691f10e7e Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Wed, 17 Apr 2024 18:52:58 +0530 Subject: [PATCH 142/259] SPRINT-1 related tasks done --- libs/common/src/auth/auth.module.ts | 18 ++--- libs/common/src/auth/services/auth.service.ts | 41 ++++++++++- .../src/auth/strategies/jwt.strategy.ts | 2 +- .../auth/strategies/refresh-token.strategy.ts | 42 ++++++++++++ .../src/constants/permission-type.enum.ts | 4 ++ libs/common/src/database/database.module.ts | 5 ++ .../src/guards/jwt-refresh.auth.guard.ts | 11 +++ .../device/device.repository.module.ts | 13 ++++ .../device/dtos/device-user-type.dto.ts | 19 ++++++ .../src/modules/device/dtos/device.dto.ts | 20 ++++++ libs/common/src/modules/device/dtos/index.ts | 2 + .../entities/device-user-type.entity.ts | 26 +++++++ .../modules/device/entities/device.entity.ts | 32 +++++++++ .../src/modules/device/entities/index.ts | 2 + libs/common/src/modules/device/index.ts | 1 + .../device-user-type.repository.ts | 10 +++ .../device/repositories/device.repository.ts | 10 +++ .../src/modules/device/repositories/index.ts | 2 + .../src/modules/permission/dtos/index.ts | 1 + .../modules/permission/dtos/permission.dto.ts | 11 +++ .../src/modules/permission/entities/index.ts | 1 + .../permission/entities/permission.entity.ts | 18 +++++ .../permission.repository.module.ts | 11 +++ .../modules/permission/repositories/index.ts | 1 + .../repositories/permission.repository.ts | 10 +++ .../src/modules/user/entities/user.entity.ts | 11 +++ package-lock.json | 41 +++++++++++ package.json | 1 + src/app.module.ts | 2 + src/auth/controllers/user-auth.controller.ts | 33 +++++++++ src/auth/services/user-auth.service.ts | 46 +++++++++++-- src/config/index.ts | 4 +- .../controllers/index.ts | 0 .../user-device-permission.controller.ts | 68 +++++++++++++++++++ src/user-device-permission/dtos/index.ts | 0 .../dtos/user-device-permission.add.dto.ts | 28 ++++++++ .../dtos/user-device-permission.edit.dto.ts | 7 ++ src/user-device-permission/services/index.ts | 0 .../user-device-permission.service.ts | 32 +++++++++ .../user-device-permission.module.ts | 21 ++++++ 40 files changed, 588 insertions(+), 19 deletions(-) create mode 100644 libs/common/src/auth/strategies/refresh-token.strategy.ts create mode 100644 libs/common/src/constants/permission-type.enum.ts create mode 100644 libs/common/src/guards/jwt-refresh.auth.guard.ts create mode 100644 libs/common/src/modules/device/device.repository.module.ts create mode 100644 libs/common/src/modules/device/dtos/device-user-type.dto.ts create mode 100644 libs/common/src/modules/device/dtos/device.dto.ts create mode 100644 libs/common/src/modules/device/dtos/index.ts create mode 100644 libs/common/src/modules/device/entities/device-user-type.entity.ts create mode 100644 libs/common/src/modules/device/entities/device.entity.ts create mode 100644 libs/common/src/modules/device/entities/index.ts create mode 100644 libs/common/src/modules/device/index.ts create mode 100644 libs/common/src/modules/device/repositories/device-user-type.repository.ts create mode 100644 libs/common/src/modules/device/repositories/device.repository.ts create mode 100644 libs/common/src/modules/device/repositories/index.ts create mode 100644 libs/common/src/modules/permission/dtos/index.ts create mode 100644 libs/common/src/modules/permission/dtos/permission.dto.ts create mode 100644 libs/common/src/modules/permission/entities/index.ts create mode 100644 libs/common/src/modules/permission/entities/permission.entity.ts create mode 100644 libs/common/src/modules/permission/permission.repository.module.ts create mode 100644 libs/common/src/modules/permission/repositories/index.ts create mode 100644 libs/common/src/modules/permission/repositories/permission.repository.ts create mode 100644 src/user-device-permission/controllers/index.ts create mode 100644 src/user-device-permission/controllers/user-device-permission.controller.ts create mode 100644 src/user-device-permission/dtos/index.ts create mode 100644 src/user-device-permission/dtos/user-device-permission.add.dto.ts create mode 100644 src/user-device-permission/dtos/user-device-permission.edit.dto.ts create mode 100644 src/user-device-permission/services/index.ts create mode 100644 src/user-device-permission/services/user-device-permission.service.ts create mode 100644 src/user-device-permission/user-device-permission.module.ts diff --git a/libs/common/src/auth/auth.module.ts b/libs/common/src/auth/auth.module.ts index 236fea3..c74aa81 100644 --- a/libs/common/src/auth/auth.module.ts +++ b/libs/common/src/auth/auth.module.ts @@ -7,22 +7,22 @@ import { JwtStrategy } from './strategies/jwt.strategy'; import { UserSessionRepository } from '../modules/session/repositories/session.repository'; import { AuthService } from './services/auth.service'; import { UserRepository } from '../modules/user/repositories'; +import { RefreshTokenStrategy } from './strategies/refresh-token.strategy'; @Module({ imports: [ ConfigModule.forRoot(), PassportModule, - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { expiresIn: configService.get('JWT_EXPIRE_TIME') }, - }), - }), + JwtModule.register({}), HelperModule, ], - providers: [JwtStrategy, UserSessionRepository, AuthService, UserRepository], + providers: [ + JwtStrategy, + RefreshTokenStrategy, + UserSessionRepository, + AuthService, + UserRepository, + ], exports: [AuthService], }) export class AuthModule {} diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index d305d94..5e57550 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,9 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import * as argon2 from 'argon2'; import { HelperHashService } from '../../helper/services'; import { UserRepository } from '../../../../common/src/modules/user/repositories'; import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { @@ -12,6 +14,7 @@ export class AuthService { private readonly userRepository: UserRepository, private readonly sessionRepository: UserSessionRepository, private readonly helperHashService: HelperHashService, + private readonly configService: ConfigService, ) {} async validateUser(email: string, pass: string): Promise { @@ -40,6 +43,24 @@ export class AuthService { return await this.sessionRepository.save(data); } + async getTokens(payload) { + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_SECRET'), + expiresIn: '24h', + }), + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_SECRET'), + expiresIn: '7d', + }), + ]); + + return { + accessToken, + refreshToken, + }; + } + async login(user: any) { const payload = { email: user.email, @@ -48,8 +69,22 @@ export class AuthService { type: user.type, sessionId: user.sessionId, }; - return { - access_token: this.jwtService.sign(payload), - }; + const tokens = await this.getTokens(payload); + await this.updateRefreshToken(user.uuid, tokens.refreshToken); + return tokens; + } + + async updateRefreshToken(userId: string, refreshToken: string) { + const hashedRefreshToken = await this.hashData(refreshToken); + await this.userRepository.update( + { uuid: userId }, + { + refreshToken: hashedRefreshToken, + }, + ); + } + + hashData(data: string) { + return argon2.hash(data); } } diff --git a/libs/common/src/auth/strategies/jwt.strategy.ts b/libs/common/src/auth/strategies/jwt.strategy.ts index ac43543..67faecf 100644 --- a/libs/common/src/auth/strategies/jwt.strategy.ts +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -6,7 +6,7 @@ import { UserSessionRepository } from '../../../src/modules/session/repositories import { AuthInterface } from '../interfaces/auth.interface'; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { constructor( private readonly sessionRepository: UserSessionRepository, private readonly configService: ConfigService, diff --git a/libs/common/src/auth/strategies/refresh-token.strategy.ts b/libs/common/src/auth/strategies/refresh-token.strategy.ts new file mode 100644 index 0000000..9b21010 --- /dev/null +++ b/libs/common/src/auth/strategies/refresh-token.strategy.ts @@ -0,0 +1,42 @@ +import { ConfigService } from '@nestjs/config'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { UserSessionRepository } from '../../../src/modules/session/repositories/session.repository'; +import { AuthInterface } from '../interfaces/auth.interface'; + +@Injectable() +export class RefreshTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh', +) { + constructor( + private readonly sessionRepository: UserSessionRepository, + private readonly configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + }); + } + + async validate(payload: AuthInterface) { + const validateUser = await this.sessionRepository.findOne({ + where: { + uuid: payload.sessionId, + isLoggedOut: false, + }, + }); + if (validateUser) { + return { + email: payload.email, + userId: payload.id, + uuid: payload.uuid, + sessionId: payload.sessionId, + }; + } else { + throw new BadRequestException('Unauthorized'); + } + } +} diff --git a/libs/common/src/constants/permission-type.enum.ts b/libs/common/src/constants/permission-type.enum.ts new file mode 100644 index 0000000..67fea31 --- /dev/null +++ b/libs/common/src/constants/permission-type.enum.ts @@ -0,0 +1,4 @@ +export enum PermissionType { + READ = 'READ', + CONTROLLABLE = 'CONTROLLABLE', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index dd29899..8af20fb 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -7,6 +7,8 @@ import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; import { HomeEntity } from '../modules/home/entities'; import { ProductEntity } from '../modules/product/entities'; +import { DeviceEntity, DeviceUserPermissionEntity } from '../modules/device/entities'; +import { PermissionTypeEntity } from '../modules/permission/entities'; @Module({ imports: [ @@ -27,6 +29,9 @@ import { ProductEntity } from '../modules/product/entities'; UserOtpEntity, HomeEntity, ProductEntity, + DeviceUserPermissionEntity, + DeviceEntity, + PermissionTypeEntity ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/guards/jwt-refresh.auth.guard.ts b/libs/common/src/guards/jwt-refresh.auth.guard.ts new file mode 100644 index 0000000..62c23d8 --- /dev/null +++ b/libs/common/src/guards/jwt-refresh.auth.guard.ts @@ -0,0 +1,11 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +export class RefreshTokenGuard extends AuthGuard('jwt-refresh') { + handleRequest(err, user) { + if (err || !user) { + throw err || new UnauthorizedException(); + } + return user; + } +} diff --git a/libs/common/src/modules/device/device.repository.module.ts b/libs/common/src/modules/device/device.repository.module.ts new file mode 100644 index 0000000..b3d35d7 --- /dev/null +++ b/libs/common/src/modules/device/device.repository.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeviceEntity, DeviceUserPermissionEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [ + TypeOrmModule.forFeature([DeviceEntity, DeviceUserPermissionEntity]), + ], +}) +export class DeviceRepositoryModule {} diff --git a/libs/common/src/modules/device/dtos/device-user-type.dto.ts b/libs/common/src/modules/device/dtos/device-user-type.dto.ts new file mode 100644 index 0000000..0571356 --- /dev/null +++ b/libs/common/src/modules/device/dtos/device-user-type.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceUserTypeDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; + + @IsString() + @IsNotEmpty() + public permissionTypeUuid: string; +} diff --git a/libs/common/src/modules/device/dtos/device.dto.ts b/libs/common/src/modules/device/dtos/device.dto.ts new file mode 100644 index 0000000..fbc07c3 --- /dev/null +++ b/libs/common/src/modules/device/dtos/device.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsString } from "class-validator"; + +export class DeviceDto{ + + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + spaceUuid:string; + + @IsString() + @IsNotEmpty() + deviceTuyaUuid:string; + + @IsString() + @IsNotEmpty() + productUuid:string; +} \ No newline at end of file diff --git a/libs/common/src/modules/device/dtos/index.ts b/libs/common/src/modules/device/dtos/index.ts new file mode 100644 index 0000000..9d647c8 --- /dev/null +++ b/libs/common/src/modules/device/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './device.dto'; +export * from './device-user-type.dto'; diff --git a/libs/common/src/modules/device/entities/device-user-type.entity.ts b/libs/common/src/modules/device/entities/device-user-type.entity.ts new file mode 100644 index 0000000..66d05a7 --- /dev/null +++ b/libs/common/src/modules/device/entities/device-user-type.entity.ts @@ -0,0 +1,26 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceUserTypeDto } from '../dtos/device-user-type.dto'; + +@Entity({ name: 'device-user-permission' }) +export class DeviceUserPermissionEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public userUuid: string; + + @Column({ + nullable: false, + }) + deviceUuid: string; + + @Column({ + nullable: false, + }) + public permissionTypeUuid: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts new file mode 100644 index 0000000..e8df1f0 --- /dev/null +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceDto } from '../dtos/device.dto'; + +@Entity({ name: 'device' }) +export class DeviceEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public spaceUuid: string; + + @Column({ + nullable: false, + }) + deviceTuyaUuid: string; + + @Column({ + nullable: false, + }) + public productUuid: string; + + @Column({ + nullable: true, + default: true, + }) + isActive: true; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/device/entities/index.ts b/libs/common/src/modules/device/entities/index.ts new file mode 100644 index 0000000..d6cf7b2 --- /dev/null +++ b/libs/common/src/modules/device/entities/index.ts @@ -0,0 +1,2 @@ +export * from './device.entity'; +export * from './device-user-type.entity'; diff --git a/libs/common/src/modules/device/index.ts b/libs/common/src/modules/device/index.ts new file mode 100644 index 0000000..0529a42 --- /dev/null +++ b/libs/common/src/modules/device/index.ts @@ -0,0 +1 @@ +export * from './device.repository.module'; diff --git a/libs/common/src/modules/device/repositories/device-user-type.repository.ts b/libs/common/src/modules/device/repositories/device-user-type.repository.ts new file mode 100644 index 0000000..e3d2176 --- /dev/null +++ b/libs/common/src/modules/device/repositories/device-user-type.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceUserPermissionEntity } from '../entities'; + +@Injectable() +export class DeviceUserTypeRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceUserPermissionEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device/repositories/device.repository.ts b/libs/common/src/modules/device/repositories/device.repository.ts new file mode 100644 index 0000000..267c06c --- /dev/null +++ b/libs/common/src/modules/device/repositories/device.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceEntity } from '../entities'; + +@Injectable() +export class DeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device/repositories/index.ts b/libs/common/src/modules/device/repositories/index.ts new file mode 100644 index 0000000..f91e07f --- /dev/null +++ b/libs/common/src/modules/device/repositories/index.ts @@ -0,0 +1,2 @@ +export * from './device.repository'; +export * from './device-user-type.repository'; diff --git a/libs/common/src/modules/permission/dtos/index.ts b/libs/common/src/modules/permission/dtos/index.ts new file mode 100644 index 0000000..cd80da6 --- /dev/null +++ b/libs/common/src/modules/permission/dtos/index.ts @@ -0,0 +1 @@ +export * from './permission.dto' \ No newline at end of file diff --git a/libs/common/src/modules/permission/dtos/permission.dto.ts b/libs/common/src/modules/permission/dtos/permission.dto.ts new file mode 100644 index 0000000..86912ee --- /dev/null +++ b/libs/common/src/modules/permission/dtos/permission.dto.ts @@ -0,0 +1,11 @@ +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class PermissionTypeDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsEnum(PermissionType) + public type: PermissionType; +} diff --git a/libs/common/src/modules/permission/entities/index.ts b/libs/common/src/modules/permission/entities/index.ts new file mode 100644 index 0000000..2d9cedb --- /dev/null +++ b/libs/common/src/modules/permission/entities/index.ts @@ -0,0 +1 @@ +export * from './permission.entity' \ No newline at end of file diff --git a/libs/common/src/modules/permission/entities/permission.entity.ts b/libs/common/src/modules/permission/entities/permission.entity.ts new file mode 100644 index 0000000..d4d17de --- /dev/null +++ b/libs/common/src/modules/permission/entities/permission.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { PermissionTypeDto } from '../dtos/permission.dto'; + +@Entity({ name: 'permission-type' }) +export class PermissionTypeEntity extends AbstractEntity { + @Column({ + nullable: false, + enum: Object.values(PermissionType), + }) + type: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/permission/permission.repository.module.ts b/libs/common/src/modules/permission/permission.repository.module.ts new file mode 100644 index 0000000..6819e9b --- /dev/null +++ b/libs/common/src/modules/permission/permission.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PermissionTypeEntity } from './entities/permission.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([PermissionTypeEntity])], +}) +export class PermissionTypeRepositoryModule {} diff --git a/libs/common/src/modules/permission/repositories/index.ts b/libs/common/src/modules/permission/repositories/index.ts new file mode 100644 index 0000000..cdd0f3a --- /dev/null +++ b/libs/common/src/modules/permission/repositories/index.ts @@ -0,0 +1 @@ +export * from './permission.repository' \ No newline at end of file diff --git a/libs/common/src/modules/permission/repositories/permission.repository.ts b/libs/common/src/modules/permission/repositories/permission.repository.ts new file mode 100644 index 0000000..c9a3f94 --- /dev/null +++ b/libs/common/src/modules/permission/repositories/permission.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { PermissionTypeEntity } from '../entities/permission.entity'; + +@Injectable() +export class PermissionTypeRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PermissionTypeEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 63f2185..a55608f 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -30,12 +30,23 @@ export class UserEntity extends AbstractEntity { }) public lastName: string; + @Column({ + nullable: true, + }) + public refreshToken: string; + @Column({ nullable: true, default: false, }) public isUserVerified: boolean; + @Column({ + nullable: false, + default: true, + }) + public isActive: boolean; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/package-lock.json b/package-lock.json index a41b90f..f333ed5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", "@tuya/tuya-connector-nodejs": "^2.1.2", + "argon2": "^0.40.1", "axios": "^1.6.7", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -2111,6 +2112,14 @@ "npm": ">=5.0.0" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3032,6 +3041,20 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "devOptional": true }, + "node_modules/argon2": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.1.tgz", + "integrity": "sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==", + "hasInstallScript": true, + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7168,6 +7191,14 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "dev": true }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -7196,6 +7227,16 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 759b7e8..a8502be 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", "@tuya/tuya-connector-nodejs": "^2.1.2", + "argon2": "^0.40.1", "axios": "^1.6.7", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", diff --git a/src/app.module.ts b/src/app.module.ts index b3c0106..ef0f28e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { HomeModule } from './home/home.module'; import { RoomModule } from './room/room.module'; import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; +import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -19,6 +20,7 @@ import { DeviceModule } from './device/device.module'; RoomModule, GroupModule, DeviceModule, + UserDevicePermissionModule ], controllers: [AuthenticationController], }) diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index c434ef8..f2a4b6f 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -2,9 +2,11 @@ import { Body, Controller, Delete, + Get, HttpStatus, Param, Post, + Req, UseGuards, } from '@nestjs/common'; import { UserAuthService } from '../services/user-auth.service'; @@ -14,6 +16,8 @@ import { ResponseMessage } from '../../../libs/common/src/response/response.deco import { UserLoginDto } from '../dtos/user-login.dto'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; +import { Request } from 'express'; +import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; @Controller({ version: '1', @@ -93,4 +97,33 @@ export class UserAuthController { message: 'Password changed successfully', }; } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('user/list') + async userList(@Req() req) { + const userList = await this.userAuthService.userList(); + return { + statusCode: HttpStatus.OK, + data: userList, + message: 'User List Fetched Successfully', + }; + } + + @ApiBearerAuth() + @UseGuards(RefreshTokenGuard) + @Get('refresh-token') + async refreshToken(@Req() req) { + const refreshToken = await this.userAuthService.refreshToken( + req.user.uuid, + req.headers.authorization, + req.user.type, + req.user.sessionId, + ); + return { + statusCode: HttpStatus.OK, + data: refreshToken, + message: 'Refresh Token added Successfully', + }; + } } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 6c1e172..7b278b2 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -1,6 +1,7 @@ import { UserRepository } from '../../../libs/common/src/modules/user/repositories'; import { BadRequestException, + ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; @@ -14,7 +15,7 @@ import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; import { EmailService } from '../../../libs/common/src/util/email.service'; import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; -import { ILoginResponse } from '../constants/login.response.constant'; +import * as argon2 from 'argon2'; @Injectable() export class UserAuthService { @@ -64,7 +65,7 @@ export class UserAuthService { ); } - async userLogin(data: UserLoginDto): Promise { + async userLogin(data: UserLoginDto) { const user = await this.authService.validateUser(data.email, data.password); if (!user) { throw new UnauthorizedException('Invalid login credentials.'); @@ -86,7 +87,7 @@ export class UserAuthService { return await this.authService.login({ email: user.email, - userId: user.id, + userId: user.uuid, uuid: user.uuid, sessionId: session[1].uuid, }); @@ -97,7 +98,7 @@ export class UserAuthService { if (!user) { throw new BadRequestException('User does not found'); } - return await this.userRepository.delete({ uuid }); + return await this.userRepository.update({ uuid }, { isActive: false }); } async findOneById(id: string): Promise { @@ -148,4 +149,41 @@ export class UserAuthService { return true; } + + async userList(): Promise { + return await this.userRepository.find({ + where: { isActive: true }, + select: { + firstName: true, + lastName: true, + email: true, + isActive: true, + }, + }); + } + + async refreshToken( + userId: string, + refreshToken: string, + type: string, + sessionId: string, + ) { + const user = await this.userRepository.findOne({ where: { uuid: userId } }); + if (!user || !user.refreshToken) + throw new ForbiddenException('Access Denied'); + const refreshTokenMatches = await argon2.verify( + user.refreshToken, + refreshToken, + ); + if (!refreshTokenMatches) throw new ForbiddenException('Access Denied'); + const tokens = await this.authService.getTokens({ + email: user.email, + userId: user.uuid, + uuid: user.uuid, + type, + sessionId, + }); + await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken); + return tokens; + } } diff --git a/src/config/index.ts b/src/config/index.ts index 0dfc023..d7d0014 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,4 @@ import AuthConfig from './auth.config'; import AppConfig from './app.config'; - -export default [AuthConfig, AppConfig]; +import JwtConfig from './jwt.config'; +export default [AuthConfig, AppConfig, JwtConfig]; diff --git a/src/user-device-permission/controllers/index.ts b/src/user-device-permission/controllers/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts new file mode 100644 index 0000000..bfd98cb --- /dev/null +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -0,0 +1,68 @@ +import { + Body, + Controller, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { UserDevicePermissionService } from '../services/user-device-permission.service'; +import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; + +@ApiTags('Device Permission Module') +@Controller({ + version: '1', + path: 'device-permission', +}) +export class UserDevicePermissionController { + constructor( + private readonly userDevicePermissionService: UserDevicePermissionService, + ) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('add') + async addDevicePermission( + @Body() userDevicePermissionDto: UserDevicePermissionAddDto, + ) { + try { + const addDetails = + await this.userDevicePermissionService.addUserPermission( + userDevicePermissionDto, + ); + return { + statusCode: HttpStatus.CREATED, + message: 'User Permission for Devices Added Successfully', + data: addDetails, + }; + } catch (err) { + throw new Error(err); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('edit/:userId') + async editDevicePermission( + @Param('userId') userId: string, + @Body() userDevicePermissionEditDto: UserDevicePermissionEditDto, + ) { + try { + await this.userDevicePermissionService.editUserPermission( + userId, + userDevicePermissionEditDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User Permission for Devices Updated Successfully', + data: {}, + }; + } catch (err) { + throw new Error(err); + } + } +} diff --git a/src/user-device-permission/dtos/index.ts b/src/user-device-permission/dtos/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/user-device-permission/dtos/user-device-permission.add.dto.ts b/src/user-device-permission/dtos/user-device-permission.add.dto.ts new file mode 100644 index 0000000..c7459df --- /dev/null +++ b/src/user-device-permission/dtos/user-device-permission.add.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserDevicePermissionAddDto { + @ApiProperty({ + description: 'user id', + required: true, + }) + @IsString() + @IsNotEmpty() + userId: string; + + @ApiProperty({ + description: 'permission type id', + required: true, + }) + @IsString() + @IsNotEmpty() + permissionTypeId: string; + + @ApiProperty({ + description: 'device id', + required: true, + }) + @IsString() + @IsNotEmpty() + deviceId: string; +} diff --git a/src/user-device-permission/dtos/user-device-permission.edit.dto.ts b/src/user-device-permission/dtos/user-device-permission.edit.dto.ts new file mode 100644 index 0000000..7cc4fce --- /dev/null +++ b/src/user-device-permission/dtos/user-device-permission.edit.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UserDevicePermissionAddDto } from './user-device-permission.add.dto'; + +export class UserDevicePermissionEditDto extends OmitType( + UserDevicePermissionAddDto, + ['userId'], +) {} diff --git a/src/user-device-permission/services/index.ts b/src/user-device-permission/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/user-device-permission/services/user-device-permission.service.ts b/src/user-device-permission/services/user-device-permission.service.ts new file mode 100644 index 0000000..7dbb971 --- /dev/null +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -0,0 +1,32 @@ +import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { Injectable } from '@nestjs/common'; +import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; +import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; + +@Injectable() +export class UserDevicePermissionService { + constructor( + private readonly deviceUserTypeRepository: DeviceUserTypeRepository, + ) {} + + async addUserPermission(userDevicePermissionDto: UserDevicePermissionAddDto) { + return await this.deviceUserTypeRepository.save({ + userUuid: userDevicePermissionDto.userId, + deviceUuid: userDevicePermissionDto.deviceId, + permissionTypeUuid: userDevicePermissionDto.permissionTypeId, + }); + } + + async editUserPermission( + userId: string, + userDevicePermissionEditDto: UserDevicePermissionEditDto, + ) { + return await this.deviceUserTypeRepository.update( + { userUuid: userId }, + { + deviceUuid: userDevicePermissionEditDto.deviceId, + permissionTypeUuid: userDevicePermissionEditDto.permissionTypeId, + }, + ); + } +} diff --git a/src/user-device-permission/user-device-permission.module.ts b/src/user-device-permission/user-device-permission.module.ts new file mode 100644 index 0000000..edd9991 --- /dev/null +++ b/src/user-device-permission/user-device-permission.module.ts @@ -0,0 +1,21 @@ +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { + DeviceRepository, + DeviceUserTypeRepository, +} from '@app/common/modules/device/repositories'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { UserDevicePermissionService } from './services/user-device-permission.service'; +import { UserDevicePermissionController } from './controllers/user-device-permission.controller'; + +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [UserDevicePermissionController], + providers: [ + DeviceUserTypeRepository, + DeviceRepository, + UserDevicePermissionService, + ], + exports: [UserDevicePermissionService], +}) +export class UserDevicePermissionModule {} From 871544f00773e0b063fe0e49cabeb2caaef5310f Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Thu, 18 Apr 2024 12:03:33 +0530 Subject: [PATCH 143/259] relations added --- .../device/entities/device-user-type.entity.ts | 18 +++++++++++++++++- .../modules/device/entities/device.entity.ts | 14 +++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/device/entities/device-user-type.entity.ts b/libs/common/src/modules/device/entities/device-user-type.entity.ts index 66d05a7..904b18a 100644 --- a/libs/common/src/modules/device/entities/device-user-type.entity.ts +++ b/libs/common/src/modules/device/entities/device-user-type.entity.ts @@ -1,6 +1,8 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceUserTypeDto } from '../dtos/device-user-type.dto'; +import { DeviceEntity } from './device.entity'; +import { PermissionTypeEntity } from '../../permission/entities'; @Entity({ name: 'device-user-permission' }) export class DeviceUserPermissionEntity extends AbstractEntity { @@ -19,6 +21,20 @@ export class DeviceUserPermissionEntity extends AbstractEntity DeviceEntity, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'device_uuid', referencedColumnName: 'uuid' }) + device: DeviceEntity; + + @ManyToOne(() => PermissionTypeEntity, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'permission_type_uuid', referencedColumnName: 'uuid' }) + type: PermissionTypeEntity; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index e8df1f0..d90d2d1 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; +import { DeviceUserPermissionEntity } from './device-user-type.entity'; @Entity({ name: 'device' }) export class DeviceEntity extends AbstractEntity { @@ -25,6 +26,17 @@ export class DeviceEntity extends AbstractEntity { }) isActive: true; + @OneToMany( + () => DeviceUserPermissionEntity, + (permission) => permission.device, + { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + ) + permission: DeviceUserPermissionEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); From 921b44d4ed3ea3a70959608cb79f209d6b282dc9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:42:55 +0300 Subject: [PATCH 144/259] Add group module entities, DTOs, repository, and repository module --- libs/common/src/database/database.module.ts | 2 ++ .../src/modules/group/dtos/group.dto.ts | 11 +++++++++ libs/common/src/modules/group/dtos/index.ts | 1 + .../modules/group/entities/group.entity.ts | 23 +++++++++++++++++++ .../src/modules/group/entities/index.ts | 1 + .../modules/group/group.repository.module.ts | 11 +++++++++ .../group/repositories/group.repository.ts | 10 ++++++++ .../src/modules/group/repositories/index.ts | 1 + 8 files changed, 60 insertions(+) create mode 100644 libs/common/src/modules/group/dtos/group.dto.ts create mode 100644 libs/common/src/modules/group/dtos/index.ts create mode 100644 libs/common/src/modules/group/entities/group.entity.ts create mode 100644 libs/common/src/modules/group/entities/index.ts create mode 100644 libs/common/src/modules/group/group.repository.module.ts create mode 100644 libs/common/src/modules/group/repositories/group.repository.ts create mode 100644 libs/common/src/modules/group/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index aa4f781..543f522 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -10,6 +10,7 @@ import { ProductEntity } from '../modules/product/entities'; import { SpaceEntity } from '../modules/space/entities'; import { SpaceTypeEntity } from '../modules/space-type/entities'; import { UserSpaceEntity } from '../modules/user-space/entities'; +import { GroupEntity } from '../modules/group/entities'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { UserSpaceEntity } from '../modules/user-space/entities'; SpaceEntity, SpaceTypeEntity, UserSpaceEntity, + GroupEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/group/dtos/group.dto.ts b/libs/common/src/modules/group/dtos/group.dto.ts new file mode 100644 index 0000000..d3696b8 --- /dev/null +++ b/libs/common/src/modules/group/dtos/group.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GroupDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public groupName: string; +} diff --git a/libs/common/src/modules/group/dtos/index.ts b/libs/common/src/modules/group/dtos/index.ts new file mode 100644 index 0000000..ba43fbc --- /dev/null +++ b/libs/common/src/modules/group/dtos/index.ts @@ -0,0 +1 @@ +export * from './group.dto'; diff --git a/libs/common/src/modules/group/entities/group.entity.ts b/libs/common/src/modules/group/entities/group.entity.ts new file mode 100644 index 0000000..9835e63 --- /dev/null +++ b/libs/common/src/modules/group/entities/group.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity } from 'typeorm'; +import { GroupDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; + +@Entity({ name: 'group' }) +export class GroupEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public groupName: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/group/entities/index.ts b/libs/common/src/modules/group/entities/index.ts new file mode 100644 index 0000000..50f4201 --- /dev/null +++ b/libs/common/src/modules/group/entities/index.ts @@ -0,0 +1 @@ +export * from './group.entity'; diff --git a/libs/common/src/modules/group/group.repository.module.ts b/libs/common/src/modules/group/group.repository.module.ts new file mode 100644 index 0000000..5b711e5 --- /dev/null +++ b/libs/common/src/modules/group/group.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GroupEntity } from './entities/group.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([GroupEntity])], +}) +export class GroupRepositoryModule {} diff --git a/libs/common/src/modules/group/repositories/group.repository.ts b/libs/common/src/modules/group/repositories/group.repository.ts new file mode 100644 index 0000000..824d671 --- /dev/null +++ b/libs/common/src/modules/group/repositories/group.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { GroupEntity } from '../entities/group.entity'; + +@Injectable() +export class GroupRepository extends Repository { + constructor(private dataSource: DataSource) { + super(GroupEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/group/repositories/index.ts b/libs/common/src/modules/group/repositories/index.ts new file mode 100644 index 0000000..7018977 --- /dev/null +++ b/libs/common/src/modules/group/repositories/index.ts @@ -0,0 +1 @@ +export * from './group.repository'; From 239dbc84b12c1cb4cb327598904e22543eb2def8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:44:34 +0300 Subject: [PATCH 145/259] Add GroupDeviceEntity and related modules --- libs/common/src/database/database.module.ts | 7 ++++- .../modules/device/entities/device.entity.ts | 7 +++++ .../group-device/dtos/group.device.dto.ts | 15 ++++++++++ .../src/modules/group-device/dtos/index.ts | 1 + .../entities/group.device.entity.ts | 30 +++++++++++++++++++ .../modules/group-device/entities/index.ts | 1 + .../group.device.repository.module.ts | 11 +++++++ .../repositories/group.device.repository.ts | 10 +++++++ .../group-device/repositories/index.ts | 1 + .../modules/group/entities/group.entity.ts | 6 +++- 10 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/modules/group-device/dtos/group.device.dto.ts create mode 100644 libs/common/src/modules/group-device/dtos/index.ts create mode 100644 libs/common/src/modules/group-device/entities/group.device.entity.ts create mode 100644 libs/common/src/modules/group-device/entities/index.ts create mode 100644 libs/common/src/modules/group-device/group.device.repository.module.ts create mode 100644 libs/common/src/modules/group-device/repositories/group.device.repository.ts create mode 100644 libs/common/src/modules/group-device/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 06cff73..819be74 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -7,12 +7,16 @@ import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; import { HomeEntity } from '../modules/home/entities'; import { ProductEntity } from '../modules/product/entities'; -import { DeviceEntity, DeviceUserPermissionEntity } from '../modules/device/entities'; +import { + DeviceEntity, + DeviceUserPermissionEntity, +} from '../modules/device/entities'; import { PermissionTypeEntity } from '../modules/permission/entities'; import { SpaceEntity } from '../modules/space/entities'; import { SpaceTypeEntity } from '../modules/space-type/entities'; import { UserSpaceEntity } from '../modules/user-space/entities'; import { GroupEntity } from '../modules/group/entities'; +import { GroupDeviceEntity } from '../modules/group-device/entities'; @Module({ imports: [ @@ -40,6 +44,7 @@ import { GroupEntity } from '../modules/group/entities'; SpaceTypeEntity, UserSpaceEntity, GroupEntity, + GroupDeviceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index d90d2d1..2c1837a 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; import { DeviceUserPermissionEntity } from './device-user-type.entity'; +import { GroupDeviceEntity } from '../../group-device/entities'; @Entity({ name: 'device' }) export class DeviceEntity extends AbstractEntity { @@ -37,6 +38,12 @@ export class DeviceEntity extends AbstractEntity { ) permission: DeviceUserPermissionEntity[]; + @OneToMany( + () => GroupDeviceEntity, + (userGroupDevices) => userGroupDevices.device, + ) + userGroupDevices: GroupDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/group-device/dtos/group.device.dto.ts b/libs/common/src/modules/group-device/dtos/group.device.dto.ts new file mode 100644 index 0000000..1a4d51c --- /dev/null +++ b/libs/common/src/modules/group-device/dtos/group.device.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GroupDeviceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; + + @IsString() + @IsNotEmpty() + public groupUuid: string; +} diff --git a/libs/common/src/modules/group-device/dtos/index.ts b/libs/common/src/modules/group-device/dtos/index.ts new file mode 100644 index 0000000..66bc84a --- /dev/null +++ b/libs/common/src/modules/group-device/dtos/index.ts @@ -0,0 +1 @@ +export * from './group.device.dto'; diff --git a/libs/common/src/modules/group-device/entities/group.device.entity.ts b/libs/common/src/modules/group-device/entities/group.device.entity.ts new file mode 100644 index 0000000..d0ac5e7 --- /dev/null +++ b/libs/common/src/modules/group-device/entities/group.device.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { GroupDeviceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceEntity } from '../../device/entities'; +import { GroupEntity } from '../../group/entities'; + +@Entity({ name: 'group-device' }) +export class GroupDeviceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.userGroupDevices, { + nullable: false, + }) + device: DeviceEntity; + + @ManyToOne(() => GroupEntity, (group) => group.groupDevices, { + nullable: false, + }) + group: GroupEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/group-device/entities/index.ts b/libs/common/src/modules/group-device/entities/index.ts new file mode 100644 index 0000000..6b96f11 --- /dev/null +++ b/libs/common/src/modules/group-device/entities/index.ts @@ -0,0 +1 @@ +export * from './group.device.entity'; diff --git a/libs/common/src/modules/group-device/group.device.repository.module.ts b/libs/common/src/modules/group-device/group.device.repository.module.ts new file mode 100644 index 0000000..a3af56d --- /dev/null +++ b/libs/common/src/modules/group-device/group.device.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GroupDeviceEntity } from './entities/group.device.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([GroupDeviceEntity])], +}) +export class GroupDeviceRepositoryModule {} diff --git a/libs/common/src/modules/group-device/repositories/group.device.repository.ts b/libs/common/src/modules/group-device/repositories/group.device.repository.ts new file mode 100644 index 0000000..472c5aa --- /dev/null +++ b/libs/common/src/modules/group-device/repositories/group.device.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { GroupDeviceEntity } from '../entities/group.device.entity'; + +@Injectable() +export class GroupDeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(GroupDeviceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/group-device/repositories/index.ts b/libs/common/src/modules/group-device/repositories/index.ts new file mode 100644 index 0000000..2b40191 --- /dev/null +++ b/libs/common/src/modules/group-device/repositories/index.ts @@ -0,0 +1 @@ +export * from './group.device.repository'; diff --git a/libs/common/src/modules/group/entities/group.entity.ts b/libs/common/src/modules/group/entities/group.entity.ts index 9835e63..745ca42 100644 --- a/libs/common/src/modules/group/entities/group.entity.ts +++ b/libs/common/src/modules/group/entities/group.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { GroupDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { GroupDeviceEntity } from '../../group-device/entities'; @Entity({ name: 'group' }) export class GroupEntity extends AbstractEntity { @@ -16,6 +17,9 @@ export class GroupEntity extends AbstractEntity { }) public groupName: string; + @OneToMany(() => GroupDeviceEntity, (groupDevice) => groupDevice.group) + groupDevices: GroupDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); From af7a9dbd7237e13a8dbf5f1dea98d01c9f4bd974 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:46:21 +0300 Subject: [PATCH 146/259] Remove unused imports and fix formatting issues --- libs/common/src/auth/auth.module.ts | 2 +- .../src/modules/device/dtos/device.dto.ts | 31 +++++++++---------- .../src/modules/permission/dtos/index.ts | 2 +- .../src/modules/permission/entities/index.ts | 2 +- .../modules/permission/repositories/index.ts | 2 +- src/app.module.ts | 2 +- src/auth/controllers/user-auth.controller.ts | 3 +- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/libs/common/src/auth/auth.module.ts b/libs/common/src/auth/auth.module.ts index c74aa81..b6c6d45 100644 --- a/libs/common/src/auth/auth.module.ts +++ b/libs/common/src/auth/auth.module.ts @@ -1,6 +1,6 @@ import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { Module } from '@nestjs/common'; import { HelperModule } from '../helper/helper.module'; import { JwtStrategy } from './strategies/jwt.strategy'; diff --git a/libs/common/src/modules/device/dtos/device.dto.ts b/libs/common/src/modules/device/dtos/device.dto.ts index fbc07c3..8873f31 100644 --- a/libs/common/src/modules/device/dtos/device.dto.ts +++ b/libs/common/src/modules/device/dtos/device.dto.ts @@ -1,20 +1,19 @@ -import { IsNotEmpty, IsString } from "class-validator"; +import { IsNotEmpty, IsString } from 'class-validator'; -export class DeviceDto{ +export class DeviceDto { + @IsString() + @IsNotEmpty() + public uuid: string; - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - spaceUuid:string; + @IsString() + @IsNotEmpty() + spaceUuid: string; - @IsString() - @IsNotEmpty() - deviceTuyaUuid:string; + @IsString() + @IsNotEmpty() + deviceTuyaUuid: string; - @IsString() - @IsNotEmpty() - productUuid:string; -} \ No newline at end of file + @IsString() + @IsNotEmpty() + productUuid: string; +} diff --git a/libs/common/src/modules/permission/dtos/index.ts b/libs/common/src/modules/permission/dtos/index.ts index cd80da6..48e985e 100644 --- a/libs/common/src/modules/permission/dtos/index.ts +++ b/libs/common/src/modules/permission/dtos/index.ts @@ -1 +1 @@ -export * from './permission.dto' \ No newline at end of file +export * from './permission.dto'; diff --git a/libs/common/src/modules/permission/entities/index.ts b/libs/common/src/modules/permission/entities/index.ts index 2d9cedb..90a8fd8 100644 --- a/libs/common/src/modules/permission/entities/index.ts +++ b/libs/common/src/modules/permission/entities/index.ts @@ -1 +1 @@ -export * from './permission.entity' \ No newline at end of file +export * from './permission.entity'; diff --git a/libs/common/src/modules/permission/repositories/index.ts b/libs/common/src/modules/permission/repositories/index.ts index cdd0f3a..528b955 100644 --- a/libs/common/src/modules/permission/repositories/index.ts +++ b/libs/common/src/modules/permission/repositories/index.ts @@ -1 +1 @@ -export * from './permission.repository' \ No newline at end of file +export * from './permission.repository'; diff --git a/src/app.module.ts b/src/app.module.ts index d6daa3d..1a60f18 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,7 +29,7 @@ import { UnitModule } from './unit/unit.module'; RoomModule, GroupModule, DeviceModule, - UserDevicePermissionModule + UserDevicePermissionModule, ], controllers: [AuthenticationController], }) diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index f2a4b6f..f8c45f9 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -16,7 +16,6 @@ import { ResponseMessage } from '../../../libs/common/src/response/response.deco import { UserLoginDto } from '../dtos/user-login.dto'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; -import { Request } from 'express'; import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; @Controller({ @@ -101,7 +100,7 @@ export class UserAuthController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/list') - async userList(@Req() req) { + async userList() { const userList = await this.userAuthService.userList(); return { statusCode: HttpStatus.OK, From 18e7e35d352759791b3fa6f6333c28a123bc19d2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:27:32 +0300 Subject: [PATCH 147/259] Add unique constraint to GroupDeviceEntity and isActive flag to both GroupDeviceEntity and GroupEntity --- .../group-device/entities/group.device.entity.ts | 8 +++++++- libs/common/src/modules/group/entities/group.entity.ts | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/group-device/entities/group.device.entity.ts b/libs/common/src/modules/group-device/entities/group.device.entity.ts index d0ac5e7..276a2b6 100644 --- a/libs/common/src/modules/group-device/entities/group.device.entity.ts +++ b/libs/common/src/modules/group-device/entities/group.device.entity.ts @@ -1,10 +1,11 @@ -import { Column, Entity, ManyToOne } from 'typeorm'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { GroupDeviceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; import { GroupEntity } from '../../group/entities'; @Entity({ name: 'group-device' }) +@Unique(['device', 'group']) export class GroupDeviceEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -23,6 +24,11 @@ export class GroupDeviceEntity extends AbstractEntity { }) group: GroupEntity; + @Column({ + nullable: true, + default: true, + }) + public isActive: boolean; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/group/entities/group.entity.ts b/libs/common/src/modules/group/entities/group.entity.ts index 745ca42..7cea8e8 100644 --- a/libs/common/src/modules/group/entities/group.entity.ts +++ b/libs/common/src/modules/group/entities/group.entity.ts @@ -17,9 +17,17 @@ export class GroupEntity extends AbstractEntity { }) public groupName: string; - @OneToMany(() => GroupDeviceEntity, (groupDevice) => groupDevice.group) + @OneToMany(() => GroupDeviceEntity, (groupDevice) => groupDevice.group, { + cascade: true, + onDelete: 'CASCADE', + }) groupDevices: GroupDeviceEntity[]; + @Column({ + nullable: true, + default: true, + }) + public isActive: boolean; constructor(partial: Partial) { super(); Object.assign(this, partial); From 7abfe2974613be2c79ef54ca98d8a6e634a23098 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:27:53 +0300 Subject: [PATCH 148/259] Refactor group controller methods and add device product guard --- src/group/controllers/group.controller.ts | 90 ++++--- src/group/dtos/add.group.dto.ts | 24 +- src/group/dtos/get.group.dto.ts | 28 -- src/group/dtos/rename.group.dto copy.ts | 10 +- src/group/group.module.ts | 20 +- src/group/interfaces/get.group.interface.ts | 28 +- src/group/services/group.service.ts | 273 +++++++++----------- src/guards/device.product.guard.ts | 71 +++++ 8 files changed, 284 insertions(+), 260 deletions(-) delete mode 100644 src/group/dtos/get.group.dto.ts create mode 100644 src/guards/device.product.guard.ts diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index be0c7a3..2b6dd82 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -5,17 +5,18 @@ import { Get, Post, UseGuards, - Query, Param, Put, Delete, + HttpException, + HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddGroupDto } from '../dtos/add.group.dto'; -import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; +import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard'; @ApiTags('Group Module') @Controller({ @@ -25,34 +26,43 @@ import { RenameGroupDto } from '../dtos/rename.group.dto copy'; export class GroupController { constructor(private readonly groupService: GroupService) {} - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get() - async getGroupsByHomeId(@Query() getGroupsDto: GetGroupDto) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Get('space/:spaceUuid') + async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { try { - return await this.groupService.getGroupsByHomeId(getGroupsDto); - } catch (err) { - throw new Error(err); + return await this.groupService.getGroupsBySpaceUuid(spaceUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get(':groupId') - async getGroupsByGroupId(@Param('groupId') groupId: number) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Get(':groupUuid') + async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { try { - return await this.groupService.getGroupsByGroupId(groupId); - } catch (err) { - throw new Error(err); + return await this.groupService.getGroupsByGroupUuid(groupUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + // @ApiBearerAuth() + @UseGuards(CheckProductUuidForAllDevicesGuard) @Post() async addGroup(@Body() addGroupDto: AddGroupDto) { try { return await this.groupService.addGroup(addGroupDto); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -67,25 +77,37 @@ export class GroupController { } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put('rename') - async renameGroup(@Body() renameGroupDto: RenameGroupDto) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Put('rename/:groupUuid') + async renameGroupByUuid( + @Param('groupUuid') groupUuid: string, + @Body() renameGroupDto: RenameGroupDto, + ) { try { - return await this.groupService.renameGroup(renameGroupDto); - } catch (err) { - throw new Error(err); + return await this.groupService.renameGroupByUuid( + groupUuid, + renameGroupDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Delete(':groupId') - async deleteGroup(@Param('groupId') groupId: number) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Delete(':groupUuid') + async deleteGroup(@Param('groupUuid') groupUuid: string) { try { - return await this.groupService.deleteGroup(groupId); - } catch (err) { - throw new Error(err); + return await this.groupService.deleteGroup(groupUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/group/dtos/add.group.dto.ts b/src/group/dtos/add.group.dto.ts index b91f793..aa2a562 100644 --- a/src/group/dtos/add.group.dto.ts +++ b/src/group/dtos/add.group.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString, IsArray } from 'class-validator'; export class AddGroupDto { @ApiProperty({ @@ -11,26 +11,10 @@ export class AddGroupDto { public groupName: string; @ApiProperty({ - description: 'homeId', + description: 'deviceUuids', required: true, }) - @IsNumberString() + @IsArray() @IsNotEmpty() - public homeId: string; - - @ApiProperty({ - description: 'productId', - required: true, - }) - @IsString() - @IsNotEmpty() - public productId: string; - - @ApiProperty({ - description: 'The list of up to 20 device IDs, separated with commas (,)', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceIds: string; + public deviceUuids: [string]; } diff --git a/src/group/dtos/get.group.dto.ts b/src/group/dtos/get.group.dto.ts deleted file mode 100644 index aad234b..0000000 --- a/src/group/dtos/get.group.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumberString } from 'class-validator'; - -export class GetGroupDto { - @ApiProperty({ - description: 'homeId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public homeId: string; - - @ApiProperty({ - description: 'pageSize', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageSize: number; - - @ApiProperty({ - description: 'pageNo', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageNo: number; -} diff --git a/src/group/dtos/rename.group.dto copy.ts b/src/group/dtos/rename.group.dto copy.ts index a85f41b..f2b0c00 100644 --- a/src/group/dtos/rename.group.dto copy.ts +++ b/src/group/dtos/rename.group.dto copy.ts @@ -1,15 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class RenameGroupDto { - @ApiProperty({ - description: 'groupId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public groupId: string; - @ApiProperty({ description: 'groupName', required: true, diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 3969d39..61d95ab 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -2,10 +2,26 @@ import { Module } from '@nestjs/common'; import { GroupService } from './services/group.service'; import { GroupController } from './controllers/group.controller'; import { ConfigModule } from '@nestjs/config'; +import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; +import { GroupRepository } from '@app/common/modules/group/repositories'; +import { GroupDeviceRepositoryModule } from '@app/common/modules/group-device/group.device.repository.module'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; @Module({ - imports: [ConfigModule], + imports: [ + ConfigModule, + GroupRepositoryModule, + GroupDeviceRepositoryModule, + DeviceRepositoryModule, + ], controllers: [GroupController], - providers: [GroupService], + providers: [ + GroupService, + GroupRepository, + GroupDeviceRepository, + DeviceRepository, + ], exports: [GroupService], }) export class GroupModule {} diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 970c343..525fa04 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -1,25 +1,15 @@ -export class GetGroupDetailsInterface { - result: { - id: string; - name: string; - }; +export interface GetGroupDetailsInterface { + groupUuid: string; + groupName: string; + createdAt: Date; + updatedAt: Date; } -export class GetGroupsInterface { - result: { - count: number; - data_list: []; - }; +export interface GetGroupsBySpaceUuidInterface { + groupUuid: string; + groupName: string; } -export class addGroupInterface { - success: boolean; - msg: string; - result: { - id: string; - }; -} - -export class controlGroupInterface { +export interface controlGroupInterface { success: boolean; result: boolean; msg: string; diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 07fbacf..f559737 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -1,21 +1,30 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupDetailsInterface, - GetGroupsInterface, - addGroupInterface, + GetGroupsBySpaceUuidInterface, controlGroupInterface, } from '../interfaces/get.group.interface'; -import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; +import { GroupRepository } from '@app/common/modules/group/repositories'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @Injectable() export class GroupService { private tuya: TuyaContext; - constructor(private readonly configService: ConfigService) { + constructor( + private readonly configService: ConfigService, + private readonly groupRepository: GroupRepository, + private readonly groupDeviceRepository: GroupDeviceRepository, + ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); // const clientId = this.configService.get('auth-config.CLIENT_ID'); @@ -26,83 +35,80 @@ export class GroupService { }); } - async getGroupsByHomeId(getGroupDto: GetGroupDto) { + async getGroupsBySpaceUuid( + spaceUuid: string, + ): Promise { try { - const response = await this.getGroupsTuya(getGroupDto); - - const groups = response.result.data_list.map((group: any) => ({ - groupId: group.id, - groupName: group.name, - })); - - return { - count: response.result.count, - groups: groups, - }; - } catch (error) { - throw new HttpException( - 'Error fetching groups', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getGroupsTuya(getGroupDto: GetGroupDto): Promise { - try { - const path = `/v2.0/cloud/thing/group`; - const response = await this.tuya.request({ - method: 'GET', - path, - query: { - space_id: getGroupDto.homeId, - page_size: getGroupDto.pageSize, - page_no: getGroupDto.pageNo, + const groupDevices = await this.groupDeviceRepository.find({ + relations: ['group', 'device'], + where: { + device: { spaceUuid }, + isActive: true, }, }); - return response as unknown as GetGroupsInterface; + + // Extract and return only the group entities + const groups = groupDevices.map((groupDevice) => { + return { + groupUuid: groupDevice.uuid, + groupName: groupDevice.group.groupName, + }; + }); + if (groups.length > 0) { + return groups; + } else { + throw new HttpException( + 'this space has no groups', + HttpStatus.NOT_FOUND, + ); + } } catch (error) { throw new HttpException( - 'Error fetching groups ', - HttpStatus.INTERNAL_SERVER_ERROR, + error.message || 'Error fetching groups', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } async addGroup(addGroupDto: AddGroupDto) { - const response = await this.addGroupTuya(addGroupDto); + try { + const group = await this.groupRepository.save({ + groupName: addGroupDto.groupName, + }); - if (response.success) { - return { - success: true, - groupId: response.result.id, - }; - } else { + const groupDevicePromises = addGroupDto.deviceUuids.map( + async (deviceUuid) => { + await this.saveGroupDevice(group.uuid, deviceUuid); + }, + ); + + await Promise.all(groupDevicePromises); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this group', + HttpStatus.BAD_REQUEST, + ); + } throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async addGroupTuya(addGroupDto: AddGroupDto): Promise { + + private async saveGroupDevice(groupUuid: string, deviceUuid: string) { try { - const path = `/v2.0/cloud/thing/group`; - const response = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: addGroupDto.homeId, - name: addGroupDto.groupName, - product_id: addGroupDto.productId, - device_ids: addGroupDto.deviceIds, + await this.groupDeviceRepository.save({ + group: { + uuid: groupUuid, + }, + device: { + uuid: deviceUuid, }, }); - - return response as addGroupInterface; } catch (error) { - throw new HttpException( - 'Error adding group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw error; } } @@ -141,105 +147,76 @@ export class GroupService { } } - async renameGroup(renameGroupDto: RenameGroupDto) { - const response = await this.renameGroupTuya(renameGroupDto); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async renameGroupTuya( + async renameGroupByUuid( + groupUuid: string, renameGroupDto: RenameGroupDto, - ): Promise { + ): Promise { try { - const path = `/v2.0/cloud/thing/group/${renameGroupDto.groupId}/${renameGroupDto.groupName}`; - const response = await this.tuya.request({ - method: 'PUT', - path, + await this.groupRepository.update( + { uuid: groupUuid }, + { groupName: renameGroupDto.groupName }, + ); + + // Fetch the updated floor + const updatedGroup = await this.groupRepository.findOneOrFail({ + where: { uuid: groupUuid }, }); - - return response as controlGroupInterface; - } catch (error) { - throw new HttpException( - 'Error rename group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async deleteGroup(groupId: number) { - const response = await this.deleteGroupTuya(groupId); - - if (response.success) { return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async deleteGroupTuya(groupId: number): Promise { - try { - const path = `/v2.0/cloud/thing/group/${groupId}`; - const response = await this.tuya.request({ - method: 'DELETE', - path, - }); - - return response as controlGroupInterface; - } catch (error) { - throw new HttpException( - 'Error delete group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getGroupsByGroupId(groupId: number) { - try { - const response = await this.getGroupsByGroupIdTuya(groupId); - - return { - groupId: response.result.id, - groupName: response.result.name, + groupUuid: updatedGroup.uuid, + groupName: updatedGroup.groupName, }; + } catch (error) { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } + } + + async deleteGroup(groupUuid: string) { + try { + const group = await this.getGroupsByGroupUuid(groupUuid); + + if (!group) { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } + + await this.groupRepository.update( + { uuid: groupUuid }, + { isActive: false }, + ); + + return { message: 'Group deleted successfully' }; } catch (error) { throw new HttpException( - 'Error fetching group', - HttpStatus.INTERNAL_SERVER_ERROR, + error.message || 'Error deleting group', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getGroupsByGroupIdTuya( - groupId: number, + async getGroupsByGroupUuid( + groupUuid: string, ): Promise { try { - const path = `/v2.0/cloud/thing/group/${groupId}`; - const response = await this.tuya.request({ - method: 'GET', - path, + const group = await this.groupRepository.findOne({ + where: { + uuid: groupUuid, + isActive: true, + }, }); - return response as GetGroupDetailsInterface; - } catch (error) { - throw new HttpException( - 'Error fetching group ', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (!group) { + throw new BadRequestException('Invalid group UUID'); + } + return { + groupUuid: group.uuid, + groupName: group.groupName, + createdAt: group.createdAt, + updatedAt: group.updatedAt, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } } } } diff --git a/src/guards/device.product.guard.ts b/src/guards/device.product.guard.ts new file mode 100644 index 0000000..2118401 --- /dev/null +++ b/src/guards/device.product.guard.ts @@ -0,0 +1,71 @@ +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + BadRequestException, + ExecutionContext, +} from '@nestjs/common'; + +@Injectable() +export class CheckProductUuidForAllDevicesGuard implements CanActivate { + constructor(private readonly deviceRepository: DeviceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { deviceUuids } = req.body; + console.log(deviceUuids); + + await this.checkAllDevicesHaveSameProductUuid(deviceUuids); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkAllDevicesHaveSameProductUuid(deviceUuids: string[]) { + const firstDevice = await this.deviceRepository.findOne({ + where: { uuid: deviceUuids[0] }, + }); + + if (!firstDevice) { + throw new BadRequestException('First device not found'); + } + + const firstProductUuid = firstDevice.productUuid; + + for (let i = 1; i < deviceUuids.length; i++) { + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuids[i] }, + }); + + if (!device) { + throw new BadRequestException(`Device ${deviceUuids[i]} not found`); + } + + if (device.productUuid !== firstProductUuid) { + throw new BadRequestException(`Devices have different product UUIDs`); + } + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Device not found', + }); + } + } +} From fc7907d204c27b9eaf6b7333536597d8929cdbc9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:05:54 +0300 Subject: [PATCH 149/259] Refactor group controller and service for better code organization --- src/group/controllers/group.controller.ts | 27 +++++----- src/group/dtos/control.group.dto.ts | 20 +++++--- src/group/services/group.service.ts | 61 ++++++++++++++++++----- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 2b6dd82..0f74ff6 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -26,8 +26,8 @@ import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.gu export class GroupController { constructor(private readonly groupService: GroupService) {} - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('space/:spaceUuid') async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { try { @@ -39,8 +39,8 @@ export class GroupController { ); } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get(':groupUuid') async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { try { @@ -52,8 +52,8 @@ export class GroupController { ); } } - // @ApiBearerAuth() - @UseGuards(CheckProductUuidForAllDevicesGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckProductUuidForAllDevicesGuard) @Post() async addGroup(@Body() addGroupDto: AddGroupDto) { try { @@ -72,13 +72,16 @@ export class GroupController { async controlGroup(@Body() controlGroupDto: ControlGroupDto) { try { return await this.groupService.controlGroup(controlGroupDto); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Put('rename/:groupUuid') async renameGroupByUuid( @Param('groupUuid') groupUuid: string, @@ -97,8 +100,8 @@ export class GroupController { } } - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Delete(':groupUuid') async deleteGroup(@Param('groupUuid') groupUuid: string) { try { diff --git a/src/group/dtos/control.group.dto.ts b/src/group/dtos/control.group.dto.ts index 33a6870..e3b48e9 100644 --- a/src/group/dtos/control.group.dto.ts +++ b/src/group/dtos/control.group.dto.ts @@ -1,20 +1,26 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsObject, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class ControlGroupDto { @ApiProperty({ - description: 'groupId', + description: 'groupUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public groupId: string; + public groupUuid: string; @ApiProperty({ - description: 'example {"switch_1":true,"add_ele":300}', + description: 'code', required: true, }) - @IsObject() + @IsString() @IsNotEmpty() - public properties: object; + public code: string; + @ApiProperty({ + description: 'value', + required: true, + }) + @IsNotEmpty() + public value: any; } diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index f559737..63e63e9 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -10,12 +10,13 @@ import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupDetailsInterface, GetGroupsBySpaceUuidInterface, - controlGroupInterface, } from '../interfaces/get.group.interface'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; import { GroupRepository } from '@app/common/modules/group/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { controlDeviceInterface } from 'src/device/interfaces/get.device.interface'; +import { ControlDeviceDto } from 'src/device/dtos'; @Injectable() export class GroupService { @@ -111,9 +112,24 @@ export class GroupService { throw error; } } - - async controlGroup(controlGroupDto: ControlGroupDto) { - const response = await this.controlGroupTuya(controlGroupDto); + async getDevicesByGroupUuid(groupUuid: string) { + try { + const devices = await this.groupDeviceRepository.find({ + relations: ['device'], + where: { + group: { + uuid: groupUuid, + }, + isActive: true, + }, + }); + return devices; + } catch (error) { + throw error; + } + } + async controlDevice(controlDeviceDto: ControlDeviceDto) { + const response = await this.controlDeviceTuya(controlDeviceDto); if (response.success) { return response; @@ -124,24 +140,45 @@ export class GroupService { ); } } - async controlGroupTuya( - controlGroupDto: ControlGroupDto, - ): Promise { + async controlDeviceTuya( + controlDeviceDto: ControlDeviceDto, + ): Promise { try { - const path = `/v2.0/cloud/thing/group/properties`; + const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceId}/commands`; const response = await this.tuya.request({ method: 'POST', path, body: { - group_id: controlGroupDto.groupId, - properties: controlGroupDto.properties, + commands: [ + { code: controlDeviceDto.code, value: controlDeviceDto.value }, + ], }, }); - return response as controlGroupInterface; + return response as controlDeviceInterface; } catch (error) { throw new HttpException( - 'Error control group', + 'Error control device from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async controlGroup(controlGroupDto: ControlGroupDto) { + const devices = await this.getDevicesByGroupUuid(controlGroupDto.groupUuid); + + try { + await Promise.all( + devices.map(async (device) => { + return this.controlDevice({ + deviceId: device.device.deviceTuyaUuid, + code: controlGroupDto.code, + value: controlGroupDto.value, + }); + }), + ); + } catch (error) { + throw new HttpException( + 'Error controlling devices', HttpStatus.INTERNAL_SERVER_ERROR, ); } From bef2f1e4ad26a0b2e610532a4b98beda28a85499 Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Tue, 23 Apr 2024 10:38:09 +0530 Subject: [PATCH 150/259] added listing api --- .../permission/entities/permission.entity.ts | 14 +++++++++++++- .../user-device-permission.controller.ts | 19 +++++++++++++++++++ .../user-device-permission.service.ts | 4 ++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/permission/entities/permission.entity.ts b/libs/common/src/modules/permission/entities/permission.entity.ts index d4d17de..f2ba950 100644 --- a/libs/common/src/modules/permission/entities/permission.entity.ts +++ b/libs/common/src/modules/permission/entities/permission.entity.ts @@ -1,7 +1,8 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { PermissionTypeDto } from '../dtos/permission.dto'; +import { DeviceUserPermissionEntity } from '../../device/entities'; @Entity({ name: 'permission-type' }) export class PermissionTypeEntity extends AbstractEntity { @@ -11,6 +12,17 @@ export class PermissionTypeEntity extends AbstractEntity { }) type: string; + @OneToMany( + () => DeviceUserPermissionEntity, + (permission) => permission.type, + { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + ) + permission: DeviceUserPermissionEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index bfd98cb..91d944c 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Get, HttpStatus, Param, Post, @@ -65,4 +66,22 @@ export class UserDevicePermissionController { throw new Error(err); } } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('list') + async fetchDevicePermission() { + try { + const deviceDetails = + await this.userDevicePermissionService.fetchuserPermission(); + return { + statusCode: HttpStatus.OK, + message: 'Device Details fetched Successfully', + data: deviceDetails, + }; + } catch (err) { + throw new Error(err); + } + } + } diff --git a/src/user-device-permission/services/user-device-permission.service.ts b/src/user-device-permission/services/user-device-permission.service.ts index 7dbb971..e50d59f 100644 --- a/src/user-device-permission/services/user-device-permission.service.ts +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -29,4 +29,8 @@ export class UserDevicePermissionService { }, ); } + + async fetchuserPermission() { + return await this.deviceUserTypeRepository.find(); + } } From 59badb41a3e8e83977a5e0f9450c3b32601493f9 Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Tue, 23 Apr 2024 12:41:37 +0530 Subject: [PATCH 151/259] auth permission guard added --- src/device/controllers/device.controller.ts | 18 ++-- src/device/device.module.ts | 7 +- src/guards/device.permission.guard.ts | 97 +++++++++++++++++++++ 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/guards/device.permission.guard.ts diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 136e94f..387e765 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -19,6 +19,8 @@ import { GetDeviceByRoomIdDto, } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; +import { AuthGuardWithRoles } from 'src/guards/device.permission.guard'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; @ApiTags('Device Module') @Controller({ @@ -29,7 +31,7 @@ export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.READ) @Get('room') async getDevicesByRoomId( @Query() getDeviceByRoomIdDto: GetDeviceByRoomIdDto, @@ -41,7 +43,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.READ) @Get('group') async getDevicesByGroupId( @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, @@ -55,7 +57,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.READ) @Get(':deviceId') async getDeviceDetailsByDeviceId(@Param('deviceId') deviceId: string) { try { @@ -65,7 +67,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.READ) @Get(':deviceId/functions') async getDeviceInstructionByDeviceId(@Param('deviceId') deviceId: string) { try { @@ -75,7 +77,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.READ) @Get(':deviceId/functions/status') async getDevicesInstructionStatus(@Param('deviceId') deviceId: string) { try { @@ -85,7 +87,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.CONTROLLABLE) @Post('room') async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { try { @@ -95,7 +97,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.CONTROLLABLE) @Post('group') async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { try { @@ -105,7 +107,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @AuthGuardWithRoles(PermissionType.CONTROLLABLE) @Post('control') async controlDevice(@Body() controlDeviceDto: ControlDeviceDto) { try { diff --git a/src/device/device.module.ts b/src/device/device.module.ts index d72db05..55d50b4 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -4,10 +4,13 @@ import { DeviceController } from './controllers/device.controller'; import { ConfigModule } from '@nestjs/config'; import { ProductRepositoryModule } from '@app/common/modules/product/product.repository.module'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ - imports: [ConfigModule, ProductRepositoryModule], + imports: [ConfigModule, ProductRepositoryModule,DeviceRepositoryModule], controllers: [DeviceController], - providers: [DeviceService, ProductRepository], + providers: [DeviceService, ProductRepository,DeviceUserTypeRepository,PermissionTypeRepository], exports: [DeviceService], }) export class DeviceModule {} diff --git a/src/guards/device.permission.guard.ts b/src/guards/device.permission.guard.ts new file mode 100644 index 0000000..3fc80af --- /dev/null +++ b/src/guards/device.permission.guard.ts @@ -0,0 +1,97 @@ +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + ExecutionContext, + BadRequestException, + applyDecorators, + SetMetadata, + UseGuards, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class DevicePermissionGuard implements CanActivate { + constructor( + private reflector: Reflector, + private readonly deviceUserTypeRepository: DeviceUserTypeRepository, + private readonly permissionTypeRepository: PermissionTypeRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + try { + const { deviceId } = req.headers; + const userId = req.user.uuid; + + const requirePermission = + this.reflector.getAllAndOverride('permission', [ + context.getHandler(), + context.getClass(), + ]); + + if (!requirePermission) { + return true; + } + await this.checkDevicePermission(deviceId, userId, requirePermission); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkDevicePermission( + deviceId: string, + userId: string, + requirePermission, + ) { + const [userPermissionDetails, permissionDetails] = await Promise.all([ + this.deviceUserTypeRepository.findOne({ + where: { deviceUuid: deviceId, userUuid: userId }, + }), + this.permissionTypeRepository.findOne({ + where: { + type: requirePermission, + }, + }), + ]); + if (!userPermissionDetails) { + throw new BadRequestException('User Permission Details Not Found'); + } + if (userPermissionDetails.permissionTypeUuid !== permissionDetails.uuid) { + throw new BadRequestException( + `User Does not have a ${requirePermission}`, + ); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'User Permission not found', + }); + } + } +} + +export function AuthGuardWithRoles(permission?: string) { + return applyDecorators( + SetMetadata('permission', permission), + UseGuards(JwtAuthGuard), + UseGuards(DevicePermissionGuard), + ); +} From d61a1b92e7dbe3957296c19df7a1a80ccb18f7ee Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Tue, 23 Apr 2024 16:24:58 +0530 Subject: [PATCH 152/259] Device Related API changes are done --- .../entities/group.device.entity.ts | 12 ++ src/device/device.module.ts | 16 ++- src/device/services/device.service.ts | 105 +++++++++++++++--- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/libs/common/src/modules/group-device/entities/group.device.entity.ts b/libs/common/src/modules/group-device/entities/group.device.entity.ts index 276a2b6..8a39dc7 100644 --- a/libs/common/src/modules/group-device/entities/group.device.entity.ts +++ b/libs/common/src/modules/group-device/entities/group.device.entity.ts @@ -14,6 +14,18 @@ export class GroupDeviceEntity extends AbstractEntity { }) public uuid: string; + @Column({ + type: 'string', + nullable: false, + }) + deviceUuid: string; + + @Column({ + type: 'string', + nullable: false, + }) + groupUuid: string; + @ManyToOne(() => DeviceEntity, (device) => device.userGroupDevices, { nullable: false, }) diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 55d50b4..3a829fc 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -5,12 +5,22 @@ import { ConfigModule } from '@nestjs/config'; import { ProductRepositoryModule } from '@app/common/modules/product/product.repository.module'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceRepositoryModule } from '@app/common/modules/device'; -import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { DeviceRepository, DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @Module({ - imports: [ConfigModule, ProductRepositoryModule,DeviceRepositoryModule], + imports: [ConfigModule, ProductRepositoryModule, DeviceRepositoryModule], controllers: [DeviceController], - providers: [DeviceService, ProductRepository,DeviceUserTypeRepository,PermissionTypeRepository], + providers: [ + DeviceService, + ProductRepository, + DeviceUserTypeRepository, + PermissionTypeRepository, + SpaceRepository, + DeviceRepository, + GroupDeviceRepository + ], exports: [DeviceService], }) export class DeviceModule {} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 0fa0be7..2560efb 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1,4 +1,9 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + NotFoundException, +} from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { @@ -23,6 +28,9 @@ import { import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @Injectable() export class DeviceService { @@ -30,6 +38,9 @@ export class DeviceService { constructor( private readonly configService: ConfigService, private readonly productRepository: ProductRepository, + private readonly spaceRepository: SpaceRepository, + private readonly deviceRepository: DeviceRepository, + private readonly groupDeviceRepository: GroupDeviceRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -42,6 +53,15 @@ export class DeviceService { async getDevicesByRoomId(getDeviceByRoomIdDto: GetDeviceByRoomIdDto) { try { + const findRoom = await this.spaceRepository.findOne({ + where: { + uuid: getDeviceByRoomIdDto.roomId, + }, + }); + if (!findRoom) { + throw new NotFoundException('Room Details Not Found'); + } + const response = await this.getDevicesByRoomIdTuya(getDeviceByRoomIdDto); return { @@ -126,7 +146,27 @@ export class DeviceService { } async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { - const response = await this.addDeviceInRoomTuya(addDeviceInRoomDto); + const [deviceDetails, roomDetails] = await Promise.all([ + this.deviceRepository.findOne({ + where: { + uuid: addDeviceInRoomDto.deviceId, + }, + }), + this.spaceRepository.findOne({ + where: { + uuid: addDeviceInRoomDto.roomId, + }, + }), + ]); + + if (!roomDetails) { + throw new NotFoundException('Room Details Not Found'); + } + + if (!deviceDetails) { + throw new NotFoundException('Device Details Not Found'); + } + const response = await this.addDeviceInRooms(addDeviceInRoomDto); if (response.success) { return { @@ -141,7 +181,7 @@ export class DeviceService { ); } } - async addDeviceInRoomTuya( + async addDeviceInRooms( addDeviceInRoomDto: AddDeviceInRoomDto, ): Promise { try { @@ -153,7 +193,6 @@ export class DeviceService { space_id: addDeviceInRoomDto.roomId, }, }); - return response as addDeviceInRoomInterface; } catch (error) { throw new HttpException( @@ -163,7 +202,17 @@ export class DeviceService { } } async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { - const response = await this.addDeviceInGroupTuya(addDeviceInGroupDto); + const deviceDetails = this.deviceRepository.findOne({ + where: { + uuid: addDeviceInGroupDto.deviceId, + }, + }); + + if (!deviceDetails) { + throw new NotFoundException('Device Details Not Found'); + } + + const response = await this.addDeviceInGroups(addDeviceInGroupDto); if (response.success) { return { @@ -178,21 +227,21 @@ export class DeviceService { ); } } - async addDeviceInGroupTuya( + async addDeviceInGroups( addDeviceInGroupDto: AddDeviceInGroupDto, ): Promise { try { - const path = `/v2.0/cloud/thing/group/${addDeviceInGroupDto.groupId}/device`; - const response = await this.tuya.request({ - method: 'PUT', - path, - body: { - space_id: addDeviceInGroupDto.homeId, - device_ids: addDeviceInGroupDto.deviceId, - }, + await this.groupDeviceRepository.create({ + deviceUuid: addDeviceInGroupDto.deviceId, + groupUuid: addDeviceInGroupDto.groupId, }); - return response as addDeviceInRoomInterface; + return { + success: true, + msg: 'Group is Added to Specific Device', + result: true, + } as addDeviceInRoomInterface; + } catch (error) { throw new HttpException( 'Error adding device in group from Tuya', @@ -239,6 +288,15 @@ export class DeviceService { async getDeviceDetailsByDeviceId(deviceId: string) { try { + const deviceDetails = await this.deviceRepository.findOne({ + where: { + uuid: deviceId, + }, + }); + if (!deviceDetails) { + throw new NotFoundException('Device Details Not Found'); + } + const response = await this.getDeviceDetailsByDeviceIdTuya(deviceId); return { @@ -335,6 +393,15 @@ export class DeviceService { deviceId: string, ): Promise { try { + const deviceDetails = await this.deviceRepository.findOne({ + where: { + uuid: deviceId, + }, + }); + if (!deviceDetails) { + throw new NotFoundException('Device Details Not Found'); + } + const response = await this.getDeviceInstructionByDeviceIdTuya(deviceId); const productId: string = await this.getProductIdByDeviceId(deviceId); @@ -383,6 +450,14 @@ export class DeviceService { } async getDevicesInstructionStatus(deviceId: string) { try { + const deviceDetails = await this.deviceRepository.findOne({ + where: { + uuid: deviceId, + }, + }); + if (!deviceDetails) { + throw new NotFoundException('Device Details Not Found'); + } const deviceStatus = await this.getDevicesInstructionStatusTuya(deviceId); const productId: string = await this.getProductIdByDeviceId(deviceId); const productType: string = From c311c51f687b8819353c2e5bb5da7b4c7f744fae Mon Sep 17 00:00:00 2001 From: VirajBrainvire Date: Tue, 23 Apr 2024 18:09:50 +0530 Subject: [PATCH 153/259] API related changes done --- src/device/interfaces/get.device.interface.ts | 2 +- src/device/services/device.service.ts | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index 30f57f8..d513c17 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -8,7 +8,7 @@ export interface GetDeviceDetailsInterface { export interface GetDevicesByRoomIdInterface { success: boolean; msg: string; - result: []; + result:any; } export interface GetDevicesByGroupIdInterface { diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 2560efb..dfa2e91 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -82,7 +82,12 @@ export class DeviceService { ): Promise { try { const path = `/v2.0/cloud/thing/space/device`; - const response = await this.tuya.request({ + const getDeviceByRoomId = await this.deviceRepository.find({ + where: { + deviceTuyaUuid: getDeviceByRoomIdDto.roomId, + }, + }); + const response:any = await this.tuya.request({ method: 'GET', path, query: { @@ -90,7 +95,26 @@ export class DeviceService { page_size: getDeviceByRoomIdDto.pageSize, }, }); - return response as GetDevicesByRoomIdInterface; + if (!getDeviceByRoomId.length) { + throw new NotFoundException('Devices Not Found'); + } + + const matchingRecords = []; + + getDeviceByRoomId.forEach((item1) => { + const matchingItem = response.find( + (item2) => item1.deviceTuyaUuid === item2.uuid, + ); + if (matchingItem) { + matchingRecords.push({...matchingItem }); + } + }); + + return { + success:true, + msg:'Device Tuya Details Fetched successfully', + result:matchingRecords + }; } catch (error) { throw new HttpException( 'Error fetching devices by room from Tuya', @@ -241,7 +265,6 @@ export class DeviceService { msg: 'Group is Added to Specific Device', result: true, } as addDeviceInRoomInterface; - } catch (error) { throw new HttpException( 'Error adding device in group from Tuya', From e0a155382b6aab5a000fdb35713f322353d77ff6 Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Tue, 23 Apr 2024 09:10:31 -0400 Subject: [PATCH 154/259] updating the ReadMe --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dee6f14..ab85df8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ # Backend -## Description +## Overview +This is the backend for an IoT application built using NestJS. It interfaces with the Tuya IoT cloud platform to manage homes, rooms, devices, ...etc. This is the backend APIs project, developed with NestJS for Syncrow IOT Project. +## Database Model +The database uses PostgreSQL and TypeORM. Below is an entity relationship diagram: + +The main entities are: + +User - Stores user account information +Home - Represents a home/space +Room - Represents a room/sub-space +Device - Represents a connected device +Product - Stores metadata about device products +Other Entities - sessions, OTPs, etc. + +The entities have a one-to-many relationship - a user has multiple homes, a home has multiple rooms, and a room has multiple devices. + +## Architecture +The application is deployed on Azure App Service using Docker containers. There are separate deployment slots for development, staging, and production environments. + + ## Installation First, ensure that you have Node.js `v20.11` or newer (LTS ONLY) installed on your system. From d8ea23e1a7a0660e5cbe227b34b245eeb95f4ce6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:56:12 +0300 Subject: [PATCH 155/259] Add ERD diagram to README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ab85df8..bdeaced 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,6 @@ $ npm run test:e2e $ npm run test:cov ``` +## ERD Diagram + +![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884) \ No newline at end of file From e078178f8087391285ead35360239e0a69b9371a Mon Sep 17 00:00:00 2001 From: Ammar Qaffaf Date: Wed, 24 Apr 2024 13:50:33 +0300 Subject: [PATCH 156/259] add architecture diagram --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bdeaced..67eb623 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,51 @@ $ npm run test:cov ## ERD Diagram -![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884) \ No newline at end of file +![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884) + + +## Architecture + +----------------------------------+ + | | + | Applications | + | | + +-----------------------------+-------------+--------------------+ + | | | + | | API Calls | + | | | + | v | + | +---------------------+------------------------+ | + | | | | | + | | Dev Slot | Staging Slot | | + | | | | | + | +---------------------+------------------------+ | + | | | | + | | | | + | | | | + | +------------------+ +---------------------+ | + | | Dev Database | | Staging Database | | + | +------------------+ +---------------------+ | + | | + | +-----------------------------------------+ | + | | | | + | | Production | | + | | | | + | +-----------------------------------------+ | + | | | | + | | | | + | | | | + | +------------------+ | | + | | Production DB | | | + | | Highly Available | | | + | | Cluster | | | + | +------------------+----------------+ | + | | Production DB | | | + | | Standby Node | | | + | +------------------+ | | + | | Production DB | | | + | | Standby Node | | | + | +------------------+ | | + | | Production DB | | | + | | Standby Node | | | + | +------------------+----------------+ | + +-----------------------------------------------------------------+ From 4060beb9384c12fdc6c7e265d073ab154eb7af13 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:41:14 +0300 Subject: [PATCH 157/259] Refactor import statements and fix formatting issues --- src/device/device.module.ts | 7 +++++-- src/device/interfaces/get.device.interface.ts | 2 +- src/device/services/device.service.ts | 10 +++++----- .../controllers/user-device-permission.controller.ts | 1 - 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 3a829fc..5421c0d 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -5,7 +5,10 @@ import { ConfigModule } from '@nestjs/config'; import { ProductRepositoryModule } from '@app/common/modules/product/product.repository.module'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceRepositoryModule } from '@app/common/modules/device'; -import { DeviceRepository, DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { + DeviceRepository, + DeviceUserTypeRepository, +} from '@app/common/modules/device/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @@ -19,7 +22,7 @@ import { GroupDeviceRepository } from '@app/common/modules/group-device/reposito PermissionTypeRepository, SpaceRepository, DeviceRepository, - GroupDeviceRepository + GroupDeviceRepository, ], exports: [DeviceService], }) diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index d513c17..1550ad9 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -8,7 +8,7 @@ export interface GetDeviceDetailsInterface { export interface GetDevicesByRoomIdInterface { success: boolean; msg: string; - result:any; + result: any; } export interface GetDevicesByGroupIdInterface { diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index dfa2e91..e1ebb0b 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -87,7 +87,7 @@ export class DeviceService { deviceTuyaUuid: getDeviceByRoomIdDto.roomId, }, }); - const response:any = await this.tuya.request({ + const response: any = await this.tuya.request({ method: 'GET', path, query: { @@ -106,14 +106,14 @@ export class DeviceService { (item2) => item1.deviceTuyaUuid === item2.uuid, ); if (matchingItem) { - matchingRecords.push({...matchingItem }); + matchingRecords.push({ ...matchingItem }); } }); return { - success:true, - msg:'Device Tuya Details Fetched successfully', - result:matchingRecords + success: true, + msg: 'Device Tuya Details Fetched successfully', + result: matchingRecords, }; } catch (error) { throw new HttpException( diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index 91d944c..f5e253f 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -83,5 +83,4 @@ export class UserDevicePermissionController { throw new Error(err); } } - } From 165dfb575db1b7acf24f444fdee1bdb9cd54e121 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:42:22 +0300 Subject: [PATCH 158/259] Refactor import statements in device controller --- src/device/controllers/device.controller.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 387e765..a34c798 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -1,15 +1,6 @@ import { DeviceService } from '../services/device.service'; -import { - Body, - Controller, - Get, - Post, - UseGuards, - Query, - Param, -} from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, Param } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddDeviceInGroupDto, AddDeviceInRoomDto, From 8c790b0d9628bd1e681a08bbf2965cab97959afb Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:27:40 +0300 Subject: [PATCH 159/259] Refactor Device Module --- .../modules/device/entities/device.entity.ts | 24 +- .../product/entities/product.entity.ts | 8 +- .../modules/space/entities/space.entity.ts | 7 + src/device/controllers/device.controller.ts | 159 ++++--- src/device/device.module.ts | 10 +- src/device/dtos/add.device.dto.ts | 30 +- src/device/dtos/control.device.dto.ts | 4 +- src/device/dtos/get.device.dto.ts | 39 +- src/device/interfaces/get.device.interface.ts | 65 ++- src/device/services/device.service.ts | 448 ++++++------------ src/group/services/group.service.ts | 26 +- src/guards/device.product.guard.ts | 4 +- src/guards/group.guard.ts | 81 ++++ src/guards/room.guard.ts | 100 ++++ 14 files changed, 527 insertions(+), 478 deletions(-) create mode 100644 src/guards/group.guard.ts create mode 100644 src/guards/room.guard.ts diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 2c1837a..df27e44 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,26 +1,19 @@ -import { Column, Entity, OneToMany } from 'typeorm'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; import { DeviceUserPermissionEntity } from './device-user-type.entity'; import { GroupDeviceEntity } from '../../group-device/entities'; +import { SpaceEntity } from '../../space/entities'; +import { ProductEntity } from '../../product/entities'; @Entity({ name: 'device' }) +@Unique(['spaceDevice', 'deviceTuyaUuid']) export class DeviceEntity extends AbstractEntity { - @Column({ - nullable: false, - }) - public spaceUuid: string; - @Column({ nullable: false, }) deviceTuyaUuid: string; - @Column({ - nullable: false, - }) - public productUuid: string; - @Column({ nullable: true, default: true, @@ -44,6 +37,15 @@ export class DeviceEntity extends AbstractEntity { ) userGroupDevices: GroupDeviceEntity[]; + @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { + nullable: false, + }) + spaceDevice: SpaceEntity; + + @ManyToOne(() => ProductEntity, (product) => product.devicesProductEntity, { + nullable: false, + }) + productDevice: ProductEntity; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 5f04d66..6553dc1 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { ProductDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -20,6 +21,11 @@ export class ProductEntity extends AbstractEntity { }) public prodType: string; + @OneToMany( + () => DeviceEntity, + (devicesProductEntity) => devicesProductEntity.productDevice, + ) + devicesProductEntity: DeviceEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index af6922e..56f7010 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -3,6 +3,7 @@ import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceTypeEntity } from '../../space-type/entities'; import { UserSpaceEntity } from '../../user-space/entities'; +import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -30,6 +31,12 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.space) userSpaces: UserSpaceEntity[]; + @OneToMany( + () => DeviceEntity, + (devicesSpaceEntity) => devicesSpaceEntity.spaceDevice, + ) + devicesSpaceEntity: DeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index a34c798..a3bbadd 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -1,5 +1,15 @@ import { DeviceService } from '../services/device.service'; -import { Body, Controller, Get, Post, Query, Param } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Query, + Param, + HttpException, + HttpStatus, + UseGuards, +} from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { AddDeviceInGroupDto, @@ -7,11 +17,12 @@ import { } from '../dtos/add.device.dto'; import { GetDeviceByGroupIdDto, - GetDeviceByRoomIdDto, + GetDeviceByRoomUuidDto, } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; -import { AuthGuardWithRoles } from 'src/guards/device.permission.guard'; -import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { CheckRoomGuard } from 'src/guards/room.guard'; +import { CheckGroupGuard } from 'src/guards/group.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Device Module') @Controller({ @@ -22,19 +33,38 @@ export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.READ) + @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') async getDevicesByRoomId( - @Query() getDeviceByRoomIdDto: GetDeviceByRoomIdDto, + @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, ) { try { - return await this.deviceService.getDevicesByRoomId(getDeviceByRoomIdDto); - } catch (err) { - throw new Error(err); + return await this.deviceService.getDevicesByRoomId( + getDeviceByRoomUuidDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckRoomGuard) + @Post('room') + async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { + try { + return await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.READ) + @UseGuards(JwtAuthGuard, CheckGroupGuard) @Get('group') async getDevicesByGroupId( @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, @@ -43,68 +73,81 @@ export class DeviceController { return await this.deviceService.getDevicesByGroupId( getDeviceByGroupIdDto, ); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.READ) - @Get(':deviceId') - async getDeviceDetailsByDeviceId(@Param('deviceId') deviceId: string) { - try { - return await this.deviceService.getDeviceDetailsByDeviceId(deviceId); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.READ) - @Get(':deviceId/functions') - async getDeviceInstructionByDeviceId(@Param('deviceId') deviceId: string) { - try { - return await this.deviceService.getDeviceInstructionByDeviceId(deviceId); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.READ) - @Get(':deviceId/functions/status') - async getDevicesInstructionStatus(@Param('deviceId') deviceId: string) { - try { - return await this.deviceService.getDevicesInstructionStatus(deviceId); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.CONTROLLABLE) - @Post('room') - async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { - try { - return await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.CONTROLLABLE) + @UseGuards(JwtAuthGuard, CheckGroupGuard) @Post('group') async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { try { return await this.deviceService.addDeviceInGroup(addDeviceInGroupDto); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() - @AuthGuardWithRoles(PermissionType.CONTROLLABLE) + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid') + async getDeviceDetailsByDeviceId(@Param('deviceUuid') deviceUuid: string) { + try { + return await this.deviceService.getDeviceDetailsByDeviceId(deviceUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid/functions') + async getDeviceInstructionByDeviceId( + @Param('deviceUuid') deviceUuid: string, + ) { + try { + return await this.deviceService.getDeviceInstructionByDeviceId( + deviceUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid/functions/status') + async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) { + try { + return await this.deviceService.getDevicesInstructionStatus(deviceUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post('control') async controlDevice(@Body() controlDeviceDto: ControlDeviceDto) { try { return await this.deviceService.controlDevice(controlDeviceDto); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 5421c0d..a9fcdc4 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -12,8 +12,15 @@ import { import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { GroupRepository } from '@app/common/modules/group/repositories'; +import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; @Module({ - imports: [ConfigModule, ProductRepositoryModule, DeviceRepositoryModule], + imports: [ + ConfigModule, + ProductRepositoryModule, + DeviceRepositoryModule, + GroupRepositoryModule, + ], controllers: [DeviceController], providers: [ DeviceService, @@ -23,6 +30,7 @@ import { GroupDeviceRepository } from '@app/common/modules/group-device/reposito SpaceRepository, DeviceRepository, GroupDeviceRepository, + GroupRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index 8b1dfe5..4adc470 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,45 +1,37 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class AddDeviceInRoomDto { @ApiProperty({ - description: 'deviceId', + description: 'deviceTuyaUuid', required: true, }) @IsString() @IsNotEmpty() - public deviceId: string; + public deviceTuyaUuid: string; @ApiProperty({ - description: 'roomId', + description: 'roomUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public roomId: string; + public roomUuid: string; } export class AddDeviceInGroupDto { @ApiProperty({ - description: 'deviceId', + description: 'deviceUuid', required: true, }) @IsString() @IsNotEmpty() - public deviceId: string; + public deviceUuid: string; @ApiProperty({ - description: 'homeId', + description: 'groupUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public homeId: string; - - @ApiProperty({ - description: 'groupId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public groupId: string; + public groupUuid: string; } diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index 660cdaf..1382dc0 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -3,12 +3,12 @@ import { IsNotEmpty, IsString } from 'class-validator'; export class ControlDeviceDto { @ApiProperty({ - description: 'deviceId', + description: 'deviceUuid', required: true, }) @IsString() @IsNotEmpty() - public deviceId: string; + public deviceUuid: string; @ApiProperty({ description: 'code', diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index d49a714..5b5200e 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,44 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; -export class GetDeviceByRoomIdDto { +export class GetDeviceByRoomUuidDto { @ApiProperty({ - description: 'roomId', + description: 'roomUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public roomId: string; - - @ApiProperty({ - description: 'pageSize', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageSize: number; + public roomUuid: string; } export class GetDeviceByGroupIdDto { @ApiProperty({ - description: 'groupId', + description: 'groupUuid', required: true, }) - @IsNumberString() + @IsString() @IsNotEmpty() - public groupId: string; - - @ApiProperty({ - description: 'pageSize', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageSize: number; - @ApiProperty({ - description: 'pageNo', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageNo: number; + public groupUuid: string; } diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index 1550ad9..f7012f7 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -1,23 +1,28 @@ export interface GetDeviceDetailsInterface { - result: { - productId: string; - }; - success: boolean; - msg: string; -} -export interface GetDevicesByRoomIdInterface { - success: boolean; - msg: string; - result: any; -} - -export interface GetDevicesByGroupIdInterface { - success: boolean; - msg: string; - result: { - count: number; - data_list: []; - }; + activeTime: number; + assetId: string; + category: string; + categoryName: string; + createTime: number; + gatewayId: string; + icon: string; + id: string; + ip: string; + lat: string; + localKey: string; + lon: string; + model: string; + name: string; + nodeId: string; + online: boolean; + productId?: string; + productName?: string; + sub: boolean; + timeZone: string; + updateTime: number; + uuid: string; + productType: string; + productUuid: string; } export interface addDeviceInRoomInterface { @@ -44,21 +49,13 @@ export interface GetDeviceDetailsFunctionsStatusInterface { success: boolean; msg: string; } -export interface GetProductInterface { - productType: string; - productId: string; -} export interface DeviceInstructionResponse { - success: boolean; - result: { - productId: string; - productType: string; - functions: { - code: string; - values: any[]; - dataType: string; - }[]; - }; - msg: string; + productUuid: string; + productType: string; + functions: { + code: string; + values: any[]; + dataType: string; + }[]; } diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index e1ebb0b..ca5fbba 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -15,15 +15,11 @@ import { GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, - GetDevicesByGroupIdInterface, - GetDevicesByRoomIdInterface, - GetProductInterface, - addDeviceInRoomInterface, controlDeviceInterface, } from '../interfaces/get.device.interface'; import { GetDeviceByGroupIdDto, - GetDeviceByRoomIdDto, + GetDeviceByRoomUuidDto, } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; @@ -51,25 +47,31 @@ export class DeviceService { }); } - async getDevicesByRoomId(getDeviceByRoomIdDto: GetDeviceByRoomIdDto) { + async getDevicesByRoomId( + getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, + ): Promise { try { - const findRoom = await this.spaceRepository.findOne({ + const devices = await this.deviceRepository.find({ where: { - uuid: getDeviceByRoomIdDto.roomId, + spaceDevice: { uuid: getDeviceByRoomUuidDto.roomUuid }, }, + relations: ['spaceDevice', 'productDevice'], }); - if (!findRoom) { - throw new NotFoundException('Room Details Not Found'); - } - - const response = await this.getDevicesByRoomIdTuya(getDeviceByRoomIdDto); - - return { - success: response.success, - devices: response.result, - msg: response.msg, - }; + const devicesData = await Promise.all( + devices.map(async (device) => { + return { + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + } as GetDeviceDetailsInterface; + }), + ); + return devicesData; } catch (error) { + // Handle the error here throw new HttpException( 'Error fetching devices by room', HttpStatus.INTERNAL_SERVER_ERROR, @@ -77,68 +79,26 @@ export class DeviceService { } } - async getDevicesByRoomIdTuya( - getDeviceByRoomIdDto: GetDeviceByRoomIdDto, - ): Promise { - try { - const path = `/v2.0/cloud/thing/space/device`; - const getDeviceByRoomId = await this.deviceRepository.find({ - where: { - deviceTuyaUuid: getDeviceByRoomIdDto.roomId, - }, - }); - const response: any = await this.tuya.request({ - method: 'GET', - path, - query: { - space_ids: getDeviceByRoomIdDto.roomId, - page_size: getDeviceByRoomIdDto.pageSize, - }, - }); - if (!getDeviceByRoomId.length) { - throw new NotFoundException('Devices Not Found'); - } - - const matchingRecords = []; - - getDeviceByRoomId.forEach((item1) => { - const matchingItem = response.find( - (item2) => item1.deviceTuyaUuid === item2.uuid, - ); - if (matchingItem) { - matchingRecords.push({ ...matchingItem }); - } - }); - - return { - success: true, - msg: 'Device Tuya Details Fetched successfully', - result: matchingRecords, - }; - } catch (error) { - throw new HttpException( - 'Error fetching devices by room from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async getDevicesByGroupId(getDeviceByGroupIdDto: GetDeviceByGroupIdDto) { try { - const devicesIds: GetDevicesByGroupIdInterface = - await this.getDevicesByGroupIdTuya(getDeviceByGroupIdDto); - const devicesDetails = await Promise.all( - devicesIds.result.data_list.map(async (device: any) => { - const deviceData = await this.getDeviceDetailsByDeviceId( - device.dev_id, - ); - return deviceData.result; + const groupDevices = await this.groupDeviceRepository.find({ + where: { group: { uuid: getDeviceByGroupIdDto.groupUuid } }, + relations: ['device'], + }); + const devicesData = await Promise.all( + groupDevices.map(async (device) => { + return { + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.device.deviceTuyaUuid, + )), + uuid: device.uuid, + productUuid: device.device.productDevice.uuid, + productType: device.device.productDevice.prodType, + } as GetDeviceDetailsInterface; }), ); - return { - success: devicesIds.success, - devices: devicesDetails, - msg: devicesIds.msg, - }; + + return devicesData; } catch (error) { throw new HttpException( 'Error fetching devices by group', @@ -147,149 +107,81 @@ export class DeviceService { } } - async getDevicesByGroupIdTuya( - getDeviceByGroupIdDto: GetDeviceByGroupIdDto, - ): Promise { - try { - const path = `/v2.0/cloud/thing/group/${getDeviceByGroupIdDto.groupId}/devices`; - const response = await this.tuya.request({ - method: 'GET', - path, - query: { - page_size: getDeviceByGroupIdDto.pageSize, - page_no: getDeviceByGroupIdDto.pageNo, - }, - }); - return response as GetDevicesByGroupIdInterface; - } catch (error) { - throw new HttpException( - 'Error fetching devices by group from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { - const [deviceDetails, roomDetails] = await Promise.all([ - this.deviceRepository.findOne({ - where: { - uuid: addDeviceInRoomDto.deviceId, - }, - }), - this.spaceRepository.findOne({ - where: { - uuid: addDeviceInRoomDto.roomId, - }, - }), - ]); - - if (!roomDetails) { - throw new NotFoundException('Room Details Not Found'); - } - - if (!deviceDetails) { - throw new NotFoundException('Device Details Not Found'); - } - const response = await this.addDeviceInRooms(addDeviceInRoomDto); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async addDeviceInRooms( - addDeviceInRoomDto: AddDeviceInRoomDto, - ): Promise { try { - const path = `/v2.0/cloud/thing/${addDeviceInRoomDto.deviceId}/transfer`; - const response = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: addDeviceInRoomDto.roomId, - }, - }); - return response as addDeviceInRoomInterface; - } catch (error) { - throw new HttpException( - 'Error adding device in room from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, + const device = await this.getDeviceDetailsByDeviceIdTuya( + addDeviceInRoomDto.deviceTuyaUuid, ); + + if (!device.productUuid) { + throw new Error('Product UUID is missing for the device.'); + } + + await this.deviceRepository.save({ + deviceTuyaUuid: addDeviceInRoomDto.deviceTuyaUuid, + spaceDevice: { uuid: addDeviceInRoomDto.roomUuid }, + productDevice: { uuid: device.productUuid }, + }); + return { message: 'device added in room successfully' }; + } catch (error) { + if (error.code === '23505') { + throw new Error('Device already exists in the room.'); + } else { + throw new Error('Failed to add device in room'); + } } } + async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { - const deviceDetails = this.deviceRepository.findOne({ - where: { - uuid: addDeviceInGroupDto.deviceId, - }, - }); - - if (!deviceDetails) { - throw new NotFoundException('Device Details Not Found'); - } - - const response = await this.addDeviceInGroups(addDeviceInGroupDto); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async addDeviceInGroups( - addDeviceInGroupDto: AddDeviceInGroupDto, - ): Promise { try { - await this.groupDeviceRepository.create({ - deviceUuid: addDeviceInGroupDto.deviceId, - groupUuid: addDeviceInGroupDto.groupId, + await this.groupDeviceRepository.save({ + device: { uuid: addDeviceInGroupDto.deviceUuid }, + group: { uuid: addDeviceInGroupDto.groupUuid }, }); - - return { - success: true, - msg: 'Group is Added to Specific Device', - result: true, - } as addDeviceInRoomInterface; + return { message: 'device added in group successfully' }; } catch (error) { - throw new HttpException( - 'Error adding device in group from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (error.code === '23505') { + throw new Error('Device already exists in the group.'); + } else { + throw new Error('Failed to add device in group'); + } } } async controlDevice(controlDeviceDto: ControlDeviceDto) { - const response = await this.controlDeviceTuya(controlDeviceDto); + try { + const deviceDetails = await this.deviceRepository.findOne({ + where: { + uuid: controlDeviceDto.deviceUuid, + }, + }); - if (response.success) { - return response; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new NotFoundException('Device Not Found'); + } + const response = await this.controlDeviceTuya( + deviceDetails.deviceTuyaUuid, + controlDeviceDto, ); + + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async controlDeviceTuya( + deviceUuid: string, controlDeviceDto: ControlDeviceDto, ): Promise { try { - const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceId}/commands`; + const path = `/v1.0/iot-03/devices/${deviceUuid}/commands`; const response = await this.tuya.request({ method: 'POST', path, @@ -309,29 +201,30 @@ export class DeviceService { } } - async getDeviceDetailsByDeviceId(deviceId: string) { + async getDeviceDetailsByDeviceId(deviceUuid: string) { try { const deviceDetails = await this.deviceRepository.findOne({ where: { - uuid: deviceId, + uuid: deviceUuid, }, }); + if (!deviceDetails) { - throw new NotFoundException('Device Details Not Found'); + throw new NotFoundException('Device Not Found'); } - const response = await this.getDeviceDetailsByDeviceIdTuya(deviceId); + const response = await this.getDeviceDetailsByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); return { - success: response.success, - result: response.result, - msg: response.msg, + ...response, + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, }; } catch (error) { - throw new HttpException( - 'Error fetching device details', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async getDeviceDetailsByDeviceIdTuya( @@ -343,19 +236,14 @@ export class DeviceService { method: 'GET', path, }); + // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); - const productType: string = await this.getProductTypeByProductId( - camelCaseResponse.result.productId, - ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { productName, productId, ...rest } = camelCaseResponse.result; return { - result: { - ...camelCaseResponse.result, - productType: productType, - }, - success: camelCaseResponse.success, - msg: camelCaseResponse.msg, + ...rest, } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( @@ -364,93 +252,39 @@ export class DeviceService { ); } } - async getProductIdByDeviceId(deviceId: string) { - try { - const deviceDetails: GetDeviceDetailsInterface = - await this.getDeviceDetailsByDeviceId(deviceId); - - return deviceDetails.result.productId; - } catch (error) { - throw new HttpException( - 'Error fetching product id by device id', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getProductByProductId(productId: string): Promise { - try { - const product = await this.productRepository - .createQueryBuilder('product') - .where('product.prodId = :productId', { productId }) - .select(['product.prodId', 'product.prodType']) - .getOne(); - - if (product) { - return { - productType: product.prodType, - productId: product.prodId, - }; - } else { - throw new HttpException('Product not found', HttpStatus.NOT_FOUND); - } - } catch (error) { - throw new HttpException( - 'Error fetching product by product id from db', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getProductTypeByProductId(productId: string) { - try { - const product = await this.getProductByProductId(productId); - return product.productType; - } catch (error) { - throw new HttpException( - 'Error getting product type by product id', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async getDeviceInstructionByDeviceId( - deviceId: string, + deviceUuid: string, ): Promise { try { const deviceDetails = await this.deviceRepository.findOne({ where: { - uuid: deviceId, + uuid: deviceUuid, }, + relations: ['productDevice'], }); + if (!deviceDetails) { - throw new NotFoundException('Device Details Not Found'); + throw new NotFoundException('Device Not Found'); } - const response = await this.getDeviceInstructionByDeviceIdTuya(deviceId); - - const productId: string = await this.getProductIdByDeviceId(deviceId); - const productType: string = - await this.getProductTypeByProductId(productId); + const response = await this.getDeviceInstructionByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); return { - success: response.success, - result: { - productId: productId, - productType: productType, - functions: response.result.functions.map((fun: any) => { - return { - code: fun.code, - values: fun.values, - dataType: fun.type, - }; - }), - }, - msg: response.msg, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + functions: response.result.functions.map((fun: any) => { + return { + code: fun.code, + values: fun.values, + dataType: fun.type, + }; + }), }; } catch (error) { - throw new HttpException( - 'Error fetching device functions by device id', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } @@ -471,28 +305,26 @@ export class DeviceService { ); } } - async getDevicesInstructionStatus(deviceId: string) { + async getDevicesInstructionStatus(deviceUuid: string) { try { const deviceDetails = await this.deviceRepository.findOne({ where: { - uuid: deviceId, + uuid: deviceUuid, }, + relations: ['productDevice'], }); + if (!deviceDetails) { - throw new NotFoundException('Device Details Not Found'); + throw new NotFoundException('Device Not Found'); } - const deviceStatus = await this.getDevicesInstructionStatusTuya(deviceId); - const productId: string = await this.getProductIdByDeviceId(deviceId); - const productType: string = - await this.getProductTypeByProductId(productId); + const deviceStatus = await this.getDevicesInstructionStatusTuya( + deviceDetails.deviceTuyaUuid, + ); + return { - result: { - productId: productId, - productType: productType, - status: deviceStatus.result[0].status, - }, - success: deviceStatus.success, - msg: deviceStatus.msg, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + status: deviceStatus.result[0].status, }; } catch (error) { throw new HttpException( @@ -503,7 +335,7 @@ export class DeviceService { } async getDevicesInstructionStatusTuya( - deviceId: string, + deviceUuid: string, ): Promise { try { const path = `/v1.0/iot-03/devices/status`; @@ -511,10 +343,10 @@ export class DeviceService { method: 'GET', path, query: { - device_ids: deviceId, + device_ids: deviceUuid, }, }); - return response as unknown as GetDeviceDetailsFunctionsStatusInterface; + return response as GetDeviceDetailsFunctionsStatusInterface; } catch (error) { throw new HttpException( 'Error fetching device functions status from Tuya', diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 63e63e9..24fffeb 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -43,7 +43,7 @@ export class GroupService { const groupDevices = await this.groupDeviceRepository.find({ relations: ['group', 'device'], where: { - device: { spaceUuid }, + device: { spaceDevice: { uuid: spaceUuid } }, isActive: true, }, }); @@ -129,22 +129,26 @@ export class GroupService { } } async controlDevice(controlDeviceDto: ControlDeviceDto) { - const response = await this.controlDeviceTuya(controlDeviceDto); + try { + const response = await this.controlDeviceTuya(controlDeviceDto); - if (response.success) { - return response; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async controlDeviceTuya( controlDeviceDto: ControlDeviceDto, ): Promise { try { - const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceId}/commands`; + const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceUuid}/commands`; const response = await this.tuya.request({ method: 'POST', path, @@ -170,7 +174,7 @@ export class GroupService { await Promise.all( devices.map(async (device) => { return this.controlDevice({ - deviceId: device.device.deviceTuyaUuid, + deviceUuid: device.device.deviceTuyaUuid, code: controlGroupDto.code, value: controlGroupDto.value, }); diff --git a/src/guards/device.product.guard.ts b/src/guards/device.product.guard.ts index 2118401..e1b4c62 100644 --- a/src/guards/device.product.guard.ts +++ b/src/guards/device.product.guard.ts @@ -36,7 +36,7 @@ export class CheckProductUuidForAllDevicesGuard implements CanActivate { throw new BadRequestException('First device not found'); } - const firstProductUuid = firstDevice.productUuid; + const firstProductUuid = firstDevice.productDevice.uuid; for (let i = 1; i < deviceUuids.length; i++) { const device = await this.deviceRepository.findOne({ @@ -47,7 +47,7 @@ export class CheckProductUuidForAllDevicesGuard implements CanActivate { throw new BadRequestException(`Device ${deviceUuids[i]} not found`); } - if (device.productUuid !== firstProductUuid) { + if (device.productDevice.uuid !== firstProductUuid) { throw new BadRequestException(`Devices have different product UUIDs`); } } diff --git a/src/guards/group.guard.ts b/src/guards/group.guard.ts new file mode 100644 index 0000000..1fe70e4 --- /dev/null +++ b/src/guards/group.guard.ts @@ -0,0 +1,81 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { GroupRepository } from '@app/common/modules/group/repositories'; + +@Injectable() +export class CheckGroupGuard implements CanActivate { + constructor( + private readonly groupRepository: GroupRepository, + private readonly deviceRepository: DeviceRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + if (req.query && req.query.groupUuid) { + const { groupUuid } = req.query; + await this.checkGroupIsFound(groupUuid); + } else if (req.body && req.body.groupUuid && req.body.deviceUuid) { + const { groupUuid, deviceUuid } = req.body; + await this.checkGroupIsFound(groupUuid); + await this.checkDeviceIsFound(deviceUuid); + } else { + throw new BadRequestException('Invalid request parameters'); + } + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkGroupIsFound(groupUuid: string) { + const group = await this.groupRepository.findOne({ + where: { + uuid: groupUuid, + }, + }); + + if (!group) { + throw new NotFoundException('Group not found'); + } + } + async checkDeviceIsFound(deviceUuid: string) { + const device = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + }); + + if (!device) { + throw new NotFoundException('Device not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'Invalid UUID', + }); + } + } +} diff --git a/src/guards/room.guard.ts b/src/guards/room.guard.ts new file mode 100644 index 0000000..71e54c4 --- /dev/null +++ b/src/guards/room.guard.ts @@ -0,0 +1,100 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; + +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class CheckRoomGuard implements CanActivate { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly spaceRepository: SpaceRepository, + private readonly deviceRepository: DeviceRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + console.log(req.body); + + if (req.query && req.query.roomUuid) { + const { roomUuid } = req.query; + await this.checkRoomIsFound(roomUuid); + } else if (req.body && req.body.roomUuid && req.body.deviceTuyaUuid) { + const { roomUuid, deviceTuyaUuid } = req.body; + await this.checkRoomIsFound(roomUuid); + await this.checkDeviceIsFoundFromTuya(deviceTuyaUuid); + } else { + throw new BadRequestException('Invalid request parameters'); + } + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkRoomIsFound(roomUuid: string) { + const room = await this.spaceRepository.findOne({ + where: { + uuid: roomUuid, + spaceType: { + type: 'room', + }, + }, + }); + if (!room) { + throw new NotFoundException('Room not found'); + } + } + async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { + console.log('deviceTuyaUuid: ', deviceTuyaUuid); + + const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + console.log('checkDeviceIsFoundFromTuya: ', response); + + if (!response.success) { + throw new NotFoundException('Device not found'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: 'Invalid UUID', + }); + } + } +} From bdcc065d7318aa005f7f1e81014aa85d3bbe2d91 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:31:00 +0300 Subject: [PATCH 160/259] Remove Home Module --- libs/common/src/database/database.module.ts | 2 - libs/common/src/modules/home/dtos/home.dto.ts | 19 --- libs/common/src/modules/home/dtos/index.ts | 1 - .../src/modules/home/entities/home.entity.ts | 34 ------ .../common/src/modules/home/entities/index.ts | 1 - .../modules/home/home.repository.module.ts | 11 -- .../home/repositories/home.repository.ts | 10 -- .../src/modules/home/repositories/index.ts | 1 - src/app.module.ts | 2 - src/home/controllers/home.controller.ts | 54 --------- src/home/controllers/index.ts | 1 - src/home/dtos/add.home.dto.ts | 20 ---- src/home/dtos/index.ts | 1 - src/home/home.module.ts | 14 --- src/home/interfaces/get.home.interface.ts | 6 - src/home/services/home.service.ts | 113 ------------------ src/home/services/index.ts | 1 - 17 files changed, 291 deletions(-) delete mode 100644 libs/common/src/modules/home/dtos/home.dto.ts delete mode 100644 libs/common/src/modules/home/dtos/index.ts delete mode 100644 libs/common/src/modules/home/entities/home.entity.ts delete mode 100644 libs/common/src/modules/home/entities/index.ts delete mode 100644 libs/common/src/modules/home/home.repository.module.ts delete mode 100644 libs/common/src/modules/home/repositories/home.repository.ts delete mode 100644 libs/common/src/modules/home/repositories/index.ts delete mode 100644 src/home/controllers/home.controller.ts delete mode 100644 src/home/controllers/index.ts delete mode 100644 src/home/dtos/add.home.dto.ts delete mode 100644 src/home/dtos/index.ts delete mode 100644 src/home/home.module.ts delete mode 100644 src/home/interfaces/get.home.interface.ts delete mode 100644 src/home/services/home.service.ts delete mode 100644 src/home/services/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 819be74..911cfb5 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -5,7 +5,6 @@ import { SnakeNamingStrategy } from './strategies'; import { UserEntity } from '../modules/user/entities/user.entity'; import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; -import { HomeEntity } from '../modules/home/entities'; import { ProductEntity } from '../modules/product/entities'; import { DeviceEntity, @@ -35,7 +34,6 @@ import { GroupDeviceEntity } from '../modules/group-device/entities'; UserEntity, UserSessionEntity, UserOtpEntity, - HomeEntity, ProductEntity, DeviceUserPermissionEntity, DeviceEntity, diff --git a/libs/common/src/modules/home/dtos/home.dto.ts b/libs/common/src/modules/home/dtos/home.dto.ts deleted file mode 100644 index eae0630..0000000 --- a/libs/common/src/modules/home/dtos/home.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class HomeDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public userUuid: string; - - @IsString() - @IsNotEmpty() - public homeId: string; - - @IsString() - @IsNotEmpty() - public homeName: string; -} diff --git a/libs/common/src/modules/home/dtos/index.ts b/libs/common/src/modules/home/dtos/index.ts deleted file mode 100644 index 217847e..0000000 --- a/libs/common/src/modules/home/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './home.dto'; diff --git a/libs/common/src/modules/home/entities/home.entity.ts b/libs/common/src/modules/home/entities/home.entity.ts deleted file mode 100644 index ac85641..0000000 --- a/libs/common/src/modules/home/entities/home.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Column, Entity } from 'typeorm'; -import { HomeDto } from '../dtos'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; - -@Entity({ name: 'home' }) -export class HomeEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - userUuid: string; - - @Column({ - nullable: false, - unique: true, - }) - public homeId: string; - - @Column({ - nullable: false, - }) - public homeName: string; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} diff --git a/libs/common/src/modules/home/entities/index.ts b/libs/common/src/modules/home/entities/index.ts deleted file mode 100644 index dbf8038..0000000 --- a/libs/common/src/modules/home/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './home.entity'; diff --git a/libs/common/src/modules/home/home.repository.module.ts b/libs/common/src/modules/home/home.repository.module.ts deleted file mode 100644 index e2527b8..0000000 --- a/libs/common/src/modules/home/home.repository.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { HomeEntity } from './entities/home.entity'; - -@Module({ - providers: [], - exports: [], - controllers: [], - imports: [TypeOrmModule.forFeature([HomeEntity])], -}) -export class HomeRepositoryModule {} diff --git a/libs/common/src/modules/home/repositories/home.repository.ts b/libs/common/src/modules/home/repositories/home.repository.ts deleted file mode 100644 index 3f525e2..0000000 --- a/libs/common/src/modules/home/repositories/home.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { HomeEntity } from '../entities/home.entity'; - -@Injectable() -export class HomeRepository extends Repository { - constructor(private dataSource: DataSource) { - super(HomeEntity, dataSource.createEntityManager()); - } -} diff --git a/libs/common/src/modules/home/repositories/index.ts b/libs/common/src/modules/home/repositories/index.ts deleted file mode 100644 index af66ad7..0000000 --- a/libs/common/src/modules/home/repositories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './home.repository'; diff --git a/src/app.module.ts b/src/app.module.ts index 1a60f18..4243057 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,7 +4,6 @@ import config from './config'; import { AuthenticationModule } from './auth/auth.module'; import { AuthenticationController } from './auth/controllers/authentication.controller'; import { UserModule } from './users/user.module'; -import { HomeModule } from './home/home.module'; import { RoomModule } from './room/room.module'; import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; @@ -25,7 +24,6 @@ import { UnitModule } from './unit/unit.module'; FloorModule, UnitModule, RoomModule, - HomeModule, RoomModule, GroupModule, DeviceModule, diff --git a/src/home/controllers/home.controller.ts b/src/home/controllers/home.controller.ts deleted file mode 100644 index c8aeb4b..0000000 --- a/src/home/controllers/home.controller.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { HomeService } from './../services/home.service'; -import { - Body, - Controller, - Get, - Post, - Param, - UseGuards, - Query, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; -import { AddHomeDto } from '../dtos/add.home.dto'; - -@ApiTags('Home Module') -@Controller({ - version: '1', - path: 'home', -}) -export class HomeController { - constructor(private readonly homeService: HomeService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get() - async getHomesByUserId(@Query('userUuid') userUuid: string) { - try { - return await this.homeService.getHomesByUserId(userUuid); - } catch (err) { - throw new Error(err); - } - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get(':homeId') - async getHomesByHomeId(@Param('homeId') homeId: string) { - try { - return await this.homeService.getHomeByHomeId(homeId); - } catch (err) { - throw new Error(err); - } - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post() - async addHome(@Body() addHomeDto: AddHomeDto) { - try { - return await this.homeService.addHome(addHomeDto); - } catch (err) { - throw new Error(err); - } - } -} diff --git a/src/home/controllers/index.ts b/src/home/controllers/index.ts deleted file mode 100644 index 66a5c99..0000000 --- a/src/home/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './home.controller'; diff --git a/src/home/dtos/add.home.dto.ts b/src/home/dtos/add.home.dto.ts deleted file mode 100644 index f0d5fbe..0000000 --- a/src/home/dtos/add.home.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class AddHomeDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - - @ApiProperty({ - description: 'homeName', - required: true, - }) - @IsString() - @IsNotEmpty() - public homeName: string; -} diff --git a/src/home/dtos/index.ts b/src/home/dtos/index.ts deleted file mode 100644 index 912a7bd..0000000 --- a/src/home/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.home.dto'; diff --git a/src/home/home.module.ts b/src/home/home.module.ts deleted file mode 100644 index 76af90d..0000000 --- a/src/home/home.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HomeService } from './services/home.service'; -import { HomeController } from './controllers/home.controller'; -import { ConfigModule } from '@nestjs/config'; -import { HomeRepositoryModule } from '@app/common/modules/home/home.repository.module'; -import { HomeRepository } from '@app/common/modules/home/repositories'; - -@Module({ - imports: [ConfigModule, HomeRepositoryModule], - controllers: [HomeController], - providers: [HomeService, HomeRepository], - exports: [HomeService], -}) -export class HomeModule {} diff --git a/src/home/interfaces/get.home.interface.ts b/src/home/interfaces/get.home.interface.ts deleted file mode 100644 index c7015f8..0000000 --- a/src/home/interfaces/get.home.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class GetHomeDetailsInterface { - result: { - id: string; - name: string; - }; -} diff --git a/src/home/services/home.service.ts b/src/home/services/home.service.ts deleted file mode 100644 index f1ad1f4..0000000 --- a/src/home/services/home.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { HomeRepository } from './../../../libs/common/src/modules/home/repositories/home.repository'; -import { HomeEntity } from './../../../libs/common/src/modules/home/entities/home.entity'; -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConfigService } from '@nestjs/config'; -import { AddHomeDto } from '../dtos'; -import { GetHomeDetailsInterface } from '../interfaces/get.home.interface'; - -@Injectable() -export class HomeService { - private tuya: TuyaContext; - constructor( - private readonly configService: ConfigService, - private readonly homeRepository: HomeRepository, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); - this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', - accessKey, - secretKey, - }); - } - - async getHomesByUserId(userUuid: string) { - const homesData = await this.findHomes(userUuid); - - const homesMapper = homesData.map((home) => ({ - homeId: home.homeId, - homeName: home.homeName, - })); - - return homesMapper; - } - - async findHomes(userUuid: string) { - try { - return await this.homeRepository.find({ - where: { - userUuid: userUuid, - }, - }); - } catch (error) { - throw new HttpException( - 'Error get homes', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async addHome(addHomeDto: AddHomeDto) { - try { - const path = `/v2.0/cloud/space/creation`; - const data = await this.tuya.request({ - method: 'POST', - path, - body: { name: addHomeDto.homeName }, - }); - if (data.success) { - const homeEntity = { - userUuid: addHomeDto.userUuid, - homeId: data.result, - homeName: addHomeDto.homeName, - } as HomeEntity; - const savedHome = await this.homeRepository.save(homeEntity); - return { - homeId: savedHome.homeId, - homeName: savedHome.homeName, - }; - } - return { - success: data.success, - homeId: data.result, - }; - } catch (error) { - throw new HttpException( - 'Error adding home', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getHomeDetails(homeId: string): Promise { - try { - const path = `/v2.0/cloud/space/${homeId}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - - return response as GetHomeDetailsInterface; - } catch (error) { - throw new HttpException( - 'Error fetching home details', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getHomeByHomeId(homeId: string) { - try { - const response = await this.getHomeDetails(homeId); - - return { - homeId: response.result.id, - homeName: response.result.name, - }; - } catch (error) { - throw new HttpException( - 'Error fetching home', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } -} diff --git a/src/home/services/index.ts b/src/home/services/index.ts deleted file mode 100644 index 23d0070..0000000 --- a/src/home/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './home.service'; From 307761c9155cebec99c7f460273fa66149b80b49 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 28 Apr 2024 09:37:11 +0300 Subject: [PATCH 161/259] Refactor controlDevice method for better readability --- src/group/services/group.service.ts | 27 ++++++++++++++------------- src/guards/device.product.guard.ts | 9 +++++---- src/guards/room.guard.ts | 5 ----- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 24fffeb..9f6c3ac 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -16,7 +16,6 @@ import { RenameGroupDto } from '../dtos/rename.group.dto copy'; import { GroupRepository } from '@app/common/modules/group/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; import { controlDeviceInterface } from 'src/device/interfaces/get.device.interface'; -import { ControlDeviceDto } from 'src/device/dtos'; @Injectable() export class GroupService { @@ -84,6 +83,7 @@ export class GroupService { ); await Promise.all(groupDevicePromises); + return { message: 'Group added successfully' }; } catch (err) { if (err.code === '23505') { throw new HttpException( @@ -128,9 +128,9 @@ export class GroupService { throw error; } } - async controlDevice(controlDeviceDto: ControlDeviceDto) { + async controlDevice(deviceUuid: string, code: string, value: any) { try { - const response = await this.controlDeviceTuya(controlDeviceDto); + const response = await this.controlDeviceTuya(deviceUuid, code, value); if (response.success) { return response; @@ -145,17 +145,17 @@ export class GroupService { } } async controlDeviceTuya( - controlDeviceDto: ControlDeviceDto, + deviceUuid: string, + code: string, + value: any, ): Promise { try { - const path = `/v1.0/iot-03/devices/${controlDeviceDto.deviceUuid}/commands`; + const path = `/v1.0/iot-03/devices/${deviceUuid}/commands`; const response = await this.tuya.request({ method: 'POST', path, body: { - commands: [ - { code: controlDeviceDto.code, value: controlDeviceDto.value }, - ], + commands: [{ code, value: value }], }, }); @@ -173,13 +173,14 @@ export class GroupService { try { await Promise.all( devices.map(async (device) => { - return this.controlDevice({ - deviceUuid: device.device.deviceTuyaUuid, - code: controlGroupDto.code, - value: controlGroupDto.value, - }); + return this.controlDevice( + device.device.deviceTuyaUuid, + controlGroupDto.code, + controlGroupDto.value, + ); }), ); + return { message: 'Group controlled successfully', success: true }; } catch (error) { throw new HttpException( 'Error controlling devices', diff --git a/src/guards/device.product.guard.ts b/src/guards/device.product.guard.ts index e1b4c62..108307a 100644 --- a/src/guards/device.product.guard.ts +++ b/src/guards/device.product.guard.ts @@ -16,7 +16,6 @@ export class CheckProductUuidForAllDevicesGuard implements CanActivate { try { const { deviceUuids } = req.body; - console.log(deviceUuids); await this.checkAllDevicesHaveSameProductUuid(deviceUuids); @@ -30,25 +29,27 @@ export class CheckProductUuidForAllDevicesGuard implements CanActivate { async checkAllDevicesHaveSameProductUuid(deviceUuids: string[]) { const firstDevice = await this.deviceRepository.findOne({ where: { uuid: deviceUuids[0] }, + relations: ['productDevice'], }); if (!firstDevice) { throw new BadRequestException('First device not found'); } - const firstProductUuid = firstDevice.productDevice.uuid; + const firstProductType = firstDevice.productDevice.prodType; for (let i = 1; i < deviceUuids.length; i++) { const device = await this.deviceRepository.findOne({ where: { uuid: deviceUuids[i] }, + relations: ['productDevice'], }); if (!device) { throw new BadRequestException(`Device ${deviceUuids[i]} not found`); } - if (device.productDevice.uuid !== firstProductUuid) { - throw new BadRequestException(`Devices have different product UUIDs`); + if (device.productDevice.prodType !== firstProductType) { + throw new BadRequestException(`Devices have different product types`); } } } diff --git a/src/guards/room.guard.ts b/src/guards/room.guard.ts index 71e54c4..15b34b5 100644 --- a/src/guards/room.guard.ts +++ b/src/guards/room.guard.ts @@ -32,8 +32,6 @@ export class CheckRoomGuard implements CanActivate { const req = context.switchToHttp().getRequest(); try { - console.log(req.body); - if (req.query && req.query.roomUuid) { const { roomUuid } = req.query; await this.checkRoomIsFound(roomUuid); @@ -66,14 +64,11 @@ export class CheckRoomGuard implements CanActivate { } } async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { - console.log('deviceTuyaUuid: ', deviceTuyaUuid); - const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; const response = await this.tuya.request({ method: 'GET', path, }); - console.log('checkDeviceIsFoundFromTuya: ', response); if (!response.success) { throw new NotFoundException('Device not found'); From c376e69e678b6361c7ca682d08b3a81c86c275b6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 28 Apr 2024 10:40:09 +0300 Subject: [PATCH 162/259] Add productUuid to response in getDeviceDetails method --- src/device/services/device.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ca5fbba..ee7a911 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -239,11 +239,18 @@ export class DeviceService { // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); + const deviceDetails = await this.deviceRepository.findOne({ + where: { + deviceTuyaUuid: deviceId, + }, + relations: ['productDevice'], + }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { productName, productId, ...rest } = camelCaseResponse.result; return { ...rest, + productUuid: deviceDetails.productDevice.uuid, } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( From cd2f4966701d323ea9b122796077cfa86ca52ff8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:39:49 +0300 Subject: [PATCH 163/259] Refactor unit service to remove unnecessary includeSubSpaces parameter --- src/unit/dtos/get.unit.dto.ts | 23 +---------------------- src/unit/services/unit.service.ts | 9 ++------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/src/unit/dtos/get.unit.dto.ts b/src/unit/dtos/get.unit.dto.ts index 82837aa..2fae52a 100644 --- a/src/unit/dtos/get.unit.dto.ts +++ b/src/unit/dtos/get.unit.dto.ts @@ -1,13 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsBoolean, - IsInt, - IsNotEmpty, - IsOptional, - IsString, - Min, -} from 'class-validator'; +import { IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; export class GetUnitDto { @ApiProperty({ @@ -35,19 +27,6 @@ export class GetUnitChildDto { @Min(1, { message: 'Page size must not be less than 1' }) @IsNotEmpty() public pageSize: number; - - @ApiProperty({ - example: true, - description: 'Flag to determine whether to fetch full hierarchy', - required: false, - default: false, - }) - @IsOptional() - @IsBoolean() - @Transform((value) => { - return value.obj.includeSubSpaces === 'true'; - }) - public includeSubSpaces: boolean = false; } export class GetUnitByUserIdDto { @ApiProperty({ diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 867300c..1daa297 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -79,7 +79,7 @@ export class UnitService { getUnitChildDto: GetUnitChildDto, ): Promise { try { - const { includeSubSpaces, page, pageSize } = getUnitChildDto; + const { page, pageSize } = getUnitChildDto; const space = await this.spaceRepository.findOneOrFail({ where: { uuid: unitUuid }, @@ -94,12 +94,7 @@ export class UnitService { where: { parent: { uuid: space.uuid } }, }); - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); + const children = await this.buildHierarchy(space, false, page, pageSize); return { uuid: space.uuid, From 2a6cce345d3d6b2233768ad4b08267be866bc158 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:53:06 +0300 Subject: [PATCH 164/259] Refactor error handling in DeviceService --- src/device/services/device.service.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ee7a911..d4c2a2b 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -125,9 +125,15 @@ export class DeviceService { return { message: 'device added in room successfully' }; } catch (error) { if (error.code === '23505') { - throw new Error('Device already exists in the room.'); + throw new HttpException( + 'Device already exists in the room', + HttpStatus.BAD_REQUEST, + ); } else { - throw new Error('Failed to add device in room'); + throw new HttpException( + 'Failed to add device in room', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } @@ -141,9 +147,15 @@ export class DeviceService { return { message: 'device added in group successfully' }; } catch (error) { if (error.code === '23505') { - throw new Error('Device already exists in the group.'); + throw new HttpException( + 'Device already exists in the group', + HttpStatus.BAD_REQUEST, + ); } else { - throw new Error('Failed to add device in group'); + throw new HttpException( + 'Failed to add device in group', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } From 9e40cdb0edc29e1dffb069f335ba656b15f1075d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:18:17 +0300 Subject: [PATCH 165/259] Remove unused imports and dependencies --- src/device/services/device.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index d4c2a2b..112deb5 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -23,8 +23,6 @@ import { } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { ProductRepository } from '@app/common/modules/product/repositories'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @@ -33,8 +31,6 @@ export class DeviceService { private tuya: TuyaContext; constructor( private readonly configService: ConfigService, - private readonly productRepository: ProductRepository, - private readonly spaceRepository: SpaceRepository, private readonly deviceRepository: DeviceRepository, private readonly groupDeviceRepository: GroupDeviceRepository, ) { From 229909f9594fbb9d23b65ae221ebd37057650231 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 1 May 2024 10:21:55 +0300 Subject: [PATCH 166/259] Import ProductRepository in DeviceService --- src/device/services/device.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 112deb5..39bc0ce 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1,3 +1,4 @@ +import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository'; import { Injectable, HttpException, @@ -33,6 +34,7 @@ export class DeviceService { private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, private readonly groupDeviceRepository: GroupDeviceRepository, + private readonly productRepository: ProductRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -247,18 +249,17 @@ export class DeviceService { // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); - const deviceDetails = await this.deviceRepository.findOne({ + const product = await this.productRepository.findOne({ where: { - deviceTuyaUuid: deviceId, + prodId: camelCaseResponse.result.productId, }, - relations: ['productDevice'], }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { productName, productId, ...rest } = camelCaseResponse.result; return { ...rest, - productUuid: deviceDetails.productDevice.uuid, + productUuid: product.uuid, } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( From d146cce1bbbdb5d047fa587c94d3bc739838dc4e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 1 May 2024 10:34:07 +0300 Subject: [PATCH 167/259] Refactor controllers to return UUID of added entities --- src/building/controllers/building.controller.ts | 4 ++-- src/building/services/building.service.ts | 3 ++- src/community/controllers/community.controller.ts | 5 +++-- src/community/services/community.service.ts | 3 ++- src/floor/controllers/floor.controller.ts | 4 ++-- src/floor/services/floor.service.ts | 3 ++- src/room/controllers/room.controller.ts | 4 ++-- src/room/services/room.service.ts | 3 ++- src/unit/controllers/unit.controller.ts | 4 ++-- src/unit/services/unit.service.ts | 3 ++- 10 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 8517ee1..dc4dbd7 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -32,8 +32,8 @@ export class BuildingController { @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { - await this.buildingService.addBuilding(addBuildingDto); - return { message: 'Building added successfully' }; + const building = await this.buildingService.addBuilding(addBuildingDto); + return { message: 'Building added successfully', uuid: building.uuid }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts index fcb803b..dfc2089 100644 --- a/src/building/services/building.service.ts +++ b/src/building/services/building.service.ts @@ -38,11 +38,12 @@ export class BuildingService { if (!spaceType) { throw new BadRequestException('Invalid building UUID'); } - await this.spaceRepository.save({ + const building = await this.spaceRepository.save({ spaceName: addBuildingDto.buildingName, parent: { uuid: addBuildingDto.communityUuid }, spaceType: { uuid: spaceType.uuid }, }); + return building; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index f20aa3d..3fd2233 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -34,8 +34,9 @@ export class CommunityController { @Post() async addCommunity(@Body() addCommunityDto: AddCommunityDto) { try { - await this.communityService.addCommunity(addCommunityDto); - return { message: 'Community added successfully' }; + const community = + await this.communityService.addCommunity(addCommunityDto); + return { message: 'Community added successfully', uuid: community.uuid }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index e88290f..66c7037 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -34,10 +34,11 @@ export class CommunityService { }, }); - await this.spaceRepository.save({ + const community = await this.spaceRepository.save({ spaceName: addCommunityDto.communityName, spaceType: { uuid: spaceType.uuid }, }); + return community; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 2033918..a42db7b 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -32,8 +32,8 @@ export class FloorController { @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { - await this.floorService.addFloor(addFloorDto); - return { message: 'Floor added successfully' }; + const floor = await this.floorService.addFloor(addFloorDto); + return { message: 'Floor added successfully', uuid: floor.uuid }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts index 91de93b..af4b26f 100644 --- a/src/floor/services/floor.service.ts +++ b/src/floor/services/floor.service.ts @@ -35,11 +35,12 @@ export class FloorService { }, }); - await this.spaceRepository.save({ + const floor = await this.spaceRepository.save({ spaceName: addFloorDto.floorName, parent: { uuid: addFloorDto.buildingUuid }, spaceType: { uuid: spaceType.uuid }, }); + return floor; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 054bedf..221fb39 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -30,8 +30,8 @@ export class RoomController { @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { - await this.roomService.addRoom(addRoomDto); - return { message: 'Room added successfully' }; + const room = await this.roomService.addRoom(addRoomDto); + return { message: 'Room added successfully', uuid: room.uuid }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts index 4771c25..b9ea30e 100644 --- a/src/room/services/room.service.ts +++ b/src/room/services/room.service.ts @@ -32,11 +32,12 @@ export class RoomService { }, }); - await this.spaceRepository.save({ + const room = await this.spaceRepository.save({ spaceName: addRoomDto.roomName, parent: { uuid: addRoomDto.unitUuid }, spaceType: { uuid: spaceType.uuid }, }); + return room; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index a3e59e9..f0f3775 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -32,8 +32,8 @@ export class UnitController { @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { - await this.unitService.addUnit(addUnitDto); - return { message: 'Unit added successfully' }; + const unit = await this.unitService.addUnit(addUnitDto); + return { message: 'Unit added successfully', uuid: unit.uuid }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 1daa297..11d654f 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -35,11 +35,12 @@ export class UnitService { }, }); - await this.spaceRepository.save({ + const unit = await this.spaceRepository.save({ spaceName: addUnitDto.unitName, parent: { uuid: addUnitDto.floorUuid }, spaceType: { uuid: spaceType.uuid }, }); + return unit; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } From aa7a09391f8b07b06fb79098c867af8ea594f7db Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:38:59 +0300 Subject: [PATCH 168/259] Remove unused DeviceUserType related files and imports --- .../device/device.repository.module.ts | 6 +-- .../device/dtos/device-user-type.dto.ts | 19 --------- libs/common/src/modules/device/dtos/index.ts | 1 - .../entities/device-user-type.entity.ts | 42 ------------------- .../modules/device/entities/device.entity.ts | 2 +- .../src/modules/device/entities/index.ts | 1 - .../device-user-type.repository.ts | 10 ----- .../src/modules/device/repositories/index.ts | 1 - 8 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 libs/common/src/modules/device/dtos/device-user-type.dto.ts delete mode 100644 libs/common/src/modules/device/entities/device-user-type.entity.ts delete mode 100644 libs/common/src/modules/device/repositories/device-user-type.repository.ts diff --git a/libs/common/src/modules/device/device.repository.module.ts b/libs/common/src/modules/device/device.repository.module.ts index b3d35d7..438e268 100644 --- a/libs/common/src/modules/device/device.repository.module.ts +++ b/libs/common/src/modules/device/device.repository.module.ts @@ -1,13 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { DeviceEntity, DeviceUserPermissionEntity } from './entities'; +import { DeviceEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [ - TypeOrmModule.forFeature([DeviceEntity, DeviceUserPermissionEntity]), - ], + imports: [TypeOrmModule.forFeature([DeviceEntity])], }) export class DeviceRepositoryModule {} diff --git a/libs/common/src/modules/device/dtos/device-user-type.dto.ts b/libs/common/src/modules/device/dtos/device-user-type.dto.ts deleted file mode 100644 index 0571356..0000000 --- a/libs/common/src/modules/device/dtos/device-user-type.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class DeviceUserTypeDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public userUuid: string; - - @IsString() - @IsNotEmpty() - public deviceUuid: string; - - @IsString() - @IsNotEmpty() - public permissionTypeUuid: string; -} diff --git a/libs/common/src/modules/device/dtos/index.ts b/libs/common/src/modules/device/dtos/index.ts index 9d647c8..343f2bd 100644 --- a/libs/common/src/modules/device/dtos/index.ts +++ b/libs/common/src/modules/device/dtos/index.ts @@ -1,2 +1 @@ export * from './device.dto'; -export * from './device-user-type.dto'; diff --git a/libs/common/src/modules/device/entities/device-user-type.entity.ts b/libs/common/src/modules/device/entities/device-user-type.entity.ts deleted file mode 100644 index 904b18a..0000000 --- a/libs/common/src/modules/device/entities/device-user-type.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { DeviceUserTypeDto } from '../dtos/device-user-type.dto'; -import { DeviceEntity } from './device.entity'; -import { PermissionTypeEntity } from '../../permission/entities'; - -@Entity({ name: 'device-user-permission' }) -export class DeviceUserPermissionEntity extends AbstractEntity { - @Column({ - nullable: false, - }) - public userUuid: string; - - @Column({ - nullable: false, - }) - deviceUuid: string; - - @Column({ - nullable: false, - }) - public permissionTypeUuid: string; - - @ManyToOne(() => DeviceEntity, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - }) - @JoinColumn({ name: 'device_uuid', referencedColumnName: 'uuid' }) - device: DeviceEntity; - - @ManyToOne(() => PermissionTypeEntity, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - }) - @JoinColumn({ name: 'permission_type_uuid', referencedColumnName: 'uuid' }) - type: PermissionTypeEntity; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index df27e44..a7f5548 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,10 +1,10 @@ import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; -import { DeviceUserPermissionEntity } from './device-user-type.entity'; import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; +import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; @Entity({ name: 'device' }) @Unique(['spaceDevice', 'deviceTuyaUuid']) diff --git a/libs/common/src/modules/device/entities/index.ts b/libs/common/src/modules/device/entities/index.ts index d6cf7b2..64911c7 100644 --- a/libs/common/src/modules/device/entities/index.ts +++ b/libs/common/src/modules/device/entities/index.ts @@ -1,2 +1 @@ export * from './device.entity'; -export * from './device-user-type.entity'; diff --git a/libs/common/src/modules/device/repositories/device-user-type.repository.ts b/libs/common/src/modules/device/repositories/device-user-type.repository.ts deleted file mode 100644 index e3d2176..0000000 --- a/libs/common/src/modules/device/repositories/device-user-type.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { DeviceUserPermissionEntity } from '../entities'; - -@Injectable() -export class DeviceUserTypeRepository extends Repository { - constructor(private dataSource: DataSource) { - super(DeviceUserPermissionEntity, dataSource.createEntityManager()); - } -} diff --git a/libs/common/src/modules/device/repositories/index.ts b/libs/common/src/modules/device/repositories/index.ts index f91e07f..bf59e16 100644 --- a/libs/common/src/modules/device/repositories/index.ts +++ b/libs/common/src/modules/device/repositories/index.ts @@ -1,2 +1 @@ export * from './device.repository'; -export * from './device-user-type.repository'; From 46bc98da550df510d6c30e90c2ba2d65a80bb24f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:39:23 +0300 Subject: [PATCH 169/259] Add device user permission module, DTOs, entities, and repositories --- ...evice.user.permission.repository.module.ts | 10 +++++ .../dtos/device.user.permission.dto.ts | 19 +++++++++ .../device-user-permission/dtos/index.ts | 1 + .../entities/device.user.permission.entity.ts | 42 +++++++++++++++++++ .../device-user-permission/entities/index.ts | 1 + .../device.user.permission.repository.ts | 10 +++++ .../repositories/index.ts | 1 + 7 files changed, 84 insertions(+) create mode 100644 libs/common/src/modules/device-user-permission/device.user.permission.repository.module.ts create mode 100644 libs/common/src/modules/device-user-permission/dtos/device.user.permission.dto.ts create mode 100644 libs/common/src/modules/device-user-permission/dtos/index.ts create mode 100644 libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts create mode 100644 libs/common/src/modules/device-user-permission/entities/index.ts create mode 100644 libs/common/src/modules/device-user-permission/repositories/device.user.permission.repository.ts create mode 100644 libs/common/src/modules/device-user-permission/repositories/index.ts diff --git a/libs/common/src/modules/device-user-permission/device.user.permission.repository.module.ts b/libs/common/src/modules/device-user-permission/device.user.permission.repository.module.ts new file mode 100644 index 0000000..d950fd8 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/device.user.permission.repository.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeviceUserPermissionEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([DeviceUserPermissionEntity])], +}) +export class DeviceUserPermissionRepositoryModule {} diff --git a/libs/common/src/modules/device-user-permission/dtos/device.user.permission.dto.ts b/libs/common/src/modules/device-user-permission/dtos/device.user.permission.dto.ts new file mode 100644 index 0000000..e78ea3c --- /dev/null +++ b/libs/common/src/modules/device-user-permission/dtos/device.user.permission.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceUserPermissionDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; + + @IsString() + @IsNotEmpty() + public permissionTypeUuid: string; +} diff --git a/libs/common/src/modules/device-user-permission/dtos/index.ts b/libs/common/src/modules/device-user-permission/dtos/index.ts new file mode 100644 index 0000000..b95b695 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/dtos/index.ts @@ -0,0 +1 @@ +export * from './device.user.permission.dto'; diff --git a/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts new file mode 100644 index 0000000..fa6e986 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts @@ -0,0 +1,42 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceUserPermissionDto } from '../dtos'; +import { PermissionTypeEntity } from '../../permission/entities'; +import { DeviceEntity } from '../../device/entities'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'device-user-permission' }) +export class DeviceUserPermissionEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public userUuid: string; + + @Column({ + nullable: false, + }) + deviceUuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.permission, { + nullable: false, + }) + device: DeviceEntity; + + @ManyToOne( + () => PermissionTypeEntity, + (permissionType) => permissionType.permission, + { + nullable: false, + }, + ) + permissionType: PermissionTypeEntity; + + @ManyToOne(() => UserEntity, (user) => user.userPermission, { + nullable: false, + }) + user: UserEntity; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/device-user-permission/entities/index.ts b/libs/common/src/modules/device-user-permission/entities/index.ts new file mode 100644 index 0000000..9e95d82 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/entities/index.ts @@ -0,0 +1 @@ +export * from './device.user.permission.entity'; diff --git a/libs/common/src/modules/device-user-permission/repositories/device.user.permission.repository.ts b/libs/common/src/modules/device-user-permission/repositories/device.user.permission.repository.ts new file mode 100644 index 0000000..a1776b3 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/repositories/device.user.permission.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceUserPermissionEntity } from '../entities'; + +@Injectable() +export class DeviceUserPermissionRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceUserPermissionEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device-user-permission/repositories/index.ts b/libs/common/src/modules/device-user-permission/repositories/index.ts new file mode 100644 index 0000000..6957209 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/repositories/index.ts @@ -0,0 +1 @@ +export * from './device.user.permission.repository'; From b8ae3540c3eed045f9cc411f78d0abbebcdc9d9c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:39:38 +0300 Subject: [PATCH 170/259] Update entity references in permission and user entities --- .../src/modules/permission/entities/permission.entity.ts | 4 ++-- libs/common/src/modules/user/entities/user.entity.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/permission/entities/permission.entity.ts b/libs/common/src/modules/permission/entities/permission.entity.ts index f2ba950..3ee3943 100644 --- a/libs/common/src/modules/permission/entities/permission.entity.ts +++ b/libs/common/src/modules/permission/entities/permission.entity.ts @@ -2,7 +2,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { PermissionTypeDto } from '../dtos/permission.dto'; -import { DeviceUserPermissionEntity } from '../../device/entities'; +import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; @Entity({ name: 'permission-type' }) export class PermissionTypeEntity extends AbstractEntity { @@ -14,7 +14,7 @@ export class PermissionTypeEntity extends AbstractEntity { @OneToMany( () => DeviceUserPermissionEntity, - (permission) => permission.type, + (permission) => permission.permissionType, { nullable: true, onDelete: 'CASCADE', diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 0981bcc..4268ae8 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -1,3 +1,4 @@ +import { DeviceUserPermissionEntity } from '../../device-user-permission/entities/device.user.permission.entity'; import { Column, Entity, OneToMany } from 'typeorm'; import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; @@ -51,6 +52,11 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) userSpaces: UserSpaceEntity[]; + @OneToMany( + () => DeviceUserPermissionEntity, + (userPermission) => userPermission.user, + ) + userPermission: DeviceUserPermissionEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); From e48c6b4a912f4648508535aa18edc712e91a394a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:40:06 +0300 Subject: [PATCH 171/259] Update imports in database module --- libs/common/src/database/database.module.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 911cfb5..146b5ba 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -6,16 +6,14 @@ import { UserEntity } from '../modules/user/entities/user.entity'; import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserOtpEntity } from '../modules/user-otp/entities'; import { ProductEntity } from '../modules/product/entities'; -import { - DeviceEntity, - DeviceUserPermissionEntity, -} from '../modules/device/entities'; +import { DeviceEntity } from '../modules/device/entities'; import { PermissionTypeEntity } from '../modules/permission/entities'; import { SpaceEntity } from '../modules/space/entities'; import { SpaceTypeEntity } from '../modules/space-type/entities'; import { UserSpaceEntity } from '../modules/user-space/entities'; import { GroupEntity } from '../modules/group/entities'; import { GroupDeviceEntity } from '../modules/group-device/entities'; +import { DeviceUserPermissionEntity } from '../modules/device-user-permission/entities'; @Module({ imports: [ @@ -43,6 +41,7 @@ import { GroupDeviceEntity } from '../modules/group-device/entities'; UserSpaceEntity, GroupEntity, GroupDeviceEntity, + DeviceUserPermissionEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), From 11c3b8015bccd2e3d5af21d479ebe2028003e9f3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:40:28 +0300 Subject: [PATCH 172/259] Refactor imports and add relations in DeviceService --- src/device/device.module.ts | 8 +++----- src/device/services/device.service.ts | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/device/device.module.ts b/src/device/device.module.ts index a9fcdc4..ba9b019 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -5,15 +5,13 @@ import { ConfigModule } from '@nestjs/config'; import { ProductRepositoryModule } from '@app/common/modules/product/product.repository.module'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceRepositoryModule } from '@app/common/modules/device'; -import { - DeviceRepository, - DeviceUserTypeRepository, -} from '@app/common/modules/device/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; import { GroupRepository } from '@app/common/modules/group/repositories'; import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; @Module({ imports: [ ConfigModule, @@ -25,7 +23,7 @@ import { GroupRepositoryModule } from '@app/common/modules/group/group.repositor providers: [ DeviceService, ProductRepository, - DeviceUserTypeRepository, + DeviceUserPermissionRepository, PermissionTypeRepository, SpaceRepository, DeviceRepository, diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 112deb5..ed61330 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -215,6 +215,7 @@ export class DeviceService { where: { uuid: deviceUuid, }, + relations: ['productDevice', 'permission'], }); if (!deviceDetails) { @@ -224,6 +225,7 @@ export class DeviceService { const response = await this.getDeviceDetailsByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); + console.log('response', deviceDetails); return { ...response, From 5e3ad8a52313c8d43affdc3708e19cc053e2f474 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 18:40:44 +0300 Subject: [PATCH 173/259] Update device permission guard and user device permission service --- src/guards/device.permission.guard.ts | 8 ++++---- .../services/user-device-permission.service.ts | 14 ++++++++------ .../user-device-permission.module.ts | 8 +++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/guards/device.permission.guard.ts b/src/guards/device.permission.guard.ts index 3fc80af..1ec21a9 100644 --- a/src/guards/device.permission.guard.ts +++ b/src/guards/device.permission.guard.ts @@ -1,6 +1,6 @@ import { PermissionType } from '@app/common/constants/permission-type.enum'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { Injectable, @@ -18,7 +18,7 @@ import { Reflector } from '@nestjs/core'; export class DevicePermissionGuard implements CanActivate { constructor( private reflector: Reflector, - private readonly deviceUserTypeRepository: DeviceUserTypeRepository, + private readonly deviceUserPermissionRepository: DeviceUserPermissionRepository, private readonly permissionTypeRepository: PermissionTypeRepository, ) {} @@ -52,7 +52,7 @@ export class DevicePermissionGuard implements CanActivate { requirePermission, ) { const [userPermissionDetails, permissionDetails] = await Promise.all([ - this.deviceUserTypeRepository.findOne({ + this.deviceUserPermissionRepository.findOne({ where: { deviceUuid: deviceId, userUuid: userId }, }), this.permissionTypeRepository.findOne({ @@ -64,7 +64,7 @@ export class DevicePermissionGuard implements CanActivate { if (!userPermissionDetails) { throw new BadRequestException('User Permission Details Not Found'); } - if (userPermissionDetails.permissionTypeUuid !== permissionDetails.uuid) { + if (userPermissionDetails.permissionType.uuid !== permissionDetails.uuid) { throw new BadRequestException( `User Does not have a ${requirePermission}`, ); diff --git a/src/user-device-permission/services/user-device-permission.service.ts b/src/user-device-permission/services/user-device-permission.service.ts index e50d59f..64073a1 100644 --- a/src/user-device-permission/services/user-device-permission.service.ts +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -1,16 +1,16 @@ -import { DeviceUserTypeRepository } from '@app/common/modules/device/repositories'; import { Injectable } from '@nestjs/common'; import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; @Injectable() export class UserDevicePermissionService { constructor( - private readonly deviceUserTypeRepository: DeviceUserTypeRepository, + private readonly deviceUserPermissionRepository: DeviceUserPermissionRepository, ) {} async addUserPermission(userDevicePermissionDto: UserDevicePermissionAddDto) { - return await this.deviceUserTypeRepository.save({ + return await this.deviceUserPermissionRepository.save({ userUuid: userDevicePermissionDto.userId, deviceUuid: userDevicePermissionDto.deviceId, permissionTypeUuid: userDevicePermissionDto.permissionTypeId, @@ -21,16 +21,18 @@ export class UserDevicePermissionService { userId: string, userDevicePermissionEditDto: UserDevicePermissionEditDto, ) { - return await this.deviceUserTypeRepository.update( + return await this.deviceUserPermissionRepository.update( { userUuid: userId }, { deviceUuid: userDevicePermissionEditDto.deviceId, - permissionTypeUuid: userDevicePermissionEditDto.permissionTypeId, + permissionType: { + uuid: userDevicePermissionEditDto.permissionTypeId, + }, }, ); } async fetchuserPermission() { - return await this.deviceUserTypeRepository.find(); + return await this.deviceUserPermissionRepository.find(); } } diff --git a/src/user-device-permission/user-device-permission.module.ts b/src/user-device-permission/user-device-permission.module.ts index edd9991..b7e5dbd 100644 --- a/src/user-device-permission/user-device-permission.module.ts +++ b/src/user-device-permission/user-device-permission.module.ts @@ -1,18 +1,16 @@ import { DeviceRepositoryModule } from '@app/common/modules/device'; -import { - DeviceRepository, - DeviceUserTypeRepository, -} from '@app/common/modules/device/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { UserDevicePermissionService } from './services/user-device-permission.service'; import { UserDevicePermissionController } from './controllers/user-device-permission.controller'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [UserDevicePermissionController], providers: [ - DeviceUserTypeRepository, + DeviceUserPermissionRepository, DeviceRepository, UserDevicePermissionService, ], From 5aa69255be91746391c3ea2f257523f715e682d1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 19:47:50 +0300 Subject: [PATCH 174/259] Add Unique constraint to DeviceUserPermissionEntity --- .../entities/device.user.permission.entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts index fa6e986..4577e29 100644 --- a/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts +++ b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne } from 'typeorm'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceUserPermissionDto } from '../dtos'; import { PermissionTypeEntity } from '../../permission/entities'; @@ -6,6 +6,7 @@ import { DeviceEntity } from '../../device/entities'; import { UserEntity } from '../../user/entities'; @Entity({ name: 'device-user-permission' }) +@Unique(['userUuid', 'deviceUuid', 'permissionType']) export class DeviceUserPermissionEntity extends AbstractEntity { @Column({ nullable: false, From fa6929a4e5308b4aa505c4940ba49ccefe81d7d0 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 19:48:03 +0300 Subject: [PATCH 175/259] Add user device permission CRUD operations --- .../controllers/index.ts | 1 + .../user-device-permission.controller.ts | 54 ++++++--- src/user-device-permission/dtos/index.ts | 2 + .../dtos/user-device-permission.add.dto.ts | 19 ++-- .../dtos/user-device-permission.edit.dto.ts | 18 ++- src/user-device-permission/services/index.ts | 1 + .../user-device-permission.service.ts | 103 +++++++++++++++--- .../user-device-permission.module.ts | 2 + 8 files changed, 155 insertions(+), 45 deletions(-) diff --git a/src/user-device-permission/controllers/index.ts b/src/user-device-permission/controllers/index.ts index e69de29..9ea28cf 100644 --- a/src/user-device-permission/controllers/index.ts +++ b/src/user-device-permission/controllers/index.ts @@ -0,0 +1 @@ +export * from './user-device-permission.controller'; diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index f5e253f..825694f 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -1,7 +1,9 @@ import { Body, Controller, + Delete, Get, + HttpException, HttpStatus, Param, Post, @@ -24,8 +26,8 @@ export class UserDevicePermissionController { private readonly userDevicePermissionService: UserDevicePermissionService, ) {} - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) @Post('add') async addDevicePermission( @Body() userDevicePermissionDto: UserDevicePermissionAddDto, @@ -40,40 +42,45 @@ export class UserDevicePermissionController { message: 'User Permission for Devices Added Successfully', data: addDetails, }; - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put('edit/:userId') + @Put('edit/:devicePermissionUuid') async editDevicePermission( - @Param('userId') userId: string, + @Param('devicePermissionUuid') devicePermissionUuid: string, @Body() userDevicePermissionEditDto: UserDevicePermissionEditDto, ) { try { await this.userDevicePermissionService.editUserPermission( - userId, + devicePermissionUuid, userDevicePermissionEditDto, ); return { statusCode: HttpStatus.OK, message: 'User Permission for Devices Updated Successfully', - data: {}, }; - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('list') - async fetchDevicePermission() { + @Get(':deviceUuid/list') + async fetchDevicePermission(@Param('deviceUuid') deviceUuid: string) { try { const deviceDetails = - await this.userDevicePermissionService.fetchuserPermission(); + await this.userDevicePermissionService.fetchUserPermission(deviceUuid); return { statusCode: HttpStatus.OK, message: 'Device Details fetched Successfully', @@ -83,4 +90,25 @@ export class UserDevicePermissionController { throw new Error(err); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':devicePermissionUuid') + async deleteDevicePermission( + @Param('devicePermissionUuid') devicePermissionUuid: string, + ) { + try { + await this.userDevicePermissionService.deleteDevicePermission( + devicePermissionUuid, + ); + return { + statusCode: HttpStatus.OK, + message: 'User Permission for Devices Deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/user-device-permission/dtos/index.ts b/src/user-device-permission/dtos/index.ts index e69de29..7792d07 100644 --- a/src/user-device-permission/dtos/index.ts +++ b/src/user-device-permission/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './user-device-permission.add.dto'; +export * from './user-device-permission.edit.dto'; diff --git a/src/user-device-permission/dtos/user-device-permission.add.dto.ts b/src/user-device-permission/dtos/user-device-permission.add.dto.ts index c7459df..12ee133 100644 --- a/src/user-device-permission/dtos/user-device-permission.add.dto.ts +++ b/src/user-device-permission/dtos/user-device-permission.add.dto.ts @@ -1,28 +1,29 @@ +import { PermissionType } from '@app/common/constants/permission-type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; export class UserDevicePermissionAddDto { @ApiProperty({ - description: 'user id', + description: 'user uuid', required: true, }) @IsString() @IsNotEmpty() - userId: string; + userUuid: string; @ApiProperty({ - description: 'permission type id', + description: 'permission type', + enum: PermissionType, required: true, }) - @IsString() - @IsNotEmpty() - permissionTypeId: string; + @IsEnum(PermissionType) + permissionType: PermissionType; @ApiProperty({ - description: 'device id', + description: 'device uuid', required: true, }) @IsString() @IsNotEmpty() - deviceId: string; + deviceUuid: string; } diff --git a/src/user-device-permission/dtos/user-device-permission.edit.dto.ts b/src/user-device-permission/dtos/user-device-permission.edit.dto.ts index 7cc4fce..ce537e1 100644 --- a/src/user-device-permission/dtos/user-device-permission.edit.dto.ts +++ b/src/user-device-permission/dtos/user-device-permission.edit.dto.ts @@ -1,7 +1,13 @@ -import { OmitType } from '@nestjs/swagger'; -import { UserDevicePermissionAddDto } from './user-device-permission.add.dto'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; -export class UserDevicePermissionEditDto extends OmitType( - UserDevicePermissionAddDto, - ['userId'], -) {} +export class UserDevicePermissionEditDto { + @ApiProperty({ + description: 'permission type', + enum: PermissionType, + required: true, + }) + @IsEnum(PermissionType) + permissionType: PermissionType; +} diff --git a/src/user-device-permission/services/index.ts b/src/user-device-permission/services/index.ts index e69de29..1dc9e53 100644 --- a/src/user-device-permission/services/index.ts +++ b/src/user-device-permission/services/index.ts @@ -0,0 +1 @@ +export * from './user-device-permission.service'; diff --git a/src/user-device-permission/services/user-device-permission.service.ts b/src/user-device-permission/services/user-device-permission.service.ts index 64073a1..b02cb6a 100644 --- a/src/user-device-permission/services/user-device-permission.service.ts +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -1,38 +1,107 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Injectable() export class UserDevicePermissionService { constructor( private readonly deviceUserPermissionRepository: DeviceUserPermissionRepository, + private readonly permissionTypeRepository: PermissionTypeRepository, ) {} async addUserPermission(userDevicePermissionDto: UserDevicePermissionAddDto) { - return await this.deviceUserPermissionRepository.save({ - userUuid: userDevicePermissionDto.userId, - deviceUuid: userDevicePermissionDto.deviceId, - permissionTypeUuid: userDevicePermissionDto.permissionTypeId, - }); + try { + const permissionType = await this.getPermissionType( + userDevicePermissionDto.permissionType, + ); + return await this.deviceUserPermissionRepository.save({ + userUuid: userDevicePermissionDto.userUuid, + deviceUuid: userDevicePermissionDto.deviceUuid, + permissionType: { + uuid: permissionType.uuid, + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This Permission already belongs to this user', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } async editUserPermission( - userId: string, + devicePermissionUuid: string, userDevicePermissionEditDto: UserDevicePermissionEditDto, ) { - return await this.deviceUserPermissionRepository.update( - { userUuid: userId }, - { - deviceUuid: userDevicePermissionEditDto.deviceId, - permissionType: { - uuid: userDevicePermissionEditDto.permissionTypeId, + try { + const permissionType = await this.getPermissionType( + userDevicePermissionEditDto.permissionType, + ); + return await this.deviceUserPermissionRepository.update( + { uuid: devicePermissionUuid }, + { + permissionType: { + uuid: permissionType.uuid, + }, }, - }, - ); + ); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This Permission already belongs to this user', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } - async fetchuserPermission() { - return await this.deviceUserPermissionRepository.find(); + async fetchUserPermission(deviceUuid: string) { + const devicePermissions = await this.deviceUserPermissionRepository.find({ + where: { + deviceUuid: deviceUuid, + }, + relations: ['permissionType', 'user'], + }); + return devicePermissions.map((permission) => { + return { + uuid: permission.uuid, + deviceUuid: permission.deviceUuid, + firstName: permission.user.firstName, + lastName: permission.user.lastName, + email: permission.user.email, + permissionType: permission.permissionType.type, + }; + }); + } + private async getPermissionType(permissionType: string) { + return await this.permissionTypeRepository.findOne({ + where: { + type: permissionType, + }, + }); + } + async deleteDevicePermission(devicePermissionUuid: string) { + try { + return await this.deviceUserPermissionRepository.delete({ + uuid: devicePermissionUuid, + }); + } catch (error) { + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } diff --git a/src/user-device-permission/user-device-permission.module.ts b/src/user-device-permission/user-device-permission.module.ts index b7e5dbd..e2a8b46 100644 --- a/src/user-device-permission/user-device-permission.module.ts +++ b/src/user-device-permission/user-device-permission.module.ts @@ -5,12 +5,14 @@ import { ConfigModule } from '@nestjs/config'; import { UserDevicePermissionService } from './services/user-device-permission.service'; import { UserDevicePermissionController } from './controllers/user-device-permission.controller'; import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [UserDevicePermissionController], providers: [ DeviceUserPermissionRepository, + PermissionTypeRepository, DeviceRepository, UserDevicePermissionService, ], From fae2fff2bab12d7b0e4652f5aa3789d9851bf679 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 21:32:00 +0300 Subject: [PATCH 176/259] Remove unnecessary field from Unique constraint --- .../entities/device.user.permission.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts index 4577e29..d136df7 100644 --- a/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts +++ b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts @@ -6,7 +6,7 @@ import { DeviceEntity } from '../../device/entities'; import { UserEntity } from '../../user/entities'; @Entity({ name: 'device-user-permission' }) -@Unique(['userUuid', 'deviceUuid', 'permissionType']) +@Unique(['userUuid', 'deviceUuid']) export class DeviceUserPermissionEntity extends AbstractEntity { @Column({ nullable: false, From 6019e92c5d199f5ecffeeb87c95b689125da42bd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 21:32:21 +0300 Subject: [PATCH 177/259] Add user permission guards and update device service methods --- src/device/controllers/device.controller.ts | 40 ++++++++--- src/device/device.module.ts | 2 + src/device/dtos/control.device.dto.ts | 8 --- src/device/services/device.service.ts | 79 +++++++++++++++++---- 4 files changed, 99 insertions(+), 30 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index a3bbadd..d8149a7 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -9,6 +9,7 @@ import { HttpException, HttpStatus, UseGuards, + Req, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { @@ -23,6 +24,8 @@ import { ControlDeviceDto } from '../dtos/control.device.dto'; import { CheckRoomGuard } from 'src/guards/room.guard'; import { CheckGroupGuard } from 'src/guards/group.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; +import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; @ApiTags('Device Module') @Controller({ @@ -37,10 +40,13 @@ export class DeviceController { @Get('room') async getDevicesByRoomId( @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, + @Req() req: any, ) { try { + const userUuid = req.user.uuid; return await this.deviceService.getDevicesByRoomId( getDeviceByRoomUuidDto, + userUuid, ); } catch (error) { throw new HttpException( @@ -68,10 +74,13 @@ export class DeviceController { @Get('group') async getDevicesByGroupId( @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, + @Req() req: any, ) { try { + const userUuid = req.user.uuid; return await this.deviceService.getDevicesByGroupId( getDeviceByGroupIdDto, + userUuid, ); } catch (error) { throw new HttpException( @@ -94,11 +103,18 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid') - async getDeviceDetailsByDeviceId(@Param('deviceUuid') deviceUuid: string) { + async getDeviceDetailsByDeviceId( + @Param('deviceUuid') deviceUuid: string, + @Req() req: any, + ) { try { - return await this.deviceService.getDeviceDetailsByDeviceId(deviceUuid); + const userUuid = req.user.uuid; + return await this.deviceService.getDeviceDetailsByDeviceId( + deviceUuid, + userUuid, + ); } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -107,7 +123,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid/functions') async getDeviceInstructionByDeviceId( @Param('deviceUuid') deviceUuid: string, @@ -124,7 +140,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid/functions/status') async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) { try { @@ -138,11 +154,17 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('control') - async controlDevice(@Body() controlDeviceDto: ControlDeviceDto) { + @UseGuards(JwtAuthGuard, CheckUserHaveControllablePermission) + @Post(':deviceUuid/control') + async controlDevice( + @Body() controlDeviceDto: ControlDeviceDto, + @Param('deviceUuid') deviceUuid: string, + ) { try { - return await this.deviceService.controlDevice(controlDeviceDto); + return await this.deviceService.controlDevice( + controlDeviceDto, + deviceUuid, + ); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/device/device.module.ts b/src/device/device.module.ts index ba9b019..e48861b 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -12,6 +12,7 @@ import { GroupDeviceRepository } from '@app/common/modules/group-device/reposito import { GroupRepository } from '@app/common/modules/group/repositories'; import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ imports: [ ConfigModule, @@ -29,6 +30,7 @@ import { DeviceUserPermissionRepository } from '@app/common/modules/device-user- DeviceRepository, GroupDeviceRepository, GroupRepository, + UserRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index 1382dc0..ab2125d 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -2,14 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class ControlDeviceDto { - @ApiProperty({ - description: 'deviceUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceUuid: string; - @ApiProperty({ description: 'code', required: true, diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ebd373d..29bddca 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -26,6 +26,7 @@ import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; @Injectable() export class DeviceService { @@ -47,13 +48,25 @@ export class DeviceService { async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, + userUuid: string, ): Promise { try { const devices = await this.deviceRepository.find({ where: { spaceDevice: { uuid: getDeviceByRoomUuidDto.roomUuid }, + permission: { + userUuid, + permissionType: { + type: PermissionType.READ || PermissionType.CONTROLLABLE, + }, + }, }, - relations: ['spaceDevice', 'productDevice'], + relations: [ + 'spaceDevice', + 'productDevice', + 'permission', + 'permission.permissionType', + ], }); const devicesData = await Promise.all( devices.map(async (device) => { @@ -64,6 +77,7 @@ export class DeviceService { uuid: device.uuid, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, } as GetDeviceDetailsInterface; }), ); @@ -77,11 +91,29 @@ export class DeviceService { } } - async getDevicesByGroupId(getDeviceByGroupIdDto: GetDeviceByGroupIdDto) { + async getDevicesByGroupId( + getDeviceByGroupIdDto: GetDeviceByGroupIdDto, + userUuid: string, + ) { try { const groupDevices = await this.groupDeviceRepository.find({ - where: { group: { uuid: getDeviceByGroupIdDto.groupUuid } }, - relations: ['device'], + where: { + group: { uuid: getDeviceByGroupIdDto.groupUuid }, + device: { + permission: { + userUuid, + permissionType: { + type: PermissionType.READ || PermissionType.CONTROLLABLE, + }, + }, + }, + }, + relations: [ + 'device', + 'device.productDevice', + 'device.permission', + 'device.permission.permissionType', + ], }); const devicesData = await Promise.all( groupDevices.map(async (device) => { @@ -89,9 +121,10 @@ export class DeviceService { ...(await this.getDeviceDetailsByDeviceIdTuya( device.device.deviceTuyaUuid, )), - uuid: device.uuid, + uuid: device.device.uuid, productUuid: device.device.productDevice.uuid, productType: device.device.productDevice.prodType, + permissionType: device.device.permission[0].permissionType.type, } as GetDeviceDetailsInterface; }), ); @@ -104,7 +137,6 @@ export class DeviceService { ); } } - async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { try { const device = await this.getDeviceDetailsByDeviceIdTuya( @@ -158,11 +190,11 @@ export class DeviceService { } } - async controlDevice(controlDeviceDto: ControlDeviceDto) { + async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { const deviceDetails = await this.deviceRepository.findOne({ where: { - uuid: controlDeviceDto.deviceUuid, + uuid: deviceUuid, }, }); @@ -183,7 +215,10 @@ export class DeviceService { ); } } catch (error) { - throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + throw new HttpException( + error.message || 'Device Not Found', + error.status || HttpStatus.NOT_FOUND, + ); } } async controlDeviceTuya( @@ -211,13 +246,18 @@ export class DeviceService { } } - async getDeviceDetailsByDeviceId(deviceUuid: string) { + async getDeviceDetailsByDeviceId(deviceUuid: string, userUuid: string) { try { + const userDevicePermission = await this.getUserDevicePermission( + userUuid, + deviceUuid, + ); + const deviceDetails = await this.deviceRepository.findOne({ where: { uuid: deviceUuid, }, - relations: ['productDevice', 'permission'], + relations: ['productDevice'], }); if (!deviceDetails) { @@ -227,16 +267,19 @@ export class DeviceService { const response = await this.getDeviceDetailsByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); - console.log('response', deviceDetails); return { ...response, uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, + permissionType: userDevicePermission, }; } catch (error) { - throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + throw new HttpException( + error.message || 'Device Not Found', + HttpStatus.NOT_FOUND, + ); } } async getDeviceDetailsByDeviceIdTuya( @@ -259,6 +302,7 @@ export class DeviceService { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { productName, productId, ...rest } = camelCaseResponse.result; + return { ...rest, productUuid: product.uuid, @@ -372,4 +416,13 @@ export class DeviceService { ); } } + private async getUserDevicePermission(userUuid: string, deviceUuid: string) { + const device = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + relations: ['permission', 'permission.permissionType'], + }); + return device.permission[0].permissionType.type; + } } From 16ed5b33fc8343d76108f57525a7970d7fb7a6cd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 21:32:31 +0300 Subject: [PATCH 178/259] Add user device permission guards --- src/guards/device.permission.guard.ts | 97 ------------------- ...er.device.controllable.permission.guard.ts | 92 ++++++++++++++++++ src/guards/user.device.permission.guard.ts | 91 +++++++++++++++++ 3 files changed, 183 insertions(+), 97 deletions(-) delete mode 100644 src/guards/device.permission.guard.ts create mode 100644 src/guards/user.device.controllable.permission.guard.ts create mode 100644 src/guards/user.device.permission.guard.ts diff --git a/src/guards/device.permission.guard.ts b/src/guards/device.permission.guard.ts deleted file mode 100644 index 1ec21a9..0000000 --- a/src/guards/device.permission.guard.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { PermissionType } from '@app/common/constants/permission-type.enum'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; -import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; -import { - Injectable, - CanActivate, - HttpStatus, - ExecutionContext, - BadRequestException, - applyDecorators, - SetMetadata, - UseGuards, -} from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; - -@Injectable() -export class DevicePermissionGuard implements CanActivate { - constructor( - private reflector: Reflector, - private readonly deviceUserPermissionRepository: DeviceUserPermissionRepository, - private readonly permissionTypeRepository: PermissionTypeRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - try { - const { deviceId } = req.headers; - const userId = req.user.uuid; - - const requirePermission = - this.reflector.getAllAndOverride('permission', [ - context.getHandler(), - context.getClass(), - ]); - - if (!requirePermission) { - return true; - } - await this.checkDevicePermission(deviceId, userId, requirePermission); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - async checkDevicePermission( - deviceId: string, - userId: string, - requirePermission, - ) { - const [userPermissionDetails, permissionDetails] = await Promise.all([ - this.deviceUserPermissionRepository.findOne({ - where: { deviceUuid: deviceId, userUuid: userId }, - }), - this.permissionTypeRepository.findOne({ - where: { - type: requirePermission, - }, - }), - ]); - if (!userPermissionDetails) { - throw new BadRequestException('User Permission Details Not Found'); - } - if (userPermissionDetails.permissionType.uuid !== permissionDetails.uuid) { - throw new BadRequestException( - `User Does not have a ${requirePermission}`, - ); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - console.error(error); - - if (error instanceof BadRequestException) { - response - .status(HttpStatus.BAD_REQUEST) - .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); - } else { - response.status(HttpStatus.NOT_FOUND).json({ - statusCode: HttpStatus.NOT_FOUND, - message: 'User Permission not found', - }); - } - } -} - -export function AuthGuardWithRoles(permission?: string) { - return applyDecorators( - SetMetadata('permission', permission), - UseGuards(JwtAuthGuard), - UseGuards(DevicePermissionGuard), - ); -} diff --git a/src/guards/user.device.controllable.permission.guard.ts b/src/guards/user.device.controllable.permission.guard.ts new file mode 100644 index 0000000..2684c8d --- /dev/null +++ b/src/guards/user.device.controllable.permission.guard.ts @@ -0,0 +1,92 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; + +@Injectable() +export class CheckUserHaveControllablePermission implements CanActivate { + constructor( + private readonly deviceRepository: DeviceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const userUuid = req.user.uuid; + const { deviceUuid } = req.params; + + const userIsFound = await this.checkUserIsFound(userUuid); + if (!userIsFound) { + throw new NotFoundException('User not found'); + } + + const userDevicePermission = await this.checkUserDevicePermission( + userUuid, + deviceUuid, + ); + + if (userDevicePermission === PermissionType.CONTROLLABLE) { + return true; + } else { + throw new BadRequestException( + 'You do not have controllable access to this device', + ); + } + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + return !!userData; + } + + private async checkUserDevicePermission( + userUuid: string, + deviceUuid: string, + ): Promise { + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid, permission: { userUuid: userUuid } }, + relations: ['permission', 'permission.permissionType'], + }); + + if (!device) { + throw new BadRequestException( + 'You do not have controllable access to this device', + ); + } + + return device.permission[0].permissionType.type; // Assuming permissionType is a string + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: error.message, + }); + } else { + response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } + } +} diff --git a/src/guards/user.device.permission.guard.ts b/src/guards/user.device.permission.guard.ts new file mode 100644 index 0000000..e63a08e --- /dev/null +++ b/src/guards/user.device.permission.guard.ts @@ -0,0 +1,91 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; + +@Injectable() +export class CheckUserHavePermission implements CanActivate { + constructor( + private readonly deviceRepository: DeviceRepository, + private readonly userRepository: UserRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const userUuid = req.user.uuid; + const { deviceUuid } = req.params; + + const userIsFound = await this.checkUserIsFound(userUuid); + if (!userIsFound) { + throw new NotFoundException('User not found'); + } + + const userDevicePermission = await this.checkUserDevicePermission( + userUuid, + deviceUuid, + ); + + if ( + userDevicePermission === PermissionType.READ || + userDevicePermission === PermissionType.CONTROLLABLE + ) { + return true; + } else { + throw new BadRequestException('You do not have access to this device'); + } + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const userData = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + return !!userData; + } + + private async checkUserDevicePermission( + userUuid: string, + deviceUuid: string, + ): Promise { + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid, permission: { userUuid: userUuid } }, + relations: ['permission', 'permission.permissionType'], + }); + + if (!device) { + throw new BadRequestException('You do not have access to this device'); + } + + return device.permission[0].permissionType.type; // Assuming permissionType is a string + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: error.message, + }); + } else { + response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } + } +} From 0d83d10f0b9b978cf81be03283513d4f69565b10 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 5 May 2024 21:32:40 +0300 Subject: [PATCH 179/259] Update user-device-permission error message --- .../controllers/user-device-permission.controller.ts | 4 ++-- .../services/user-device-permission.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index 825694f..fef4cc7 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -26,8 +26,8 @@ export class UserDevicePermissionController { private readonly userDevicePermissionService: UserDevicePermissionService, ) {} - // @ApiBearerAuth() - // @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Post('add') async addDevicePermission( @Body() userDevicePermissionDto: UserDevicePermissionAddDto, diff --git a/src/user-device-permission/services/user-device-permission.service.ts b/src/user-device-permission/services/user-device-permission.service.ts index b02cb6a..c1f3d07 100644 --- a/src/user-device-permission/services/user-device-permission.service.ts +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -26,7 +26,7 @@ export class UserDevicePermissionService { } catch (error) { if (error.code === '23505') { throw new HttpException( - 'This Permission already belongs to this user', + 'This User already belongs to this device', HttpStatus.BAD_REQUEST, ); } @@ -56,7 +56,7 @@ export class UserDevicePermissionService { } catch (error) { if (error.code === '23505') { throw new HttpException( - 'This Permission already belongs to this user', + 'This User already belongs to this device', HttpStatus.BAD_REQUEST, ); } From b65bef982d60d052bfbb7a14ef4769b5cbcb86a9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 6 May 2024 10:30:29 +0300 Subject: [PATCH 180/259] Add role type enum, dto, entity, repository, and module --- libs/common/src/constants/role.type.enum.ts | 4 ++++ .../common/src/modules/role-type/dtos/index.ts | 1 + .../modules/role-type/dtos/role.type.dto.ts | 11 +++++++++++ .../src/modules/role-type/entities/index.ts | 1 + .../role-type/entities/role.type.entity.ts | 18 ++++++++++++++++++ .../modules/role-type/repositories/index.ts | 1 + .../repositories/role.type.repository.ts | 10 ++++++++++ .../role-type/role.type.repository.module.ts | 11 +++++++++++ 8 files changed, 57 insertions(+) create mode 100644 libs/common/src/constants/role.type.enum.ts create mode 100644 libs/common/src/modules/role-type/dtos/index.ts create mode 100644 libs/common/src/modules/role-type/dtos/role.type.dto.ts create mode 100644 libs/common/src/modules/role-type/entities/index.ts create mode 100644 libs/common/src/modules/role-type/entities/role.type.entity.ts create mode 100644 libs/common/src/modules/role-type/repositories/index.ts create mode 100644 libs/common/src/modules/role-type/repositories/role.type.repository.ts create mode 100644 libs/common/src/modules/role-type/role.type.repository.module.ts diff --git a/libs/common/src/constants/role.type.enum.ts b/libs/common/src/constants/role.type.enum.ts new file mode 100644 index 0000000..34b3929 --- /dev/null +++ b/libs/common/src/constants/role.type.enum.ts @@ -0,0 +1,4 @@ +export enum RoleType { + USER = 'USER', + ADMIN = 'ADMIN', +} diff --git a/libs/common/src/modules/role-type/dtos/index.ts b/libs/common/src/modules/role-type/dtos/index.ts new file mode 100644 index 0000000..6452280 --- /dev/null +++ b/libs/common/src/modules/role-type/dtos/index.ts @@ -0,0 +1 @@ +export * from './role.type.dto'; diff --git a/libs/common/src/modules/role-type/dtos/role.type.dto.ts b/libs/common/src/modules/role-type/dtos/role.type.dto.ts new file mode 100644 index 0000000..77c9c46 --- /dev/null +++ b/libs/common/src/modules/role-type/dtos/role.type.dto.ts @@ -0,0 +1,11 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class RoleTypeDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsEnum(RoleType) + public type: RoleType; +} diff --git a/libs/common/src/modules/role-type/entities/index.ts b/libs/common/src/modules/role-type/entities/index.ts new file mode 100644 index 0000000..a6c063c --- /dev/null +++ b/libs/common/src/modules/role-type/entities/index.ts @@ -0,0 +1 @@ +export * from './role.type.entity'; diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts new file mode 100644 index 0000000..974365c --- /dev/null +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { RoleTypeDto } from '../dtos/role.type.dto'; +import { RoleType } from '@app/common/constants/role.type.enum'; + +@Entity({ name: 'role-type' }) +export class RoleTypeEntity extends AbstractEntity { + @Column({ + nullable: false, + enum: Object.values(RoleType), + }) + type: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/role-type/repositories/index.ts b/libs/common/src/modules/role-type/repositories/index.ts new file mode 100644 index 0000000..e2581bc --- /dev/null +++ b/libs/common/src/modules/role-type/repositories/index.ts @@ -0,0 +1 @@ +export * from './role.type.repository'; diff --git a/libs/common/src/modules/role-type/repositories/role.type.repository.ts b/libs/common/src/modules/role-type/repositories/role.type.repository.ts new file mode 100644 index 0000000..d21469f --- /dev/null +++ b/libs/common/src/modules/role-type/repositories/role.type.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { RoleTypeEntity } from '../entities/role.type.entity'; + +@Injectable() +export class RoleTypeRepository extends Repository { + constructor(private dataSource: DataSource) { + super(RoleTypeEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/role-type/role.type.repository.module.ts b/libs/common/src/modules/role-type/role.type.repository.module.ts new file mode 100644 index 0000000..85ecc41 --- /dev/null +++ b/libs/common/src/modules/role-type/role.type.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RoleTypeEntity } from './entities/role.type.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([RoleTypeEntity])], +}) +export class RoleTypeRepositoryModule {} From 7d6964208cedc7f79c2ad7080eeae0ea6c1af03b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 6 May 2024 18:33:49 +0300 Subject: [PATCH 181/259] Add UserRoleEntity and related modules for user role management --- .../role-type/entities/role.type.entity.ts | 10 ++++++-- .../src/modules/user-role/dtos/index.ts | 1 + .../modules/user-role/dtos/user.role.dto.ts | 15 ++++++++++++ .../src/modules/user-role/entities/index.ts | 1 + .../user-role/entities/user.role.entity.ts | 23 +++++++++++++++++++ .../modules/user-role/repositories/index.ts | 1 + .../repositories/user.role.repository.ts | 10 ++++++++ .../user-role/user.role.repository.module.ts | 10 ++++++++ .../src/modules/user/entities/user.entity.ts | 8 +++++++ 9 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/modules/user-role/dtos/index.ts create mode 100644 libs/common/src/modules/user-role/dtos/user.role.dto.ts create mode 100644 libs/common/src/modules/user-role/entities/index.ts create mode 100644 libs/common/src/modules/user-role/entities/user.role.entity.ts create mode 100644 libs/common/src/modules/user-role/repositories/index.ts create mode 100644 libs/common/src/modules/user-role/repositories/user.role.repository.ts create mode 100644 libs/common/src/modules/user-role/user.role.repository.module.ts diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index 974365c..c65db72 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -1,7 +1,8 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { RoleTypeDto } from '../dtos/role.type.dto'; import { RoleType } from '@app/common/constants/role.type.enum'; +import { UserRoleEntity } from '../../user-role/entities'; @Entity({ name: 'role-type' }) export class RoleTypeEntity extends AbstractEntity { @@ -10,7 +11,12 @@ export class RoleTypeEntity extends AbstractEntity { enum: Object.values(RoleType), }) type: string; - + @OneToMany(() => UserRoleEntity, (role) => role.roleType, { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + role: UserRoleEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/user-role/dtos/index.ts b/libs/common/src/modules/user-role/dtos/index.ts new file mode 100644 index 0000000..7879674 --- /dev/null +++ b/libs/common/src/modules/user-role/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.role.dto'; diff --git a/libs/common/src/modules/user-role/dtos/user.role.dto.ts b/libs/common/src/modules/user-role/dtos/user.role.dto.ts new file mode 100644 index 0000000..3ff4dab --- /dev/null +++ b/libs/common/src/modules/user-role/dtos/user.role.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserRoleDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public roleTypeUuid: string; +} diff --git a/libs/common/src/modules/user-role/entities/index.ts b/libs/common/src/modules/user-role/entities/index.ts new file mode 100644 index 0000000..b6f3bd9 --- /dev/null +++ b/libs/common/src/modules/user-role/entities/index.ts @@ -0,0 +1 @@ +export * from './user.role.entity'; diff --git a/libs/common/src/modules/user-role/entities/user.role.entity.ts b/libs/common/src/modules/user-role/entities/user.role.entity.ts new file mode 100644 index 0000000..e375723 --- /dev/null +++ b/libs/common/src/modules/user-role/entities/user.role.entity.ts @@ -0,0 +1,23 @@ +import { Entity, ManyToOne } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserRoleDto } from '../dtos'; +import { UserEntity } from '../../user/entities'; +import { RoleTypeEntity } from '../../role-type/entities'; + +@Entity({ name: 'user-role' }) +export class UserRoleEntity extends AbstractEntity { + @ManyToOne(() => UserEntity, (user) => user.role, { + nullable: false, + }) + user: UserEntity; + + @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.role, { + nullable: false, + }) + roleType: RoleTypeEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user-role/repositories/index.ts b/libs/common/src/modules/user-role/repositories/index.ts new file mode 100644 index 0000000..abf7247 --- /dev/null +++ b/libs/common/src/modules/user-role/repositories/index.ts @@ -0,0 +1 @@ +export * from './user.role.repository'; diff --git a/libs/common/src/modules/user-role/repositories/user.role.repository.ts b/libs/common/src/modules/user-role/repositories/user.role.repository.ts new file mode 100644 index 0000000..9bc9a24 --- /dev/null +++ b/libs/common/src/modules/user-role/repositories/user.role.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserRoleEntity } from '../entities'; + +@Injectable() +export class UserRoleRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserRoleEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user-role/user.role.repository.module.ts b/libs/common/src/modules/user-role/user.role.repository.module.ts new file mode 100644 index 0000000..540787e --- /dev/null +++ b/libs/common/src/modules/user-role/user.role.repository.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserRoleEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserRoleEntity])], +}) +export class UserRoleRepositoryModule {} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 4268ae8..ffe402c 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -3,6 +3,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user-space/entities'; +import { UserRoleEntity } from '../../user-role/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -57,6 +58,13 @@ export class UserEntity extends AbstractEntity { (userPermission) => userPermission.user, ) userPermission: DeviceUserPermissionEntity[]; + + @OneToMany(() => UserRoleEntity, (role) => role.user, { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + role: UserRoleEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); From 6bd827fe57bb912c94ee0630ee792ffd2f0fec38 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 6 May 2024 18:34:03 +0300 Subject: [PATCH 182/259] Add UserRoleEntity and RoleTypeEntity imports to database module --- libs/common/src/database/database.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 146b5ba..ce51bae 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -14,6 +14,8 @@ import { UserSpaceEntity } from '../modules/user-space/entities'; import { GroupEntity } from '../modules/group/entities'; import { GroupDeviceEntity } from '../modules/group-device/entities'; import { DeviceUserPermissionEntity } from '../modules/device-user-permission/entities'; +import { UserRoleEntity } from '../modules/user-role/entities'; +import { RoleTypeEntity } from '../modules/role-type/entities'; @Module({ imports: [ @@ -42,6 +44,8 @@ import { DeviceUserPermissionEntity } from '../modules/device-user-permission/en GroupEntity, GroupDeviceEntity, DeviceUserPermissionEntity, + UserRoleEntity, + RoleTypeEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), From 1dcb0a3052f099fc6d1b93a453845ae6c5c6dd46 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 6 May 2024 19:10:24 +0300 Subject: [PATCH 183/259] Add UserRoleRepository and RoleTypeRepository imports --- libs/common/src/auth/services/auth.service.ts | 4 ++ src/auth/auth.module.ts | 4 ++ src/auth/controllers/user-auth.controller.ts | 2 +- src/auth/services/user-auth.service.ts | 41 +++++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 5e57550..45b19fa 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -22,7 +22,9 @@ export class AuthService { where: { email, }, + relations: ['role.roleType'], }); + if (!user.isUserVerified) { throw new BadRequestException('User is not verified'); } @@ -68,7 +70,9 @@ export class AuthService { uuid: user.uuid, type: user.type, sessionId: user.sessionId, + roles: user.roles, }; + const tokens = await this.getTokens(payload); await this.updateRefreshToken(user.uuid, tokens.refreshToken); return tokens; diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 2165d5e..10515e7 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -9,6 +9,8 @@ import { UserAuthService } from './services'; import { UserRepository } from '../../libs/common/src/modules/user/repositories'; import { UserSessionRepository } from '../../libs/common/src/modules/session/repositories/session.repository'; import { UserOtpRepository } from '../../libs/common/src/modules/user-otp/repositories/user-otp.repository'; +import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; @Module({ imports: [ConfigModule, UserRepositoryModule, CommonModule], @@ -19,6 +21,8 @@ import { UserOtpRepository } from '../../libs/common/src/modules/user-otp/reposi UserRepository, UserSessionRepository, UserOtpRepository, + UserRoleRepository, + RoleTypeRepository, ], exports: [AuthenticationService, UserAuthService], }) diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index f8c45f9..3796649 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -47,7 +47,7 @@ export class UserAuthController { return { statusCode: HttpStatus.CREATED, data: accessToken, - message: 'User Loggedin Successfully', + message: 'User Logged in Successfully', }; } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 7b278b2..705ba9e 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -1,3 +1,5 @@ +import { RoleTypeRepository } from './../../../libs/common/src/modules/role-type/repositories/role.type.repository'; +import { UserRoleRepository } from './../../../libs/common/src/modules/user-role/repositories/user.role.repository'; import { UserRepository } from '../../../libs/common/src/modules/user/repositories'; import { BadRequestException, @@ -16,6 +18,7 @@ import { EmailService } from '../../../libs/common/src/util/email.service'; import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; import * as argon2 from 'argon2'; +import { RoleType } from '@app/common/constants/role.type.enum'; @Injectable() export class UserAuthService { @@ -26,6 +29,8 @@ export class UserAuthService { private readonly helperHashService: HelperHashService, private readonly authService: AuthService, private readonly emailService: EmailService, + private readonly userRoleRepository: UserRoleRepository, + private readonly roleTypeRepository: RoleTypeRepository, ) {} async signUp(userSignUpDto: UserSignUpDto): Promise { @@ -33,12 +38,31 @@ export class UserAuthService { if (findUser) { throw new BadRequestException('User already registered with given email'); } - const salt = this.helperHashService.randomSalt(10); - const password = this.helperHashService.bcrypt( + const salt = this.helperHashService.randomSalt(10); // Hash the password using bcrypt + const hashedPassword = await this.helperHashService.bcrypt( userSignUpDto.password, salt, ); - return await this.userRepository.save({ ...userSignUpDto, password }); + + try { + const user = await this.userRepository.save({ + ...userSignUpDto, + password: hashedPassword, + }); + + const defaultUserRoleUuid = await this.getRoleUuidByRoleType( + RoleType.USER, + ); + + await this.userRoleRepository.save({ + user: { uuid: user.uuid }, + roleType: { uuid: defaultUserRoleUuid }, + }); + + return user; + } catch (error) { + throw new BadRequestException('Failed to register user'); + } } async findUser(email: string) { @@ -67,6 +91,7 @@ export class UserAuthService { async userLogin(data: UserLoginDto) { const user = await this.authService.validateUser(data.email, data.password); + if (!user) { throw new UnauthorizedException('Invalid login credentials.'); } @@ -89,6 +114,9 @@ export class UserAuthService { email: user.email, userId: user.uuid, uuid: user.uuid, + roles: user.role.map((role) => { + return { uuid: role.uuid, type: role.roleType.type }; + }), sessionId: session[1].uuid, }); } @@ -186,4 +214,11 @@ export class UserAuthService { await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken); return tokens; } + private async getRoleUuidByRoleType(roleType: string) { + const role = await this.roleTypeRepository.findOne({ + where: { type: roleType }, + }); + + return role.uuid; + } } From a093fd3f72176d2615d3c7027d86e44f3935fcad Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 6 May 2024 23:49:09 +0300 Subject: [PATCH 184/259] Add Role Module with Role Controller, Service, and DTOs --- src/app.module.ts | 2 + src/role/controllers/index.ts | 1 + src/role/controllers/role.controller.ts | 58 +++++++++++++++++++++++++ src/role/dtos/index.ts | 1 + src/role/dtos/role.edit.dto.ts | 13 ++++++ src/role/role.module.ts | 25 +++++++++++ src/role/services/index.ts | 1 + src/role/services/role.service.ts | 44 +++++++++++++++++++ 8 files changed, 145 insertions(+) create mode 100644 src/role/controllers/index.ts create mode 100644 src/role/controllers/role.controller.ts create mode 100644 src/role/dtos/index.ts create mode 100644 src/role/dtos/role.edit.dto.ts create mode 100644 src/role/role.module.ts create mode 100644 src/role/services/index.ts create mode 100644 src/role/services/role.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 4243057..c9f7275 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { CommunityModule } from './community/community.module'; import { BuildingModule } from './building/building.module'; import { FloorModule } from './floor/floor.module'; import { UnitModule } from './unit/unit.module'; +import { RoleModule } from './role/role.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -19,6 +20,7 @@ import { UnitModule } from './unit/unit.module'; }), AuthenticationModule, UserModule, + RoleModule, CommunityModule, BuildingModule, FloorModule, diff --git a/src/role/controllers/index.ts b/src/role/controllers/index.ts new file mode 100644 index 0000000..fa5a3e4 --- /dev/null +++ b/src/role/controllers/index.ts @@ -0,0 +1 @@ +export * from './role.controller'; diff --git a/src/role/controllers/role.controller.ts b/src/role/controllers/role.controller.ts new file mode 100644 index 0000000..1bc87ce --- /dev/null +++ b/src/role/controllers/role.controller.ts @@ -0,0 +1,58 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { RoleService } from '../services/role.service'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UserRoleEditDto } from '../dtos'; + +@ApiTags('Role Module') +@Controller({ + version: '1', + path: 'role', +}) +export class RoleController { + constructor(private readonly roleService: RoleService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('types') + async fetchRoleTypes() { + try { + const roleTypes = await this.roleService.fetchRoleTypes(); + return { + statusCode: HttpStatus.OK, + message: 'Role Types fetched Successfully', + data: roleTypes, + }; + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('edit/user/:userUuid') + async editUserRoleType( + @Param('userUuid') userUuid: string, + @Body() userRoleEditDto: UserRoleEditDto, + ) { + try { + await this.roleService.editUserRoleType(userUuid, userRoleEditDto); + return { + statusCode: HttpStatus.OK, + message: 'User Role Updated Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/role/dtos/index.ts b/src/role/dtos/index.ts new file mode 100644 index 0000000..8fc5216 --- /dev/null +++ b/src/role/dtos/index.ts @@ -0,0 +1 @@ +export * from './role.edit.dto'; diff --git a/src/role/dtos/role.edit.dto.ts b/src/role/dtos/role.edit.dto.ts new file mode 100644 index 0000000..5cd0aac --- /dev/null +++ b/src/role/dtos/role.edit.dto.ts @@ -0,0 +1,13 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +export class UserRoleEditDto { + @ApiProperty({ + description: 'role type', + enum: RoleType, + required: true, + }) + @IsEnum(RoleType) + roleType: RoleType; +} diff --git a/src/role/role.module.ts b/src/role/role.module.ts new file mode 100644 index 0000000..a487979 --- /dev/null +++ b/src/role/role.module.ts @@ -0,0 +1,25 @@ +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { RoleService } from './services/role.service'; +import { RoleController } from './controllers/role.controller'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; + +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [RoleController], + providers: [ + DeviceUserPermissionRepository, + PermissionTypeRepository, + DeviceRepository, + RoleService, + RoleTypeRepository, + UserRoleRepository, + ], + exports: [RoleService], +}) +export class RoleModule {} diff --git a/src/role/services/index.ts b/src/role/services/index.ts new file mode 100644 index 0000000..1adbb54 --- /dev/null +++ b/src/role/services/index.ts @@ -0,0 +1 @@ +export * from './role.service'; diff --git a/src/role/services/role.service.ts b/src/role/services/role.service.ts new file mode 100644 index 0000000..b8db739 --- /dev/null +++ b/src/role/services/role.service.ts @@ -0,0 +1,44 @@ +import { RoleTypeRepository } from './../../../libs/common/src/modules/role-type/repositories/role.type.repository'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { UserRoleEditDto } from '../dtos/role.edit.dto'; +import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; + +@Injectable() +export class RoleService { + constructor( + private readonly roleTypeRepository: RoleTypeRepository, + private readonly userRoleRepository: UserRoleRepository, + ) {} + + async editUserRoleType(userUuid: string, userRoleEditDto: UserRoleEditDto) { + try { + const roleType = await this.fetchRoleByType(userRoleEditDto.roleType); + return await this.userRoleRepository.update( + { user: { uuid: userUuid } }, + { + roleType: { + uuid: roleType.uuid, + }, + }, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async fetchRoleTypes() { + const roleTypes = await this.roleTypeRepository.find(); + + return roleTypes; + } + private async fetchRoleByType(roleType: string) { + return await this.roleTypeRepository.findOne({ + where: { + type: roleType, + }, + }); + } +} From 85424554cb93baf1a977dc3d85fa1510c46f0c01 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 7 May 2024 00:32:53 +0300 Subject: [PATCH 185/259] Add roles to user payload in JWT and refresh token strategies --- .../src/auth/interfaces/auth.interface.ts | 1 + .../src/auth/strategies/jwt.strategy.ts | 3 ++- .../auth/strategies/refresh-token.strategy.ts | 3 ++- src/guards/admin.role.guard.ts | 17 +++++++++++++++ src/guards/user.role.guard.ts | 21 +++++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/guards/admin.role.guard.ts create mode 100644 src/guards/user.role.guard.ts diff --git a/libs/common/src/auth/interfaces/auth.interface.ts b/libs/common/src/auth/interfaces/auth.interface.ts index ae85fe1..8e57cd6 100644 --- a/libs/common/src/auth/interfaces/auth.interface.ts +++ b/libs/common/src/auth/interfaces/auth.interface.ts @@ -4,4 +4,5 @@ export class AuthInterface { uuid: string; sessionId: string; id: number; + roles: string[]; } diff --git a/libs/common/src/auth/strategies/jwt.strategy.ts b/libs/common/src/auth/strategies/jwt.strategy.ts index 67faecf..ed29a3a 100644 --- a/libs/common/src/auth/strategies/jwt.strategy.ts +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -28,9 +28,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { if (validateUser) { return { email: payload.email, - userId: payload.id, + userUuid: payload.uuid, uuid: payload.uuid, sessionId: payload.sessionId, + roles: payload.roles, }; } else { throw new BadRequestException('Unauthorized'); diff --git a/libs/common/src/auth/strategies/refresh-token.strategy.ts b/libs/common/src/auth/strategies/refresh-token.strategy.ts index 9b21010..ee36eac 100644 --- a/libs/common/src/auth/strategies/refresh-token.strategy.ts +++ b/libs/common/src/auth/strategies/refresh-token.strategy.ts @@ -31,9 +31,10 @@ export class RefreshTokenStrategy extends PassportStrategy( if (validateUser) { return { email: payload.email, - userId: payload.id, + userUuid: payload.uuid, uuid: payload.uuid, sessionId: payload.sessionId, + roles: payload.roles, }; } else { throw new BadRequestException('Unauthorized'); diff --git a/src/guards/admin.role.guard.ts b/src/guards/admin.role.guard.ts new file mode 100644 index 0000000..0c3b259 --- /dev/null +++ b/src/guards/admin.role.guard.ts @@ -0,0 +1,17 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +export class AdminRoleGuard extends AuthGuard('jwt') { + handleRequest(err, user) { + const isAdmin = user.roles.some((role) => role.type === RoleType.ADMIN); + if (err || !user) { + throw err || new UnauthorizedException(); + } else { + if (!isAdmin) { + throw new BadRequestException('Only admin role can access this route'); + } + } + return user; + } +} diff --git a/src/guards/user.role.guard.ts b/src/guards/user.role.guard.ts new file mode 100644 index 0000000..b21632a --- /dev/null +++ b/src/guards/user.role.guard.ts @@ -0,0 +1,21 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +export class UserRoleGuard extends AuthGuard('jwt') { + handleRequest(err, user) { + const isUserOrAdmin = user.roles.some( + (role) => role.type === RoleType.ADMIN || role.type === RoleType.USER, + ); + if (err || !user) { + throw err || new UnauthorizedException(); + } else { + if (!isUserOrAdmin) { + throw new BadRequestException( + 'Only admin or user role can access this route', + ); + } + } + return user; + } +} From 4058449f4b591d860faf057135aaec3546f0a8af Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 7 May 2024 00:33:04 +0300 Subject: [PATCH 186/259] Update guard usage in controllers --- src/auth/controllers/user-auth.controller.ts | 6 +++--- .../controllers/building.controller.ts | 17 +++++++++-------- .../controllers/community.controller.ts | 15 ++++++++------- src/device/controllers/device.controller.ts | 18 +++++++++--------- src/floor/controllers/floor.controller.ts | 17 +++++++++-------- src/group/controllers/group.controller.ts | 14 +++++++------- src/role/controllers/role.controller.ts | 6 +++--- src/room/controllers/room.controller.ts | 15 ++++++++------- src/unit/controllers/unit.controller.ts | 17 +++++++++-------- .../user-device-permission.controller.ts | 10 +++++----- src/users/controllers/user.controller.ts | 4 ++-- 11 files changed, 72 insertions(+), 67 deletions(-) diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index 3796649..fd139f9 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -14,9 +14,9 @@ import { UserSignUpDto } from '../dtos/user-auth.dto'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ResponseMessage } from '../../../libs/common/src/response/response.decorator'; import { UserLoginDto } from '../dtos/user-login.dto'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @Controller({ version: '1', @@ -52,7 +52,7 @@ export class UserAuthController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Delete('user/delete/:id') async userDelete(@Param('id') id: string) { await this.userAuthService.deleteUser(id); @@ -98,7 +98,7 @@ export class UserAuthController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Get('user/list') async userList() { const userList = await this.userAuthService.userList(); diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index dc4dbd7..b561ff9 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -12,12 +12,13 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddBuildingDto, AddUserBuildingDto } from '../dtos/add.building.dto'; import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; import { CheckUserBuildingGuard } from 'src/guards/user.building.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Building Module') @Controller({ @@ -28,7 +29,7 @@ export class BuildingController { constructor(private readonly buildingService: BuildingService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) + @UseGuards(AdminRoleGuard, CheckCommunityTypeGuard) @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { @@ -43,7 +44,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':buildingUuid') async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -59,7 +60,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('child/:buildingUuid') async getBuildingChildByUuid( @Param('buildingUuid') buildingUuid: string, @@ -79,7 +80,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('parent/:buildingUuid') async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -94,7 +95,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserBuildingGuard) + @UseGuards(AdminRoleGuard, CheckUserBuildingGuard) @Post('user') async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) { try { @@ -108,7 +109,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('user/:userUuid') async getBuildingsByUserId(@Param('userUuid') userUuid: string) { try { @@ -122,7 +123,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:buildingUuid') async renameBuildingByUuid( @Param('buildingUuid') buildingUuid: string, diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 3fd2233..144041d 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -12,7 +12,6 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddCommunityDto, AddUserCommunityDto, @@ -20,6 +19,8 @@ import { import { GetCommunityChildDto } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Community Module') @Controller({ @@ -30,7 +31,7 @@ export class CommunityController { constructor(private readonly communityService: CommunityService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Post() async addCommunity(@Body() addCommunityDto: AddCommunityDto) { try { @@ -46,7 +47,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':communityUuid') async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { try { @@ -62,7 +63,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('child/:communityUuid') async getCommunityChildByUuid( @Param('communityUuid') communityUuid: string, @@ -83,7 +84,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('user/:userUuid') async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { try { @@ -96,7 +97,7 @@ export class CommunityController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserCommunityGuard) + @UseGuards(AdminRoleGuard, CheckUserCommunityGuard) @Post('user') async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { try { @@ -110,7 +111,7 @@ export class CommunityController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:communityUuid') async renameCommunityByUuid( @Param('communityUuid') communityUuid: string, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index d8149a7..7ebf1ca 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -23,9 +23,9 @@ import { import { ControlDeviceDto } from '../dtos/control.device.dto'; import { CheckRoomGuard } from 'src/guards/room.guard'; import { CheckGroupGuard } from 'src/guards/group.guard'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Device Module') @Controller({ @@ -36,7 +36,7 @@ export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckRoomGuard) + @UseGuards(UserRoleGuard, CheckRoomGuard) @Get('room') async getDevicesByRoomId( @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, @@ -57,7 +57,7 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckRoomGuard) + @UseGuards(UserRoleGuard, CheckRoomGuard) @Post('room') async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { try { @@ -70,7 +70,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckGroupGuard) + @UseGuards(UserRoleGuard, CheckGroupGuard) @Get('group') async getDevicesByGroupId( @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, @@ -90,7 +90,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckGroupGuard) + @UseGuards(UserRoleGuard, CheckGroupGuard) @Post('group') async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { try { @@ -103,7 +103,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(UserRoleGuard, CheckUserHavePermission) @Get(':deviceUuid') async getDeviceDetailsByDeviceId( @Param('deviceUuid') deviceUuid: string, @@ -123,7 +123,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(UserRoleGuard, CheckUserHavePermission) @Get(':deviceUuid/functions') async getDeviceInstructionByDeviceId( @Param('deviceUuid') deviceUuid: string, @@ -140,7 +140,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(UserRoleGuard, CheckUserHavePermission) @Get(':deviceUuid/functions/status') async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) { try { @@ -154,7 +154,7 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHaveControllablePermission) + @UseGuards(UserRoleGuard, CheckUserHaveControllablePermission) @Post(':deviceUuid/control') async controlDevice( @Body() controlDeviceDto: ControlDeviceDto, diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index a42db7b..2fd9dfc 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -12,12 +12,13 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddFloorDto, AddUserFloorDto } from '../dtos/add.floor.dto'; import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; import { CheckUserFloorGuard } from 'src/guards/user.floor.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @ApiTags('Floor Module') @Controller({ @@ -28,7 +29,7 @@ export class FloorController { constructor(private readonly floorService: FloorService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) + @UseGuards(AdminRoleGuard, CheckBuildingTypeGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { @@ -43,7 +44,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':floorUuid') async getFloorByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -58,7 +59,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('child/:floorUuid') async getFloorChildByUuid( @Param('floorUuid') floorUuid: string, @@ -78,7 +79,7 @@ export class FloorController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('parent/:floorUuid') async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -93,7 +94,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserFloorGuard) + @UseGuards(AdminRoleGuard, CheckUserFloorGuard) @Post('user') async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) { try { @@ -108,7 +109,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('user/:userUuid') async getFloorsByUserId(@Param('userUuid') userUuid: string) { try { @@ -122,7 +123,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:floorUuid') async renameFloorByUuid( @Param('floorUuid') floorUuid: string, diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 0f74ff6..c00ff92 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -12,11 +12,11 @@ import { HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddGroupDto } from '../dtos/add.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Group Module') @Controller({ @@ -27,7 +27,7 @@ export class GroupController { constructor(private readonly groupService: GroupService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('space/:spaceUuid') async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { try { @@ -40,7 +40,7 @@ export class GroupController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':groupUuid') async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { try { @@ -53,7 +53,7 @@ export class GroupController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckProductUuidForAllDevicesGuard) + @UseGuards(UserRoleGuard, CheckProductUuidForAllDevicesGuard) @Post() async addGroup(@Body() addGroupDto: AddGroupDto) { try { @@ -67,7 +67,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Post('control') async controlGroup(@Body() controlGroupDto: ControlGroupDto) { try { @@ -81,7 +81,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:groupUuid') async renameGroupByUuid( @Param('groupUuid') groupUuid: string, @@ -101,7 +101,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Delete(':groupUuid') async deleteGroup(@Param('groupUuid') groupUuid: string) { try { diff --git a/src/role/controllers/role.controller.ts b/src/role/controllers/role.controller.ts index 1bc87ce..ebabe93 100644 --- a/src/role/controllers/role.controller.ts +++ b/src/role/controllers/role.controller.ts @@ -10,8 +10,8 @@ import { } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { RoleService } from '../services/role.service'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { UserRoleEditDto } from '../dtos'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @ApiTags('Role Module') @Controller({ @@ -21,7 +21,7 @@ import { UserRoleEditDto } from '../dtos'; export class RoleController { constructor(private readonly roleService: RoleService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Get('types') async fetchRoleTypes() { try { @@ -36,7 +36,7 @@ export class RoleController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Put('edit/user/:userUuid') async editUserRoleType( @Param('userUuid') userUuid: string, diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 221fb39..d39b584 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -11,11 +11,12 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddRoomDto, AddUserRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; import { CheckUserRoomGuard } from 'src/guards/user.room.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Room Module') @Controller({ @@ -26,7 +27,7 @@ export class RoomController { constructor(private readonly roomService: RoomService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) + @UseGuards(AdminRoleGuard, CheckUnitTypeGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { @@ -41,7 +42,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':roomUuid') async getRoomByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -56,7 +57,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('parent/:roomUuid') async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -70,7 +71,7 @@ export class RoomController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserRoomGuard) + @UseGuards(AdminRoleGuard, CheckUserRoomGuard) @Post('user') async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) { try { @@ -84,7 +85,7 @@ export class RoomController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('user/:userUuid') async getRoomsByUserId(@Param('userUuid') userUuid: string) { try { @@ -98,7 +99,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:roomUuid') async renameRoomByUuid( @Param('roomUuid') roomUuid: string, diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index f0f3775..c42793c 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -12,12 +12,13 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddUnitDto, AddUserUnitDto } from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { UserRoleGuard } from 'src/guards/user.role.guard'; @ApiTags('Unit Module') @Controller({ @@ -28,7 +29,7 @@ export class UnitController { constructor(private readonly unitService: UnitService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) + @UseGuards(AdminRoleGuard, CheckFloorTypeGuard) @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { @@ -43,7 +44,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get(':unitUuid') async getUnitByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -58,7 +59,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('child/:unitUuid') async getUnitChildByUuid( @Param('unitUuid') unitUuid: string, @@ -75,7 +76,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('parent/:unitUuid') async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -89,7 +90,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserUnitGuard) + @UseGuards(AdminRoleGuard, CheckUserUnitGuard) @Post('user') async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { try { @@ -103,7 +104,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Get('user/:userUuid') async getUnitsByUserId(@Param('userUuid') userUuid: string) { try { @@ -117,7 +118,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(UserRoleGuard) @Put('rename/:unitUuid') async renameUnitByUuid( @Param('unitUuid') unitUuid: string, diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index fef4cc7..2f68708 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -13,8 +13,8 @@ import { import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { UserDevicePermissionService } from '../services/user-device-permission.service'; import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @ApiTags('Device Permission Module') @Controller({ @@ -27,7 +27,7 @@ export class UserDevicePermissionController { ) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Post('add') async addDevicePermission( @Body() userDevicePermissionDto: UserDevicePermissionAddDto, @@ -51,7 +51,7 @@ export class UserDevicePermissionController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Put('edit/:devicePermissionUuid') async editDevicePermission( @Param('devicePermissionUuid') devicePermissionUuid: string, @@ -75,7 +75,7 @@ export class UserDevicePermissionController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Get(':deviceUuid/list') async fetchDevicePermission(@Param('deviceUuid') deviceUuid: string) { try { @@ -91,7 +91,7 @@ export class UserDevicePermissionController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Delete(':devicePermissionUuid') async deleteDevicePermission( @Param('devicePermissionUuid') devicePermissionUuid: string, diff --git a/src/users/controllers/user.controller.ts b/src/users/controllers/user.controller.ts index 4620cd0..09518e8 100644 --- a/src/users/controllers/user.controller.ts +++ b/src/users/controllers/user.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { UserService } from '../services/user.service'; import { UserListDto } from '../dtos/user.list.dto'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @ApiTags('User Module') @Controller({ @@ -13,7 +13,7 @@ export class UserController { constructor(private readonly userService: UserService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(AdminRoleGuard) @Get('list') async userList(@Query() userListDto: UserListDto) { try { From c30a21b04f5d81042e6952a01774f4b10721e864 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 7 May 2024 09:48:16 +0300 Subject: [PATCH 187/259] Add Unique constraint to user and roleType fields in UserRoleEntity --- libs/common/src/modules/user-role/entities/user.role.entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/user-role/entities/user.role.entity.ts b/libs/common/src/modules/user-role/entities/user.role.entity.ts index e375723..34b49c4 100644 --- a/libs/common/src/modules/user-role/entities/user.role.entity.ts +++ b/libs/common/src/modules/user-role/entities/user.role.entity.ts @@ -1,10 +1,11 @@ -import { Entity, ManyToOne } from 'typeorm'; +import { Entity, ManyToOne, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserRoleDto } from '../dtos'; import { UserEntity } from '../../user/entities'; import { RoleTypeEntity } from '../../role-type/entities'; @Entity({ name: 'user-role' }) +@Unique(['user', 'roleType']) export class UserRoleEntity extends AbstractEntity { @ManyToOne(() => UserEntity, (user) => user.role, { nullable: false, From bee140f517e0f50b44a1a6fc1eecff8de6cbfe78 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 7 May 2024 22:27:45 +0300 Subject: [PATCH 188/259] Add space permission service and guards for various entities --- libs/common/src/helper/helper.module.ts | 9 +++-- libs/common/src/helper/services/index.ts | 1 + .../services/space.permission.service.ts | 35 +++++++++++++++++++ src/guards/building.permission.guard.ts | 35 +++++++++++++++++++ src/guards/community.permission.guard.ts | 35 +++++++++++++++++++ src/guards/floor.permission.guard.ts | 35 +++++++++++++++++++ src/guards/room.permission.guard.ts | 35 +++++++++++++++++++ src/guards/unit.permission.guard.ts | 35 +++++++++++++++++++ 8 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 libs/common/src/helper/services/space.permission.service.ts create mode 100644 src/guards/building.permission.guard.ts create mode 100644 src/guards/community.permission.guard.ts create mode 100644 src/guards/floor.permission.guard.ts create mode 100644 src/guards/room.permission.guard.ts create mode 100644 src/guards/unit.permission.guard.ts diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index 826883a..baa8f07 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -1,11 +1,14 @@ import { Global, Module } from '@nestjs/common'; import { HelperHashService } from './services'; +import { SpacePermissionService } from './services/space.permission.service'; +import { SpaceRepository } from '../modules/space/repositories'; +import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; @Global() @Module({ - providers: [HelperHashService], - exports: [HelperHashService], + providers: [HelperHashService, SpacePermissionService, SpaceRepository], + exports: [HelperHashService, SpacePermissionService], controllers: [], - imports: [], + imports: [SpaceRepositoryModule], }) export class HelperModule {} diff --git a/libs/common/src/helper/services/index.ts b/libs/common/src/helper/services/index.ts index 0ede816..46139ab 100644 --- a/libs/common/src/helper/services/index.ts +++ b/libs/common/src/helper/services/index.ts @@ -1 +1,2 @@ export * from './helper.hash.service'; +export * from './space.permission.service'; diff --git a/libs/common/src/helper/services/space.permission.service.ts b/libs/common/src/helper/services/space.permission.service.ts new file mode 100644 index 0000000..326c381 --- /dev/null +++ b/libs/common/src/helper/services/space.permission.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException } from '@nestjs/common'; + +@Injectable() +export class SpacePermissionService { + constructor(private readonly spaceRepository: SpaceRepository) {} + + async checkUserPermission( + spaceUuid: string, + userUuid: string, + type: string, + ): Promise { + const spaceData = await this.spaceRepository.findOne({ + where: { + uuid: spaceUuid, + spaceType: { + type: type, + }, + userSpaces: { + user: { + uuid: userUuid, + }, + }, + }, + relations: ['spaceType', 'userSpaces', 'userSpaces.user'], + }); + + if (!spaceData) { + throw new BadRequestException( + `You do not have permission to access this ${type}`, + ); + } + } +} diff --git a/src/guards/building.permission.guard.ts b/src/guards/building.permission.guard.ts new file mode 100644 index 0000000..d73f19b --- /dev/null +++ b/src/guards/building.permission.guard.ts @@ -0,0 +1,35 @@ +import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common'; + +@Injectable() +export class BuildingPermissionGuard implements CanActivate { + constructor(private readonly permissionService: SpacePermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { buildingUuid } = req.params; + const { user } = req; + + if (!buildingUuid) { + throw new BadRequestException('buildingUuid is required'); + } + + await this.permissionService.checkUserPermission( + buildingUuid, + user.uuid, + 'building', + ); + + return true; + } catch (error) { + throw error; + } + } +} diff --git a/src/guards/community.permission.guard.ts b/src/guards/community.permission.guard.ts new file mode 100644 index 0000000..2e44cd1 --- /dev/null +++ b/src/guards/community.permission.guard.ts @@ -0,0 +1,35 @@ +import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common'; + +@Injectable() +export class CommunityPermissionGuard implements CanActivate { + constructor(private readonly permissionService: SpacePermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { communityUuid } = req.params; + const { user } = req; + + if (!communityUuid) { + throw new BadRequestException('communityUuid is required'); + } + + await this.permissionService.checkUserPermission( + communityUuid, + user.uuid, + 'community', + ); + + return true; + } catch (error) { + throw error; + } + } +} diff --git a/src/guards/floor.permission.guard.ts b/src/guards/floor.permission.guard.ts new file mode 100644 index 0000000..5e5c6ce --- /dev/null +++ b/src/guards/floor.permission.guard.ts @@ -0,0 +1,35 @@ +import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common'; + +@Injectable() +export class FloorPermissionGuard implements CanActivate { + constructor(private readonly permissionService: SpacePermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { floorUuid } = req.params; + const { user } = req; + + if (!floorUuid) { + throw new BadRequestException('floorUuid is required'); + } + + await this.permissionService.checkUserPermission( + floorUuid, + user.uuid, + 'floor', + ); + + return true; + } catch (error) { + throw error; + } + } +} diff --git a/src/guards/room.permission.guard.ts b/src/guards/room.permission.guard.ts new file mode 100644 index 0000000..721a92f --- /dev/null +++ b/src/guards/room.permission.guard.ts @@ -0,0 +1,35 @@ +import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common'; + +@Injectable() +export class RoomPermissionGuard implements CanActivate { + constructor(private readonly permissionService: SpacePermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { roomUuid } = req.params; + const { user } = req; + + if (!roomUuid) { + throw new BadRequestException('roomUuid is required'); + } + + await this.permissionService.checkUserPermission( + roomUuid, + user.uuid, + 'room', + ); + + return true; + } catch (error) { + throw error; + } + } +} diff --git a/src/guards/unit.permission.guard.ts b/src/guards/unit.permission.guard.ts new file mode 100644 index 0000000..e0d4958 --- /dev/null +++ b/src/guards/unit.permission.guard.ts @@ -0,0 +1,35 @@ +import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common'; + +@Injectable() +export class UnitPermissionGuard implements CanActivate { + constructor(private readonly permissionService: SpacePermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { unitUuid } = req.params; + const { user } = req; + + if (!unitUuid) { + throw new BadRequestException('unitUuid is required'); + } + + await this.permissionService.checkUserPermission( + unitUuid, + user.uuid, + 'unit', + ); + + return true; + } catch (error) { + throw error; + } + } +} From aff52be5408db3b1ee6ca5e3cc6a0a07a10ee190 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 7 May 2024 22:27:59 +0300 Subject: [PATCH 189/259] Add permission guards to controllers --- src/building/controllers/building.controller.ts | 9 +++++---- src/community/community.module.ts | 4 +++- src/community/controllers/community.controller.ts | 5 +++-- src/floor/controllers/floor.controller.ts | 9 +++++---- src/room/controllers/room.controller.ts | 7 ++++--- src/unit/controllers/unit.controller.ts | 9 +++++---- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index dc4dbd7..9843c8e 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -18,6 +18,7 @@ import { GetBuildingChildDto } from '../dtos/get.building.dto'; import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; import { CheckUserBuildingGuard } from 'src/guards/user.building.guard'; +import { BuildingPermissionGuard } from 'src/guards/building.permission.guard'; @ApiTags('Building Module') @Controller({ @@ -43,7 +44,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) @Get(':buildingUuid') async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -59,7 +60,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) @Get('child/:buildingUuid') async getBuildingChildByUuid( @Param('buildingUuid') buildingUuid: string, @@ -79,7 +80,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) @Get('parent/:buildingUuid') async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -122,7 +123,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) @Put('rename/:buildingUuid') async renameBuildingByUuid( @Param('buildingUuid') buildingUuid: string, diff --git a/src/community/community.module.ts b/src/community/community.module.ts index 742e3ad..e27627e 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -10,6 +10,7 @@ import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.s import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; import { UserRepository } from '@app/common/modules/user/repositories'; +import { SpacePermissionService } from '@app/common/helper/services'; @Module({ imports: [ @@ -26,7 +27,8 @@ import { UserRepository } from '@app/common/modules/user/repositories'; SpaceTypeRepository, UserSpaceRepository, UserRepository, + SpacePermissionService, ], - exports: [CommunityService], + exports: [CommunityService, SpacePermissionService], }) export class CommunityModule {} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 3fd2233..c7fd248 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -20,6 +20,7 @@ import { import { GetCommunityChildDto } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; +import { CommunityPermissionGuard } from 'src/guards/community.permission.guard'; @ApiTags('Community Module') @Controller({ @@ -46,7 +47,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) @Get(':communityUuid') async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { try { @@ -62,7 +63,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) @Get('child/:communityUuid') async getCommunityChildByUuid( @Param('communityUuid') communityUuid: string, diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index a42db7b..304d4a2 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -18,6 +18,7 @@ import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; import { CheckUserFloorGuard } from 'src/guards/user.floor.guard'; +import { FloorPermissionGuard } from 'src/guards/floor.permission.guard'; @ApiTags('Floor Module') @Controller({ @@ -43,7 +44,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, FloorPermissionGuard) @Get(':floorUuid') async getFloorByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -58,7 +59,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, FloorPermissionGuard) @Get('child/:floorUuid') async getFloorChildByUuid( @Param('floorUuid') floorUuid: string, @@ -78,7 +79,7 @@ export class FloorController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, FloorPermissionGuard) @Get('parent/:floorUuid') async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -122,7 +123,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, FloorPermissionGuard) @Put('rename/:floorUuid') async renameFloorByUuid( @Param('floorUuid') floorUuid: string, diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 221fb39..f59300b 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -16,6 +16,7 @@ import { AddRoomDto, AddUserRoomDto } from '../dtos/add.room.dto'; import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; import { CheckUserRoomGuard } from 'src/guards/user.room.guard'; +import { RoomPermissionGuard } from 'src/guards/room.permission.guard'; @ApiTags('Room Module') @Controller({ @@ -41,7 +42,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoomPermissionGuard) @Get(':roomUuid') async getRoomByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -56,7 +57,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoomPermissionGuard) @Get('parent/:roomUuid') async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -98,7 +99,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, RoomPermissionGuard) @Put('rename/:roomUuid') async renameRoomByUuid( @Param('roomUuid') roomUuid: string, diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index f0f3775..89ca053 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -18,6 +18,7 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; +import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; @ApiTags('Unit Module') @Controller({ @@ -43,7 +44,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, UnitPermissionGuard) @Get(':unitUuid') async getUnitByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -58,7 +59,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, UnitPermissionGuard) @Get('child/:unitUuid') async getUnitChildByUuid( @Param('unitUuid') unitUuid: string, @@ -75,7 +76,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, UnitPermissionGuard) @Get('parent/:unitUuid') async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -117,7 +118,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, UnitPermissionGuard) @Put('rename/:unitUuid') async renameUnitByUuid( @Param('unitUuid') unitUuid: string, From 616ddd4d4c1cf95b5944d24d9d9e0a06cae9fe52 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 8 May 2024 09:35:42 +0300 Subject: [PATCH 190/259] Refactor space permission service --- .../services/space.permission.service.ts | 36 ++++++++++--------- .../controllers/community.controller.ts | 2 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/libs/common/src/helper/services/space.permission.service.ts b/libs/common/src/helper/services/space.permission.service.ts index 326c381..0f11cbc 100644 --- a/libs/common/src/helper/services/space.permission.service.ts +++ b/libs/common/src/helper/services/space.permission.service.ts @@ -11,25 +11,29 @@ export class SpacePermissionService { userUuid: string, type: string, ): Promise { - const spaceData = await this.spaceRepository.findOne({ - where: { - uuid: spaceUuid, - spaceType: { - type: type, - }, - userSpaces: { - user: { - uuid: userUuid, + try { + const spaceData = await this.spaceRepository.findOne({ + where: { + uuid: spaceUuid, + spaceType: { + type: type, + }, + userSpaces: { + user: { + uuid: userUuid, + }, }, }, - }, - relations: ['spaceType', 'userSpaces', 'userSpaces.user'], - }); + relations: ['spaceType', 'userSpaces', 'userSpaces.user'], + }); - if (!spaceData) { - throw new BadRequestException( - `You do not have permission to access this ${type}`, - ); + if (!spaceData) { + throw new BadRequestException( + `You do not have permission to access this ${type}`, + ); + } + } catch (err) { + throw new BadRequestException(err.message || 'Invalid UUID'); } } } diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index c7fd248..4a80015 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -111,7 +111,7 @@ export class CommunityController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) @Put('rename/:communityUuid') async renameCommunityByUuid( @Param('communityUuid') communityUuid: string, From d1fa15cff1cfbd6716799fc60f6578353abae2ec Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 11 May 2024 21:01:39 +0300 Subject: [PATCH 191/259] Update role types in enums and role guards --- libs/common/src/constants/role.type.enum.ts | 3 ++- src/guards/admin.role.guard.ts | 5 ++++- src/guards/user.role.guard.ts | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/common/src/constants/role.type.enum.ts b/libs/common/src/constants/role.type.enum.ts index 34b3929..72bf14e 100644 --- a/libs/common/src/constants/role.type.enum.ts +++ b/libs/common/src/constants/role.type.enum.ts @@ -1,4 +1,5 @@ export enum RoleType { - USER = 'USER', + SUPER_ADMIN = 'SUPER_ADMIN', ADMIN = 'ADMIN', + USER = 'USER', } diff --git a/src/guards/admin.role.guard.ts b/src/guards/admin.role.guard.ts index 0c3b259..fc26067 100644 --- a/src/guards/admin.role.guard.ts +++ b/src/guards/admin.role.guard.ts @@ -4,7 +4,10 @@ import { AuthGuard } from '@nestjs/passport'; export class AdminRoleGuard extends AuthGuard('jwt') { handleRequest(err, user) { - const isAdmin = user.roles.some((role) => role.type === RoleType.ADMIN); + const isAdmin = user.roles.some( + (role) => + role.type === RoleType.SUPER_ADMIN || role.type === RoleType.ADMIN, + ); if (err || !user) { throw err || new UnauthorizedException(); } else { diff --git a/src/guards/user.role.guard.ts b/src/guards/user.role.guard.ts index b21632a..4864fe0 100644 --- a/src/guards/user.role.guard.ts +++ b/src/guards/user.role.guard.ts @@ -5,7 +5,10 @@ import { AuthGuard } from '@nestjs/passport'; export class UserRoleGuard extends AuthGuard('jwt') { handleRequest(err, user) { const isUserOrAdmin = user.roles.some( - (role) => role.type === RoleType.ADMIN || role.type === RoleType.USER, + (role) => + role.type === RoleType.SUPER_ADMIN || + role.type === RoleType.ADMIN || + role.type === RoleType.USER, ); if (err || !user) { throw err || new UnauthorizedException(); From 3e9fff3822e44f8d855b0f9a8efdf7072283436b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 11 May 2024 21:02:25 +0300 Subject: [PATCH 192/259] Add super admin configuration and service --- libs/common/src/config/index.ts | 3 +- libs/common/src/config/super.admin.config.ts | 9 +++ libs/common/src/helper/helper.module.ts | 25 ++++++- .../helper/services/super.admin.sarvice.ts | 72 +++++++++++++++++++ src/main.ts | 4 ++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 libs/common/src/config/super.admin.config.ts create mode 100644 libs/common/src/helper/services/super.admin.sarvice.ts diff --git a/libs/common/src/config/index.ts b/libs/common/src/config/index.ts index 94380ce..d4cbbdb 100644 --- a/libs/common/src/config/index.ts +++ b/libs/common/src/config/index.ts @@ -1,2 +1,3 @@ import emailConfig from './email.config'; -export default [emailConfig]; +import superAdminConfig from './super.admin.config'; +export default [emailConfig, superAdminConfig]; diff --git a/libs/common/src/config/super.admin.config.ts b/libs/common/src/config/super.admin.config.ts new file mode 100644 index 0000000..90a156e --- /dev/null +++ b/libs/common/src/config/super.admin.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'super-admin', + (): Record => ({ + SUPER_ADMIN_EMAIL: process.env.SUPER_ADMIN_EMAIL, + SUPER_ADMIN_PASSWORD: process.env.SUPER_ADMIN_PASSWORD, + }), +); diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index 826883a..f5fc400 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -1,11 +1,30 @@ import { Global, Module } from '@nestjs/common'; import { HelperHashService } from './services'; +import { UserRepository } from '../modules/user/repositories'; +import { UserRepositoryModule } from '../modules/user/user.repository.module'; +import { UserRoleRepository } from '../modules/user-role/repositories'; +import { UserRoleRepositoryModule } from '../modules/user-role/user.role.repository.module'; +import { RoleTypeRepository } from '../modules/role-type/repositories'; +import { RoleTypeRepositoryModule } from '../modules/role-type/role.type.repository.module'; +import { ConfigModule } from '@nestjs/config'; +import { SuperAdminService } from './services/super.admin.sarvice'; @Global() @Module({ - providers: [HelperHashService], - exports: [HelperHashService], + providers: [ + HelperHashService, + SuperAdminService, + UserRepository, + UserRoleRepository, + RoleTypeRepository, + ], + exports: [HelperHashService, SuperAdminService], controllers: [], - imports: [], + imports: [ + ConfigModule.forRoot(), + UserRepositoryModule, + UserRoleRepositoryModule, + RoleTypeRepositoryModule, + ], }) export class HelperModule {} diff --git a/libs/common/src/helper/services/super.admin.sarvice.ts b/libs/common/src/helper/services/super.admin.sarvice.ts new file mode 100644 index 0000000..e9efbce --- /dev/null +++ b/libs/common/src/helper/services/super.admin.sarvice.ts @@ -0,0 +1,72 @@ +import { HelperHashService } from './helper.hash.service'; +import { Injectable } from '@nestjs/common'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { RoleType } from '@app/common/constants/role.type.enum'; +import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class SuperAdminService { + constructor( + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + private readonly userRoleRepository: UserRoleRepository, + private readonly roleTypeRepository: RoleTypeRepository, + private readonly helperHashService: HelperHashService, + ) {} + + async createSuperAdminIfNotFound(): Promise { + try { + const superAdminData = await this.userRoleRepository.find({ + where: { roleType: { type: RoleType.SUPER_ADMIN } }, + relations: ['roleType'], + }); + + if (superAdminData.length <= 0) { + // Create the super admin user if not found + console.log('Creating super admin user...'); + + await this.createSuperAdmin(); + } + } catch (err) { + console.error('Error while checking super admin:', err); + throw err; + } + } + private async getRoleUuidByRoleType(roleType: string) { + const role = await this.roleTypeRepository.findOne({ + where: { type: roleType }, + }); + + return role.uuid; + } + private async createSuperAdmin(): Promise { + const salt = this.helperHashService.randomSalt(10); // Hash the password using bcrypt + const hashedPassword = await this.helperHashService.bcrypt( + this.configService.get('super-admin.SUPER_ADMIN_PASSWORD'), + salt, + ); + try { + const user = await this.userRepository.save({ + email: this.configService.get('super-admin.SUPER_ADMIN_EMAIL'), + password: hashedPassword, + firstName: 'Super', + lastName: 'Admin', + isUserVerified: true, + isActive: true, + }); + const defaultUserRoleUuid = await this.getRoleUuidByRoleType( + RoleType.SUPER_ADMIN, + ); + + await this.userRoleRepository.save({ + user: { uuid: user.uuid }, + roleType: { uuid: defaultUserRoleUuid }, + }); + } catch (err) { + console.error('Error while creating super admin:', err); + throw err; + } + } +} diff --git a/src/main.ts b/src/main.ts index 129f319..61dfb92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { SuperAdminService } from '@app/common/helper/services/super.admin.sarvice'; async function bootstrap() { const app = await NestFactory.create(AuthModule); @@ -33,6 +34,9 @@ async function bootstrap() { }, }), ); + // Create super admin user + const superAdminService = app.get(SuperAdminService); + await superAdminService.createSuperAdminIfNotFound(); await app.listen(process.env.PORT || 4000); } From e7024a5cb86fd773d904306902046b8f0adee148 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 11 May 2024 21:02:35 +0300 Subject: [PATCH 193/259] Add Unique constraint to 'type' column in RoleTypeEntity --- libs/common/src/modules/role-type/entities/role.type.entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index c65db72..0621b7d 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -1,10 +1,11 @@ -import { Column, Entity, OneToMany } from 'typeorm'; +import { Column, Entity, OneToMany, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { RoleTypeDto } from '../dtos/role.type.dto'; import { RoleType } from '@app/common/constants/role.type.enum'; import { UserRoleEntity } from '../../user-role/entities'; @Entity({ name: 'role-type' }) +@Unique(['type']) export class RoleTypeEntity extends AbstractEntity { @Column({ nullable: false, From ea19361a5995a0009e3c30ce37bcec18bb7ca1a1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 11 May 2024 21:19:53 +0300 Subject: [PATCH 194/259] Update role guards to differentiate between admin and super admin roles --- src/auth/controllers/user-auth.controller.ts | 6 +++--- src/guards/admin.role.guard.ts | 8 ++++---- src/guards/super.admin.role.guard.ts | 21 ++++++++++++++++++++ src/guards/user.role.guard.ts | 12 +++++------ src/role/controllers/role.controller.ts | 6 +++--- src/role/dtos/role.edit.dto.ts | 9 ++++++--- 6 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 src/guards/super.admin.role.guard.ts diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index fd139f9..86f9ce6 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -16,7 +16,7 @@ import { ResponseMessage } from '../../../libs/common/src/response/response.deco import { UserLoginDto } from '../dtos/user-login.dto'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; @Controller({ version: '1', @@ -52,7 +52,7 @@ export class UserAuthController { } @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(SuperAdminRoleGuard) @Delete('user/delete/:id') async userDelete(@Param('id') id: string) { await this.userAuthService.deleteUser(id); @@ -98,7 +98,7 @@ export class UserAuthController { } @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(SuperAdminRoleGuard) @Get('user/list') async userList() { const userList = await this.userAuthService.userList(); diff --git a/src/guards/admin.role.guard.ts b/src/guards/admin.role.guard.ts index fc26067..f7d64a0 100644 --- a/src/guards/admin.role.guard.ts +++ b/src/guards/admin.role.guard.ts @@ -4,13 +4,13 @@ import { AuthGuard } from '@nestjs/passport'; export class AdminRoleGuard extends AuthGuard('jwt') { handleRequest(err, user) { - const isAdmin = user.roles.some( - (role) => - role.type === RoleType.SUPER_ADMIN || role.type === RoleType.ADMIN, - ); if (err || !user) { throw err || new UnauthorizedException(); } else { + const isAdmin = user.roles.some( + (role) => + role.type === RoleType.SUPER_ADMIN || role.type === RoleType.ADMIN, + ); if (!isAdmin) { throw new BadRequestException('Only admin role can access this route'); } diff --git a/src/guards/super.admin.role.guard.ts b/src/guards/super.admin.role.guard.ts new file mode 100644 index 0000000..ef93a75 --- /dev/null +++ b/src/guards/super.admin.role.guard.ts @@ -0,0 +1,21 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +export class SuperAdminRoleGuard extends AuthGuard('jwt') { + handleRequest(err, user) { + if (err || !user) { + throw err || new UnauthorizedException(); + } else { + const isSuperAdmin = user.roles.some( + (role) => role.type === RoleType.SUPER_ADMIN, + ); + if (!isSuperAdmin) { + throw new BadRequestException( + 'Only super admin role can access this route', + ); + } + } + return user; + } +} diff --git a/src/guards/user.role.guard.ts b/src/guards/user.role.guard.ts index 4864fe0..59abad8 100644 --- a/src/guards/user.role.guard.ts +++ b/src/guards/user.role.guard.ts @@ -4,15 +4,15 @@ import { AuthGuard } from '@nestjs/passport'; export class UserRoleGuard extends AuthGuard('jwt') { handleRequest(err, user) { - const isUserOrAdmin = user.roles.some( - (role) => - role.type === RoleType.SUPER_ADMIN || - role.type === RoleType.ADMIN || - role.type === RoleType.USER, - ); if (err || !user) { throw err || new UnauthorizedException(); } else { + const isUserOrAdmin = user.roles.some( + (role) => + role.type === RoleType.SUPER_ADMIN || + role.type === RoleType.ADMIN || + role.type === RoleType.USER, + ); if (!isUserOrAdmin) { throw new BadRequestException( 'Only admin or user role can access this route', diff --git a/src/role/controllers/role.controller.ts b/src/role/controllers/role.controller.ts index ebabe93..2a6c917 100644 --- a/src/role/controllers/role.controller.ts +++ b/src/role/controllers/role.controller.ts @@ -11,7 +11,7 @@ import { import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { RoleService } from '../services/role.service'; import { UserRoleEditDto } from '../dtos'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; @ApiTags('Role Module') @Controller({ @@ -21,7 +21,7 @@ import { AdminRoleGuard } from 'src/guards/admin.role.guard'; export class RoleController { constructor(private readonly roleService: RoleService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(SuperAdminRoleGuard) @Get('types') async fetchRoleTypes() { try { @@ -36,7 +36,7 @@ export class RoleController { } } @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(SuperAdminRoleGuard) @Put('edit/user/:userUuid') async editUserRoleType( @Param('userUuid') userUuid: string, diff --git a/src/role/dtos/role.edit.dto.ts b/src/role/dtos/role.edit.dto.ts index 5cd0aac..9e9b394 100644 --- a/src/role/dtos/role.edit.dto.ts +++ b/src/role/dtos/role.edit.dto.ts @@ -1,13 +1,16 @@ import { RoleType } from '@app/common/constants/role.type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; +import { IsEnum, IsIn } from 'class-validator'; export class UserRoleEditDto { @ApiProperty({ - description: 'role type', - enum: RoleType, + description: 'Role type (USER or ADMIN)', + enum: [RoleType.USER, RoleType.ADMIN], required: true, }) @IsEnum(RoleType) + @IsIn([RoleType.USER, RoleType.ADMIN], { + message: 'roleType must be one of the following values: USER, ADMIN', + }) roleType: RoleType; } From 6b881ce01e3528b0746b9a3623114ba2f664556d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 11 May 2024 22:25:43 +0300 Subject: [PATCH 195/259] Add ProductType enum and implement getDevicesInGetaway method --- .../constants/permission-type.enum copy.ts | 8 ++ src/device/controllers/device.controller.ts | 13 +++ src/device/services/device.service.ts | 91 ++++++++++++++----- 3 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 libs/common/src/constants/permission-type.enum copy.ts diff --git a/libs/common/src/constants/permission-type.enum copy.ts b/libs/common/src/constants/permission-type.enum copy.ts new file mode 100644 index 0000000..f0d24ad --- /dev/null +++ b/libs/common/src/constants/permission-type.enum copy.ts @@ -0,0 +1,8 @@ +export enum ProductType { + AC = 'AC', + GW = 'GW', + CPS = 'CPS', + DL = 'DL', + WPS = 'WPS', + TH_G = '3G', +} diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index a3bbadd..986f23a 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -150,4 +150,17 @@ export class DeviceController { ); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('getaway/:gatewayUuid/devices') + async getDevicesInGetaway(@Param('gatewayUuid') gatewayUuid: string) { + try { + return await this.deviceService.getDevicesInGetaway(gatewayUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 39bc0ce..310c2e6 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -26,6 +26,7 @@ import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { ProductType } from '@app/common/constants/permission-type.enum copy'; @Injectable() export class DeviceService { @@ -160,11 +161,9 @@ export class DeviceService { async controlDevice(controlDeviceDto: ControlDeviceDto) { try { - const deviceDetails = await this.deviceRepository.findOne({ - where: { - uuid: controlDeviceDto.deviceUuid, - }, - }); + const deviceDetails = await this.getDeviceByDeviceUuid( + controlDeviceDto.deviceUuid, + ); if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new NotFoundException('Device Not Found'); @@ -213,11 +212,7 @@ export class DeviceService { async getDeviceDetailsByDeviceId(deviceUuid: string) { try { - const deviceDetails = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - }); + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); @@ -256,7 +251,7 @@ export class DeviceService { }); // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { productName, productId, ...rest } = camelCaseResponse.result; + const { productName, productId, id, ...rest } = camelCaseResponse.result; return { ...rest, productUuid: product.uuid, @@ -273,12 +268,7 @@ export class DeviceService { deviceUuid: string, ): Promise { try { - const deviceDetails = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - relations: ['productDevice'], - }); + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); @@ -323,12 +313,7 @@ export class DeviceService { } async getDevicesInstructionStatus(deviceUuid: string) { try { - const deviceDetails = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - relations: ['productDevice'], - }); + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); @@ -370,4 +355,64 @@ export class DeviceService { ); } } + async getDevicesInGetaway(gatewayUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(gatewayUuid); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } else if (deviceDetails.productDevice.prodType !== ProductType.GW) { + throw new NotFoundException('This is not a gateway device'); + } + + const response = await this.getDevicesInGetawayTuya( + deviceDetails.deviceTuyaUuid, + ); + + return { + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + device: response, + }; + } catch (error) { + throw new HttpException( + error.message || 'Device Not Found', + HttpStatus.NOT_FOUND, + ); + } + } + async getDevicesInGetawayTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.0/devices/${deviceId}/sub-devices`; + const response: any = await this.tuya.request({ + method: 'GET', + path, + }); + const camelCaseResponse = response.result.map((device: any) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { product_id, category, id, ...rest } = device; + const camelCaseDevice = convertKeysToCamelCase({ ...rest }); + return camelCaseDevice as GetDeviceDetailsInterface[]; + }); + + return camelCaseResponse; + } catch (error) { + throw new HttpException( + 'Error fetching device details from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async getDeviceByDeviceUuid(deviceUuid: string) { + return await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + relations: ['productDevice'], + }); + } } From cd40dc897be123305049d155b03c2de97b3fbf28 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 18 May 2024 20:23:17 +0300 Subject: [PATCH 196/259] Add endpoint to update device firmware --- ...type.enum copy.ts => product-type.enum.ts} | 0 src/device/controllers/device.controller.ts | 19 ++++++ src/device/interfaces/get.device.interface.ts | 5 ++ src/device/services/device.service.ts | 58 +++++++++++++++++-- 4 files changed, 78 insertions(+), 4 deletions(-) rename libs/common/src/constants/{permission-type.enum copy.ts => product-type.enum.ts} (100%) diff --git a/libs/common/src/constants/permission-type.enum copy.ts b/libs/common/src/constants/product-type.enum.ts similarity index 100% rename from libs/common/src/constants/permission-type.enum copy.ts rename to libs/common/src/constants/product-type.enum.ts diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 986f23a..e5ac98c 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -152,6 +152,25 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) + @Post(':deviceUuid/firmware/:firmwareVersion') + async updateDeviceFirmware( + @Param('deviceUuid') deviceUuid: string, + @Param('firmwareVersion') firmwareVersion: number, + ) { + try { + return await this.deviceService.updateDeviceFirmware( + deviceUuid, + firmwareVersion, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('getaway/:gatewayUuid/devices') async getDevicesInGetaway(@Param('gatewayUuid') gatewayUuid: string) { try { diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index f7012f7..526c199 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -59,3 +59,8 @@ export interface DeviceInstructionResponse { dataType: string; }[]; } +export interface updateDeviceFirmwareInterface { + success: boolean; + result: boolean; + msg: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 310c2e6..7302250 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -4,6 +4,7 @@ import { HttpException, HttpStatus, NotFoundException, + BadRequestException, } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; @@ -17,6 +18,7 @@ import { GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, controlDeviceInterface, + updateDeviceFirmwareInterface, } from '../interfaces/get.device.interface'; import { GetDeviceByGroupIdDto, @@ -26,7 +28,7 @@ import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; -import { ProductType } from '@app/common/constants/permission-type.enum copy'; +import { ProductType } from '@app/common/constants/product-type.enum'; @Injectable() export class DeviceService { @@ -163,6 +165,7 @@ export class DeviceService { try { const deviceDetails = await this.getDeviceByDeviceUuid( controlDeviceDto.deviceUuid, + false, ); if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { @@ -362,7 +365,7 @@ export class DeviceService { if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } else if (deviceDetails.productDevice.prodType !== ProductType.GW) { - throw new NotFoundException('This is not a gateway device'); + throw new BadRequestException('This is not a gateway device'); } const response = await this.getDevicesInGetawayTuya( @@ -407,12 +410,59 @@ export class DeviceService { } } - private async getDeviceByDeviceUuid(deviceUuid: string) { + private async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { return await this.deviceRepository.findOne({ where: { uuid: deviceUuid, }, - relations: ['productDevice'], + ...(withProductDevice && { relations: ['productDevice'] }), }); } + + async updateDeviceFirmware(deviceUuid: string, firmwareVersion: number) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new NotFoundException('Device Not Found'); + } + const response = await this.updateDeviceFirmwareTuya( + deviceDetails.deviceTuyaUuid, + firmwareVersion, + ); + + if (response.success) { + return response; + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + } + async updateDeviceFirmwareTuya( + deviceUuid: string, + firmwareVersion: number, + ): Promise { + try { + const path = `/v2.0/cloud/thing/${deviceUuid}/firmware/${firmwareVersion}`; + const response = await this.tuya.request({ + method: 'POST', + path, + }); + + return response as updateDeviceFirmwareInterface; + } catch (error) { + throw new HttpException( + 'Error updating device firmware from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From ad15164e15c8802a194adb8f462f271a3794b12d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 18 May 2024 21:53:27 +0300 Subject: [PATCH 197/259] feat: Add seeder module and services for all lookup tables --- libs/common/src/constants/space-type.enum.ts | 7 +++ libs/common/src/helper/helper.module.ts | 25 ++------- libs/common/src/seed/seeder.module.ts | 43 +++++++++++++++ .../seed/services/permission.type.seeder.ts | 49 +++++++++++++++++ .../src/seed/services/role.type.seeder.ts | 46 ++++++++++++++++ .../src/seed/services/seeder.service.ts | 21 ++++++++ .../src/seed/services/space.type.seeder.ts | 52 +++++++++++++++++++ .../services/supper.admin.seeder.ts} | 4 +- src/app.module.ts | 2 + src/main.ts | 12 +++-- 10 files changed, 233 insertions(+), 28 deletions(-) create mode 100644 libs/common/src/constants/space-type.enum.ts create mode 100644 libs/common/src/seed/seeder.module.ts create mode 100644 libs/common/src/seed/services/permission.type.seeder.ts create mode 100644 libs/common/src/seed/services/role.type.seeder.ts create mode 100644 libs/common/src/seed/services/seeder.service.ts create mode 100644 libs/common/src/seed/services/space.type.seeder.ts rename libs/common/src/{helper/services/super.admin.sarvice.ts => seed/services/supper.admin.seeder.ts} (96%) diff --git a/libs/common/src/constants/space-type.enum.ts b/libs/common/src/constants/space-type.enum.ts new file mode 100644 index 0000000..22688e7 --- /dev/null +++ b/libs/common/src/constants/space-type.enum.ts @@ -0,0 +1,7 @@ +export enum SpaceType { + COMMUNITY = 'community', + BUILDING = 'building', + FLOOR = 'floor', + UNIT = 'unit', + ROOM = 'room', +} diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index f5fc400..826883a 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -1,30 +1,11 @@ import { Global, Module } from '@nestjs/common'; import { HelperHashService } from './services'; -import { UserRepository } from '../modules/user/repositories'; -import { UserRepositoryModule } from '../modules/user/user.repository.module'; -import { UserRoleRepository } from '../modules/user-role/repositories'; -import { UserRoleRepositoryModule } from '../modules/user-role/user.role.repository.module'; -import { RoleTypeRepository } from '../modules/role-type/repositories'; -import { RoleTypeRepositoryModule } from '../modules/role-type/role.type.repository.module'; -import { ConfigModule } from '@nestjs/config'; -import { SuperAdminService } from './services/super.admin.sarvice'; @Global() @Module({ - providers: [ - HelperHashService, - SuperAdminService, - UserRepository, - UserRoleRepository, - RoleTypeRepository, - ], - exports: [HelperHashService, SuperAdminService], + providers: [HelperHashService], + exports: [HelperHashService], controllers: [], - imports: [ - ConfigModule.forRoot(), - UserRepositoryModule, - UserRoleRepositoryModule, - RoleTypeRepositoryModule, - ], + imports: [], }) export class HelperModule {} diff --git a/libs/common/src/seed/seeder.module.ts b/libs/common/src/seed/seeder.module.ts new file mode 100644 index 0000000..a6e2585 --- /dev/null +++ b/libs/common/src/seed/seeder.module.ts @@ -0,0 +1,43 @@ +import { Global, Module } from '@nestjs/common'; +import { SeederService } from './services/seeder.service'; +import { PermissionTypeRepository } from '../modules/permission/repositories'; +import { PermissionTypeSeeder } from './services/permission.type.seeder'; +import { PermissionTypeRepositoryModule } from '../modules/permission/permission.repository.module'; +import { ConfigModule } from '@nestjs/config'; +import { RoleTypeRepositoryModule } from '../modules/role-type/role.type.repository.module'; +import { RoleTypeRepository } from '../modules/role-type/repositories'; +import { RoleTypeSeeder } from './services/role.type.seeder'; +import { SpaceTypeRepository } from '../modules/space-type/repositories'; +import { SpaceTypeSeeder } from './services/space.type.seeder'; +import { SpaceTypeRepositoryModule } from '../modules/space-type/space.type.repository.module'; +import { SuperAdminSeeder } from './services/supper.admin.seeder'; +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'; +@Global() +@Module({ + providers: [ + PermissionTypeSeeder, + RoleTypeSeeder, + SpaceTypeSeeder, + SeederService, + PermissionTypeRepository, + RoleTypeRepository, + SpaceTypeRepository, + SuperAdminSeeder, + UserRepository, + UserRoleRepository, + ], + exports: [SeederService], + controllers: [], + imports: [ + ConfigModule.forRoot(), + PermissionTypeRepositoryModule, + RoleTypeRepositoryModule, + UserRepositoryModule, + UserRoleRepositoryModule, + SpaceTypeRepositoryModule, + ], +}) +export class SeederModule {} diff --git a/libs/common/src/seed/services/permission.type.seeder.ts b/libs/common/src/seed/services/permission.type.seeder.ts new file mode 100644 index 0000000..537bddb --- /dev/null +++ b/libs/common/src/seed/services/permission.type.seeder.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { PermissionTypeRepository } from '../../modules/permission/repositories'; +import { PermissionType } from '../../constants/permission-type.enum'; + +@Injectable() +export class PermissionTypeSeeder { + constructor( + private readonly permissionTypeRepository: PermissionTypeRepository, + ) {} + + async addPermissionTypeDataIfNotFound(): Promise { + try { + const existingPermissionTypes = + await this.permissionTypeRepository.find(); + + const permissionTypeNames = existingPermissionTypes.map((pt) => pt.type); + + const missingPermissionTypes = []; + if (!permissionTypeNames.includes(PermissionType.CONTROLLABLE)) { + missingPermissionTypes.push(PermissionType.CONTROLLABLE); + } + if (!permissionTypeNames.includes(PermissionType.READ)) { + missingPermissionTypes.push(PermissionType.READ); + } + + if (missingPermissionTypes.length > 0) { + await this.addPermissionTypeData(missingPermissionTypes); + } + } catch (err) { + console.error('Error while checking permission type data:', err); + throw err; + } + } + + private async addPermissionTypeData( + permissionTypes: string[], + ): Promise { + try { + const permissionTypeEntities = permissionTypes.map((type) => ({ + type, + })); + + await this.permissionTypeRepository.save(permissionTypeEntities); + } catch (err) { + console.error('Error while adding permission type data:', err); + throw err; + } + } +} diff --git a/libs/common/src/seed/services/role.type.seeder.ts b/libs/common/src/seed/services/role.type.seeder.ts new file mode 100644 index 0000000..ea8bf15 --- /dev/null +++ b/libs/common/src/seed/services/role.type.seeder.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { RoleType } from '../../constants/role.type.enum'; +import { RoleTypeRepository } from '../../modules/role-type/repositories'; + +@Injectable() +export class RoleTypeSeeder { + constructor(private readonly roleTypeRepository: RoleTypeRepository) {} + + async addRoleTypeDataIfNotFound(): Promise { + try { + const existingRoleTypes = await this.roleTypeRepository.find(); + + const roleTypeNames = existingRoleTypes.map((pt) => pt.type); + + const missingRoleTypes = []; + if (!roleTypeNames.includes(RoleType.SUPER_ADMIN)) { + missingRoleTypes.push(RoleType.SUPER_ADMIN); + } + if (!roleTypeNames.includes(RoleType.ADMIN)) { + missingRoleTypes.push(RoleType.ADMIN); + } + if (!roleTypeNames.includes(RoleType.USER)) { + missingRoleTypes.push(RoleType.USER); + } + if (missingRoleTypes.length > 0) { + await this.addRoleTypeData(missingRoleTypes); + } + } catch (err) { + console.error('Error while checking role type data:', err); + throw err; + } + } + + private async addRoleTypeData(roleTypes: string[]): Promise { + try { + const roleTypeEntities = roleTypes.map((type) => ({ + type, + })); + + await this.roleTypeRepository.save(roleTypeEntities); + } catch (err) { + console.error('Error while adding role type data:', err); + throw err; + } + } +} diff --git a/libs/common/src/seed/services/seeder.service.ts b/libs/common/src/seed/services/seeder.service.ts new file mode 100644 index 0000000..92b5f51 --- /dev/null +++ b/libs/common/src/seed/services/seeder.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { PermissionTypeSeeder } from './permission.type.seeder'; +import { RoleTypeSeeder } from './role.type.seeder'; +import { SpaceTypeSeeder } from './space.type.seeder'; +import { SuperAdminSeeder } from './supper.admin.seeder'; +@Injectable() +export class SeederService { + constructor( + private readonly permissionTypeSeeder: PermissionTypeSeeder, + private readonly roleTypeSeeder: RoleTypeSeeder, + private readonly spaceTypeSeeder: SpaceTypeSeeder, + private readonly superAdminSeeder: SuperAdminSeeder, + ) {} + + async seed() { + await this.permissionTypeSeeder.addPermissionTypeDataIfNotFound(); + await this.roleTypeSeeder.addRoleTypeDataIfNotFound(); + await this.spaceTypeSeeder.addSpaceTypeDataIfNotFound(); + await this.superAdminSeeder.createSuperAdminIfNotFound(); + } +} diff --git a/libs/common/src/seed/services/space.type.seeder.ts b/libs/common/src/seed/services/space.type.seeder.ts new file mode 100644 index 0000000..3cbcb87 --- /dev/null +++ b/libs/common/src/seed/services/space.type.seeder.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { SpaceType } from '../../constants/space-type.enum'; +import { SpaceTypeRepository } from '../../modules/space-type/repositories'; + +@Injectable() +export class SpaceTypeSeeder { + constructor(private readonly spaceTypeRepository: SpaceTypeRepository) {} + + async addSpaceTypeDataIfNotFound(): Promise { + try { + const existingSpaceTypes = await this.spaceTypeRepository.find(); + + const spaceTypeNames = existingSpaceTypes.map((pt) => pt.type); + + const missingSpaceTypes = []; + if (!spaceTypeNames.includes(SpaceType.COMMUNITY)) { + missingSpaceTypes.push(SpaceType.COMMUNITY); + } + if (!spaceTypeNames.includes(SpaceType.BUILDING)) { + missingSpaceTypes.push(SpaceType.BUILDING); + } + if (!spaceTypeNames.includes(SpaceType.FLOOR)) { + missingSpaceTypes.push(SpaceType.FLOOR); + } + if (!spaceTypeNames.includes(SpaceType.UNIT)) { + missingSpaceTypes.push(SpaceType.UNIT); + } + if (!spaceTypeNames.includes(SpaceType.ROOM)) { + missingSpaceTypes.push(SpaceType.ROOM); + } + if (missingSpaceTypes.length > 0) { + await this.addSpaceTypeData(missingSpaceTypes); + } + } catch (err) { + console.error('Error while checking space type data:', err); + throw err; + } + } + + private async addSpaceTypeData(spaceTypes: string[]): Promise { + try { + const spaceTypeEntities = spaceTypes.map((type) => ({ + type, + })); + + await this.spaceTypeRepository.save(spaceTypeEntities); + } catch (err) { + console.error('Error while adding space type data:', err); + throw err; + } + } +} diff --git a/libs/common/src/helper/services/super.admin.sarvice.ts b/libs/common/src/seed/services/supper.admin.seeder.ts similarity index 96% rename from libs/common/src/helper/services/super.admin.sarvice.ts rename to libs/common/src/seed/services/supper.admin.seeder.ts index e9efbce..6cb5f60 100644 --- a/libs/common/src/helper/services/super.admin.sarvice.ts +++ b/libs/common/src/seed/services/supper.admin.seeder.ts @@ -1,13 +1,13 @@ -import { HelperHashService } from './helper.hash.service'; import { Injectable } from '@nestjs/common'; import { UserRepository } from '@app/common/modules/user/repositories'; import { RoleType } from '@app/common/constants/role.type.enum'; import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; import { ConfigService } from '@nestjs/config'; +import { HelperHashService } from '../../helper/services'; @Injectable() -export class SuperAdminService { +export class SuperAdminSeeder { constructor( private readonly configService: ConfigService, private readonly userRepository: UserRepository, diff --git a/src/app.module.ts b/src/app.module.ts index c9f7275..6add0be 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { BuildingModule } from './building/building.module'; import { FloorModule } from './floor/floor.module'; import { UnitModule } from './unit/unit.module'; import { RoleModule } from './role/role.module'; +import { SeederModule } from '@app/common/seed/seeder.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -30,6 +31,7 @@ import { RoleModule } from './role/role.module'; GroupModule, DeviceModule, UserDevicePermissionModule, + SeederModule, ], controllers: [AuthenticationController], }) diff --git a/src/main.ts b/src/main.ts index 61dfb92..0253ad0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +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 { SuperAdminService } from '@app/common/helper/services/super.admin.sarvice'; +import { SeederService } from '@app/common/seed/services/seeder.service'; async function bootstrap() { const app = await NestFactory.create(AuthModule); @@ -34,10 +34,14 @@ async function bootstrap() { }, }), ); - // Create super admin user - const superAdminService = app.get(SuperAdminService); - await superAdminService.createSuperAdminIfNotFound(); + const seederService = app.get(SeederService); + try { + await seederService.seed(); + console.log('Seeding complete!'); + } catch (error) { + console.error('Seeding failed!', error); + } await app.listen(process.env.PORT || 4000); } console.log('Starting auth at port ...', process.env.PORT || 4000); From 1bc8fee061bd161b7828723ffb2aaea054ce9e5a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 18 May 2024 22:10:48 +0300 Subject: [PATCH 198/259] Remove onDelete and onUpdate properties from entity relationships --- libs/common/src/modules/device/entities/device.entity.ts | 2 -- libs/common/src/modules/group/entities/group.entity.ts | 1 - .../common/src/modules/permission/entities/permission.entity.ts | 2 -- libs/common/src/modules/role-type/entities/role.type.entity.ts | 2 -- libs/common/src/modules/user/entities/user.entity.ts | 2 -- 5 files changed, 9 deletions(-) diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index a7f5548..53dbbb3 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -25,8 +25,6 @@ export class DeviceEntity extends AbstractEntity { (permission) => permission.device, { nullable: true, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', }, ) permission: DeviceUserPermissionEntity[]; diff --git a/libs/common/src/modules/group/entities/group.entity.ts b/libs/common/src/modules/group/entities/group.entity.ts index 7cea8e8..525f84d 100644 --- a/libs/common/src/modules/group/entities/group.entity.ts +++ b/libs/common/src/modules/group/entities/group.entity.ts @@ -19,7 +19,6 @@ export class GroupEntity extends AbstractEntity { @OneToMany(() => GroupDeviceEntity, (groupDevice) => groupDevice.group, { cascade: true, - onDelete: 'CASCADE', }) groupDevices: GroupDeviceEntity[]; diff --git a/libs/common/src/modules/permission/entities/permission.entity.ts b/libs/common/src/modules/permission/entities/permission.entity.ts index 3ee3943..d15d936 100644 --- a/libs/common/src/modules/permission/entities/permission.entity.ts +++ b/libs/common/src/modules/permission/entities/permission.entity.ts @@ -17,8 +17,6 @@ export class PermissionTypeEntity extends AbstractEntity { (permission) => permission.permissionType, { nullable: true, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', }, ) permission: DeviceUserPermissionEntity[]; diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index 0621b7d..1697327 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -14,8 +14,6 @@ export class RoleTypeEntity extends AbstractEntity { type: string; @OneToMany(() => UserRoleEntity, (role) => role.roleType, { nullable: true, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', }) role: UserRoleEntity[]; constructor(partial: Partial) { diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index ffe402c..ddf487b 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -61,8 +61,6 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserRoleEntity, (role) => role.user, { nullable: true, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', }) role: UserRoleEntity[]; constructor(partial: Partial) { From 6415d789923f3fb68c1be204e70e4cec71361166 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 18 May 2024 23:00:22 +0300 Subject: [PATCH 199/259] Remove USER role --- .../src/auth/interfaces/auth.interface.ts | 2 +- libs/common/src/auth/services/auth.service.ts | 4 +-- .../src/auth/strategies/jwt.strategy.ts | 2 +- .../auth/strategies/refresh-token.strategy.ts | 2 +- libs/common/src/constants/role.type.enum.ts | 1 - .../role-type/entities/role.type.entity.ts | 2 +- .../user-role/entities/user.role.entity.ts | 4 +-- .../src/modules/user/entities/user.entity.ts | 2 +- .../src/seed/services/role.type.seeder.ts | 4 +-- src/auth/services/user-auth.service.ts | 18 +---------- .../controllers/building.controller.ts | 12 +++---- .../controllers/community.controller.ts | 10 +++--- src/device/controllers/device.controller.ts | 18 +++++------ src/floor/controllers/floor.controller.ts | 12 +++---- src/group/controllers/group.controller.ts | 14 ++++---- src/guards/user.role.guard.ts | 24 -------------- src/role/controllers/role.controller.ts | 16 ++++------ src/role/dtos/index.ts | 2 +- src/role/dtos/role.add.dto.ts | 24 ++++++++++++++ src/role/dtos/role.edit.dto.ts | 16 ---------- src/role/services/role.service.ts | 32 ++++++++++++------- src/room/controllers/room.controller.ts | 10 +++--- src/unit/controllers/unit.controller.ts | 12 +++---- 23 files changed, 107 insertions(+), 136 deletions(-) delete mode 100644 src/guards/user.role.guard.ts create mode 100644 src/role/dtos/role.add.dto.ts delete mode 100644 src/role/dtos/role.edit.dto.ts diff --git a/libs/common/src/auth/interfaces/auth.interface.ts b/libs/common/src/auth/interfaces/auth.interface.ts index 8e57cd6..9b73050 100644 --- a/libs/common/src/auth/interfaces/auth.interface.ts +++ b/libs/common/src/auth/interfaces/auth.interface.ts @@ -4,5 +4,5 @@ export class AuthInterface { uuid: string; sessionId: string; id: number; - roles: string[]; + roles?: string[]; } diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 45b19fa..f1c1c0d 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -22,7 +22,7 @@ export class AuthService { where: { email, }, - relations: ['role.roleType'], + relations: ['roles.roleType'], }); if (!user.isUserVerified) { @@ -70,7 +70,7 @@ export class AuthService { uuid: user.uuid, type: user.type, sessionId: user.sessionId, - roles: user.roles, + roles: user?.roles, }; const tokens = await this.getTokens(payload); diff --git a/libs/common/src/auth/strategies/jwt.strategy.ts b/libs/common/src/auth/strategies/jwt.strategy.ts index ed29a3a..d548dd8 100644 --- a/libs/common/src/auth/strategies/jwt.strategy.ts +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { userUuid: payload.uuid, uuid: payload.uuid, sessionId: payload.sessionId, - roles: payload.roles, + roles: payload?.roles, }; } else { throw new BadRequestException('Unauthorized'); diff --git a/libs/common/src/auth/strategies/refresh-token.strategy.ts b/libs/common/src/auth/strategies/refresh-token.strategy.ts index ee36eac..b6d6c2a 100644 --- a/libs/common/src/auth/strategies/refresh-token.strategy.ts +++ b/libs/common/src/auth/strategies/refresh-token.strategy.ts @@ -34,7 +34,7 @@ export class RefreshTokenStrategy extends PassportStrategy( userUuid: payload.uuid, uuid: payload.uuid, sessionId: payload.sessionId, - roles: payload.roles, + roles: payload?.roles, }; } else { throw new BadRequestException('Unauthorized'); diff --git a/libs/common/src/constants/role.type.enum.ts b/libs/common/src/constants/role.type.enum.ts index 72bf14e..3051b04 100644 --- a/libs/common/src/constants/role.type.enum.ts +++ b/libs/common/src/constants/role.type.enum.ts @@ -1,5 +1,4 @@ export enum RoleType { SUPER_ADMIN = 'SUPER_ADMIN', ADMIN = 'ADMIN', - USER = 'USER', } diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index 1697327..fd332fb 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -15,7 +15,7 @@ export class RoleTypeEntity extends AbstractEntity { @OneToMany(() => UserRoleEntity, (role) => role.roleType, { nullable: true, }) - role: UserRoleEntity[]; + roles: UserRoleEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/user-role/entities/user.role.entity.ts b/libs/common/src/modules/user-role/entities/user.role.entity.ts index 34b49c4..c733594 100644 --- a/libs/common/src/modules/user-role/entities/user.role.entity.ts +++ b/libs/common/src/modules/user-role/entities/user.role.entity.ts @@ -7,12 +7,12 @@ import { RoleTypeEntity } from '../../role-type/entities'; @Entity({ name: 'user-role' }) @Unique(['user', 'roleType']) export class UserRoleEntity extends AbstractEntity { - @ManyToOne(() => UserEntity, (user) => user.role, { + @ManyToOne(() => UserEntity, (user) => user.roles, { nullable: false, }) user: UserEntity; - @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.role, { + @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.roles, { nullable: false, }) roleType: RoleTypeEntity; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index ddf487b..17a9268 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -62,7 +62,7 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserRoleEntity, (role) => role.user, { nullable: true, }) - role: UserRoleEntity[]; + roles: UserRoleEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/seed/services/role.type.seeder.ts b/libs/common/src/seed/services/role.type.seeder.ts index ea8bf15..5f7a4b2 100644 --- a/libs/common/src/seed/services/role.type.seeder.ts +++ b/libs/common/src/seed/services/role.type.seeder.ts @@ -19,9 +19,7 @@ export class RoleTypeSeeder { if (!roleTypeNames.includes(RoleType.ADMIN)) { missingRoleTypes.push(RoleType.ADMIN); } - if (!roleTypeNames.includes(RoleType.USER)) { - missingRoleTypes.push(RoleType.USER); - } + if (missingRoleTypes.length > 0) { await this.addRoleTypeData(missingRoleTypes); } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 705ba9e..e74967d 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -50,15 +50,6 @@ export class UserAuthService { password: hashedPassword, }); - const defaultUserRoleUuid = await this.getRoleUuidByRoleType( - RoleType.USER, - ); - - await this.userRoleRepository.save({ - user: { uuid: user.uuid }, - roleType: { uuid: defaultUserRoleUuid }, - }); - return user; } catch (error) { throw new BadRequestException('Failed to register user'); @@ -114,7 +105,7 @@ export class UserAuthService { email: user.email, userId: user.uuid, uuid: user.uuid, - roles: user.role.map((role) => { + roles: user?.roles?.map((role) => { return { uuid: role.uuid, type: role.roleType.type }; }), sessionId: session[1].uuid, @@ -214,11 +205,4 @@ export class UserAuthService { await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken); return tokens; } - private async getRoleUuidByRoleType(roleType: string) { - const role = await this.roleTypeRepository.findOne({ - where: { type: roleType }, - }); - - return role.uuid; - } } diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index b561ff9..220b96e 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -18,7 +18,7 @@ import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; import { CheckUserBuildingGuard } from 'src/guards/user.building.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Building Module') @Controller({ @@ -44,7 +44,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':buildingUuid') async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -60,7 +60,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('child/:buildingUuid') async getBuildingChildByUuid( @Param('buildingUuid') buildingUuid: string, @@ -80,7 +80,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('parent/:buildingUuid') async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { try { @@ -109,7 +109,7 @@ export class BuildingController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getBuildingsByUserId(@Param('userUuid') userUuid: string) { try { @@ -123,7 +123,7 @@ export class BuildingController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:buildingUuid') async renameBuildingByUuid( @Param('buildingUuid') buildingUuid: string, diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 144041d..c3d2739 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -20,7 +20,7 @@ import { GetCommunityChildDto } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Community Module') @Controller({ @@ -47,7 +47,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':communityUuid') async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { try { @@ -63,7 +63,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('child/:communityUuid') async getCommunityChildByUuid( @Param('communityUuid') communityUuid: string, @@ -84,7 +84,7 @@ export class CommunityController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { try { @@ -111,7 +111,7 @@ export class CommunityController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:communityUuid') async renameCommunityByUuid( @Param('communityUuid') communityUuid: string, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 7ebf1ca..e014c0c 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -25,7 +25,7 @@ import { CheckRoomGuard } from 'src/guards/room.guard'; import { CheckGroupGuard } from 'src/guards/group.guard'; import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Device Module') @Controller({ @@ -36,7 +36,7 @@ export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckRoomGuard) + @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') async getDevicesByRoomId( @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, @@ -57,7 +57,7 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckRoomGuard) + @UseGuards(JwtAuthGuard, CheckRoomGuard) @Post('room') async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { try { @@ -70,7 +70,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckGroupGuard) + @UseGuards(JwtAuthGuard, CheckGroupGuard) @Get('group') async getDevicesByGroupId( @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, @@ -90,7 +90,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckGroupGuard) + @UseGuards(JwtAuthGuard, CheckGroupGuard) @Post('group') async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { try { @@ -103,7 +103,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid') async getDeviceDetailsByDeviceId( @Param('deviceUuid') deviceUuid: string, @@ -123,7 +123,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid/functions') async getDeviceInstructionByDeviceId( @Param('deviceUuid') deviceUuid: string, @@ -140,7 +140,7 @@ export class DeviceController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid/functions/status') async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) { try { @@ -154,7 +154,7 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckUserHaveControllablePermission) + @UseGuards(JwtAuthGuard, CheckUserHaveControllablePermission) @Post(':deviceUuid/control') async controlDevice( @Body() controlDeviceDto: ControlDeviceDto, diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 2fd9dfc..ea27429 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -17,7 +17,7 @@ import { GetFloorChildDto } from '../dtos/get.floor.dto'; import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; import { CheckUserFloorGuard } from 'src/guards/user.floor.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; @ApiTags('Floor Module') @@ -44,7 +44,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':floorUuid') async getFloorByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -59,7 +59,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('child/:floorUuid') async getFloorChildByUuid( @Param('floorUuid') floorUuid: string, @@ -79,7 +79,7 @@ export class FloorController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('parent/:floorUuid') async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { try { @@ -109,7 +109,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getFloorsByUserId(@Param('userUuid') userUuid: string) { try { @@ -123,7 +123,7 @@ export class FloorController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:floorUuid') async renameFloorByUuid( @Param('floorUuid') floorUuid: string, diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index c00ff92..e936e06 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -16,7 +16,7 @@ import { AddGroupDto } from '../dtos/add.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Group Module') @Controller({ @@ -27,7 +27,7 @@ export class GroupController { constructor(private readonly groupService: GroupService) {} @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('space/:spaceUuid') async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { try { @@ -40,7 +40,7 @@ export class GroupController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':groupUuid') async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { try { @@ -53,7 +53,7 @@ export class GroupController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard, CheckProductUuidForAllDevicesGuard) + @UseGuards(JwtAuthGuard, CheckProductUuidForAllDevicesGuard) @Post() async addGroup(@Body() addGroupDto: AddGroupDto) { try { @@ -67,7 +67,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Post('control') async controlGroup(@Body() controlGroupDto: ControlGroupDto) { try { @@ -81,7 +81,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:groupUuid') async renameGroupByUuid( @Param('groupUuid') groupUuid: string, @@ -101,7 +101,7 @@ export class GroupController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Delete(':groupUuid') async deleteGroup(@Param('groupUuid') groupUuid: string) { try { diff --git a/src/guards/user.role.guard.ts b/src/guards/user.role.guard.ts deleted file mode 100644 index 59abad8..0000000 --- a/src/guards/user.role.guard.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RoleType } from '@app/common/constants/role.type.enum'; -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -export class UserRoleGuard extends AuthGuard('jwt') { - handleRequest(err, user) { - if (err || !user) { - throw err || new UnauthorizedException(); - } else { - const isUserOrAdmin = user.roles.some( - (role) => - role.type === RoleType.SUPER_ADMIN || - role.type === RoleType.ADMIN || - role.type === RoleType.USER, - ); - if (!isUserOrAdmin) { - throw new BadRequestException( - 'Only admin or user role can access this route', - ); - } - } - return user; - } -} diff --git a/src/role/controllers/role.controller.ts b/src/role/controllers/role.controller.ts index 2a6c917..cb2038d 100644 --- a/src/role/controllers/role.controller.ts +++ b/src/role/controllers/role.controller.ts @@ -4,13 +4,12 @@ import { Get, HttpException, HttpStatus, - Param, - Put, + Post, UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { RoleService } from '../services/role.service'; -import { UserRoleEditDto } from '../dtos'; +import { AddUserRoleDto } from '../dtos'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; @ApiTags('Role Module') @@ -37,16 +36,13 @@ export class RoleController { } @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard) - @Put('edit/user/:userUuid') - async editUserRoleType( - @Param('userUuid') userUuid: string, - @Body() userRoleEditDto: UserRoleEditDto, - ) { + @Post() + async addUserRoleType(@Body() addUserRoleDto: AddUserRoleDto) { try { - await this.roleService.editUserRoleType(userUuid, userRoleEditDto); + await this.roleService.addUserRoleType(addUserRoleDto); return { statusCode: HttpStatus.OK, - message: 'User Role Updated Successfully', + message: 'User Role Added Successfully', }; } catch (error) { throw new HttpException( diff --git a/src/role/dtos/index.ts b/src/role/dtos/index.ts index 8fc5216..a9b9771 100644 --- a/src/role/dtos/index.ts +++ b/src/role/dtos/index.ts @@ -1 +1 @@ -export * from './role.edit.dto'; +export * from './role.add.dto'; diff --git a/src/role/dtos/role.add.dto.ts b/src/role/dtos/role.add.dto.ts new file mode 100644 index 0000000..8f17920 --- /dev/null +++ b/src/role/dtos/role.add.dto.ts @@ -0,0 +1,24 @@ +import { RoleType } from '@app/common/constants/role.type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator'; + +export class AddUserRoleDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + + @ApiProperty({ + description: 'Role type (ADMIN)', + enum: [RoleType.ADMIN], + required: true, + }) + @IsEnum(RoleType) + @IsIn([RoleType.ADMIN], { + message: 'roleType must be one of the following values: ADMIN', + }) + roleType: RoleType; +} diff --git a/src/role/dtos/role.edit.dto.ts b/src/role/dtos/role.edit.dto.ts deleted file mode 100644 index 9e9b394..0000000 --- a/src/role/dtos/role.edit.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { RoleType } from '@app/common/constants/role.type.enum'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsIn } from 'class-validator'; - -export class UserRoleEditDto { - @ApiProperty({ - description: 'Role type (USER or ADMIN)', - enum: [RoleType.USER, RoleType.ADMIN], - required: true, - }) - @IsEnum(RoleType) - @IsIn([RoleType.USER, RoleType.ADMIN], { - message: 'roleType must be one of the following values: USER, ADMIN', - }) - roleType: RoleType; -} diff --git a/src/role/services/role.service.ts b/src/role/services/role.service.ts index b8db739..ece3780 100644 --- a/src/role/services/role.service.ts +++ b/src/role/services/role.service.ts @@ -1,7 +1,8 @@ import { RoleTypeRepository } from './../../../libs/common/src/modules/role-type/repositories/role.type.repository'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { UserRoleEditDto } from '../dtos/role.edit.dto'; +import { AddUserRoleDto } from '../dtos/role.add.dto'; import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; +import { QueryFailedError } from 'typeorm'; @Injectable() export class RoleService { @@ -10,18 +11,27 @@ export class RoleService { private readonly userRoleRepository: UserRoleRepository, ) {} - async editUserRoleType(userUuid: string, userRoleEditDto: UserRoleEditDto) { + async addUserRoleType(addUserRoleDto: AddUserRoleDto) { try { - const roleType = await this.fetchRoleByType(userRoleEditDto.roleType); - return await this.userRoleRepository.update( - { user: { uuid: userUuid } }, - { - roleType: { - uuid: roleType.uuid, - }, - }, - ); + const roleType = await this.fetchRoleByType(addUserRoleDto.roleType); + + if (roleType.uuid) { + return await this.userRoleRepository.save({ + user: { uuid: addUserRoleDto.userUuid }, + roleType: { uuid: roleType.uuid }, + }); + } } catch (error) { + if ( + error instanceof QueryFailedError && + error.driverError.code === '23505' + ) { + // Postgres unique constraint violation error code + throw new HttpException( + 'This role already exists for this user', + HttpStatus.CONFLICT, + ); + } throw new HttpException( error.message || 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR, diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index d39b584..984e8a5 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -16,7 +16,7 @@ import { UpdateRoomNameDto } from '../dtos/update.room.dto'; import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; import { CheckUserRoomGuard } from 'src/guards/user.room.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Room Module') @Controller({ @@ -42,7 +42,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':roomUuid') async getRoomByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -57,7 +57,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('parent/:roomUuid') async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { try { @@ -85,7 +85,7 @@ export class RoomController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getRoomsByUserId(@Param('userUuid') userUuid: string) { try { @@ -99,7 +99,7 @@ export class RoomController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:roomUuid') async renameRoomByUuid( @Param('roomUuid') roomUuid: string, diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index c42793c..885b018 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -18,7 +18,7 @@ import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { UserRoleGuard } from 'src/guards/user.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Unit Module') @Controller({ @@ -44,7 +44,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get(':unitUuid') async getUnitByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -59,7 +59,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('child/:unitUuid') async getUnitChildByUuid( @Param('unitUuid') unitUuid: string, @@ -76,7 +76,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('parent/:unitUuid') async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { try { @@ -104,7 +104,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getUnitsByUserId(@Param('userUuid') userUuid: string) { try { @@ -118,7 +118,7 @@ export class UnitController { } @ApiBearerAuth() - @UseGuards(UserRoleGuard) + @UseGuards(JwtAuthGuard) @Put('rename/:unitUuid') async renameUnitByUuid( @Param('unitUuid') unitUuid: string, From cab86dbd69e7923264e84dc0d8a6a0cc6e4044f2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 18 May 2024 23:10:29 +0300 Subject: [PATCH 200/259] Remove unused import of RoleType enum in UserAuthService --- src/auth/services/user-auth.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index e74967d..a514c1c 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -18,7 +18,6 @@ import { EmailService } from '../../../libs/common/src/util/email.service'; import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; import * as argon2 from 'argon2'; -import { RoleType } from '@app/common/constants/role.type.enum'; @Injectable() export class UserAuthService { From 835fde8304858c840c63822f70c96553496cfd1b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 19 May 2024 14:25:19 +0300 Subject: [PATCH 201/259] feat: Add success response with data in controllers --- src/building/controllers/building.controller.ts | 13 +++++++++++-- src/community/controllers/community.controller.ts | 13 +++++++++++-- src/device/controllers/device.controller.ts | 10 +++++++++- src/floor/controllers/floor.controller.ts | 13 +++++++++++-- src/room/controllers/room.controller.ts | 13 +++++++++++-- src/unit/controllers/unit.controller.ts | 13 +++++++++++-- 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 9843c8e..62c846f 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -34,7 +34,12 @@ export class BuildingController { async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { const building = await this.buildingService.addBuilding(addBuildingDto); - return { message: 'Building added successfully', uuid: building.uuid }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Building added successfully', + data: building, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -100,7 +105,11 @@ export class BuildingController { async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) { try { await this.buildingService.addUserBuilding(addUserBuildingDto); - return { message: 'user building added successfully' }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user building added successfully', + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 4a80015..14cf37e 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -37,7 +37,12 @@ export class CommunityController { try { const community = await this.communityService.addCommunity(addCommunityDto); - return { message: 'Community added successfully', uuid: community.uuid }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Community added successfully', + data: community, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -102,7 +107,11 @@ export class CommunityController { async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { try { await this.communityService.addUserCommunity(addUserCommunityDto); - return { message: 'user community added successfully' }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user community added successfully', + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index d8149a7..72321ef 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -61,7 +61,15 @@ export class DeviceController { @Post('room') async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { try { - return await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + const device = + await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device added in room successfully', + data: device, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 304d4a2..1216018 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -34,7 +34,12 @@ export class FloorController { async addFloor(@Body() addFloorDto: AddFloorDto) { try { const floor = await this.floorService.addFloor(addFloorDto); - return { message: 'Floor added successfully', uuid: floor.uuid }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Floor added successfully', + data: floor, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -99,7 +104,11 @@ export class FloorController { async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) { try { await this.floorService.addUserFloor(addUserFloorDto); - return { message: 'user floor added successfully' }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user floor added successfully', + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index f59300b..50d80e5 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -32,7 +32,12 @@ export class RoomController { async addRoom(@Body() addRoomDto: AddRoomDto) { try { const room = await this.roomService.addRoom(addRoomDto); - return { message: 'Room added successfully', uuid: room.uuid }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Room added successfully', + data: room, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -76,7 +81,11 @@ export class RoomController { async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) { try { await this.roomService.addUserRoom(addUserRoomDto); - return { message: 'user room added successfully' }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user room added successfully', + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 89ca053..fa48784 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -34,7 +34,12 @@ export class UnitController { async addUnit(@Body() addUnitDto: AddUnitDto) { try { const unit = await this.unitService.addUnit(addUnitDto); - return { message: 'Unit added successfully', uuid: unit.uuid }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Unit added successfully', + data: unit, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -95,7 +100,11 @@ export class UnitController { async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { try { await this.unitService.addUserUnit(addUserUnitDto); - return { message: 'user unit added successfully' }; + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user unit added successfully', + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', From 4ddc379bf7b3a9cd92d6418ca5b5a4ab8fc212c5 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 19 May 2024 14:25:29 +0300 Subject: [PATCH 202/259] Add support for multiple permission types in DeviceService --- src/device/services/device.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 29bddca..5f59d59 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -27,6 +27,7 @@ import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { In } from 'typeorm'; @Injectable() export class DeviceService { @@ -57,7 +58,7 @@ export class DeviceService { permission: { userUuid, permissionType: { - type: PermissionType.READ || PermissionType.CONTROLLABLE, + type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), }, }, }, @@ -81,6 +82,7 @@ export class DeviceService { } as GetDeviceDetailsInterface; }), ); + return devicesData; } catch (error) { // Handle the error here @@ -147,12 +149,11 @@ export class DeviceService { throw new Error('Product UUID is missing for the device.'); } - await this.deviceRepository.save({ + return await this.deviceRepository.save({ deviceTuyaUuid: addDeviceInRoomDto.deviceTuyaUuid, spaceDevice: { uuid: addDeviceInRoomDto.roomUuid }, productDevice: { uuid: device.productUuid }, }); - return { message: 'device added in room successfully' }; } catch (error) { if (error.code === '23505') { throw new HttpException( From cc006ff3c0be6da5840575564b8c260d33af0a4a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 21 May 2024 16:20:05 +0300 Subject: [PATCH 203/259] resolve conflict with dev branch --- src/device/services/device.service.ts | 56 +++++++++++++-------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 8d3ae3d..fdb75b8 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -4,6 +4,7 @@ import { HttpException, HttpStatus, NotFoundException, + BadRequestException, } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; @@ -17,6 +18,7 @@ import { GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, controlDeviceInterface, + updateDeviceFirmwareInterface, } from '../interfaces/get.device.interface'; import { GetDeviceByGroupIdDto, @@ -26,9 +28,9 @@ import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; -import { ProductType } from '@app/common/constants/product-type.enum'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { In } from 'typeorm'; +import { ProductType } from '@app/common/constants/product-type.enum'; @Injectable() export class DeviceService { @@ -47,7 +49,17 @@ export class DeviceService { secretKey, }); } - + private async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { + return await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + ...(withProductDevice && { relations: ['productDevice'] }), + }); + } async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, userUuid: string, @@ -251,12 +263,6 @@ export class DeviceService { deviceUuid, ); - const deviceDetails = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - relations: ['productDevice'], - }); const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { @@ -405,6 +411,18 @@ export class DeviceService { ); } } + private async getUserDevicePermission(userUuid: string, deviceUuid: string) { + const device = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + permission: { + userUuid: userUuid, + }, + }, + relations: ['permission', 'permission.permissionType'], + }); + return device.permission[0].permissionType.type; + } async getDevicesInGetaway(gatewayUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(gatewayUuid); @@ -456,19 +474,6 @@ export class DeviceService { ); } } - - private async getDeviceByDeviceUuid( - deviceUuid: string, - withProductDevice: boolean = true, - ) { - return await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - ...(withProductDevice && { relations: ['productDevice'] }), - }); - } - async updateDeviceFirmware(deviceUuid: string, firmwareVersion: number) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); @@ -512,13 +517,4 @@ export class DeviceService { ); } } - private async getUserDevicePermission(userUuid: string, deviceUuid: string) { - const device = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - relations: ['permission', 'permission.permissionType'], - }); - return device.permission[0].permissionType.type; - } } From fdab3fa6873235d2f414ebc074be6ccc96c73f13 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:34:28 +0300 Subject: [PATCH 204/259] Add device and user notification modules --- .../device.notification.module.ts | 10 ++++++ .../dtos/device.notification.dto.ts | 15 +++++++++ .../modules/device-notification/dtos/index.ts | 1 + .../entities/device.notification.entity.ts | 33 +++++++++++++++++++ .../device-notification/entities/index.ts | 1 + .../device.notification.repository.ts | 10 ++++++ .../device-notification/repositories/index.ts | 1 + .../modules/device/entities/device.entity.ts | 10 +++++- .../modules/user-notification/dtos/index.ts | 1 + .../dtos/user.notification.dto.ts | 19 +++++++++++ .../user-notification/entities/index.ts | 1 + .../entities/user.notification.entity.ts | 27 +++++++++++++++ .../user-notification/repositories/index.ts | 1 + .../user.notification.repository.ts | 10 ++++++ .../user.notification.repository.module.ts | 10 ++++++ .../src/modules/user/entities/user.entity.ts | 13 +++++++- 16 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/modules/device-notification/device.notification.module.ts create mode 100644 libs/common/src/modules/device-notification/dtos/device.notification.dto.ts create mode 100644 libs/common/src/modules/device-notification/dtos/index.ts create mode 100644 libs/common/src/modules/device-notification/entities/device.notification.entity.ts create mode 100644 libs/common/src/modules/device-notification/entities/index.ts create mode 100644 libs/common/src/modules/device-notification/repositories/device.notification.repository.ts create mode 100644 libs/common/src/modules/device-notification/repositories/index.ts create mode 100644 libs/common/src/modules/user-notification/dtos/index.ts create mode 100644 libs/common/src/modules/user-notification/dtos/user.notification.dto.ts create mode 100644 libs/common/src/modules/user-notification/entities/index.ts create mode 100644 libs/common/src/modules/user-notification/entities/user.notification.entity.ts create mode 100644 libs/common/src/modules/user-notification/repositories/index.ts create mode 100644 libs/common/src/modules/user-notification/repositories/user.notification.repository.ts create mode 100644 libs/common/src/modules/user-notification/user.notification.repository.module.ts diff --git a/libs/common/src/modules/device-notification/device.notification.module.ts b/libs/common/src/modules/device-notification/device.notification.module.ts new file mode 100644 index 0000000..73acce3 --- /dev/null +++ b/libs/common/src/modules/device-notification/device.notification.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeviceNotificationEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([DeviceNotificationEntity])], +}) +export class DeviceNotificationRepositoryModule {} diff --git a/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts b/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts new file mode 100644 index 0000000..0746c14 --- /dev/null +++ b/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceNotificationDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; +} diff --git a/libs/common/src/modules/device-notification/dtos/index.ts b/libs/common/src/modules/device-notification/dtos/index.ts new file mode 100644 index 0000000..d205031 --- /dev/null +++ b/libs/common/src/modules/device-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './device.notification.dto'; diff --git a/libs/common/src/modules/device-notification/entities/device.notification.entity.ts b/libs/common/src/modules/device-notification/entities/device.notification.entity.ts new file mode 100644 index 0000000..84e592a --- /dev/null +++ b/libs/common/src/modules/device-notification/entities/device.notification.entity.ts @@ -0,0 +1,33 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceNotificationDto } from '../dtos'; +import { DeviceEntity } from '../../device/entities'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'device-notification' }) +@Unique(['userUuid', 'deviceUuid']) +export class DeviceNotificationEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public userUuid: string; + + @Column({ + nullable: false, + }) + deviceUuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.permission, { + nullable: false, + }) + device: DeviceEntity; + + @ManyToOne(() => UserEntity, (user) => user.userPermission, { + nullable: false, + }) + user: UserEntity; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/device-notification/entities/index.ts b/libs/common/src/modules/device-notification/entities/index.ts new file mode 100644 index 0000000..fedfc80 --- /dev/null +++ b/libs/common/src/modules/device-notification/entities/index.ts @@ -0,0 +1 @@ +export * from './device.notification.entity'; diff --git a/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts b/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts new file mode 100644 index 0000000..b226791 --- /dev/null +++ b/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceNotificationEntity } from '../entities'; + +@Injectable() +export class DeviceNotificationRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceNotificationEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device-notification/repositories/index.ts b/libs/common/src/modules/device-notification/repositories/index.ts new file mode 100644 index 0000000..1f3da6f --- /dev/null +++ b/libs/common/src/modules/device-notification/repositories/index.ts @@ -0,0 +1 @@ +export * from './device.notification.repository'; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 53dbbb3..fee939a 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -5,6 +5,7 @@ import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; @Entity({ name: 'device' }) @Unique(['spaceDevice', 'deviceTuyaUuid']) @@ -28,7 +29,14 @@ export class DeviceEntity extends AbstractEntity { }, ) permission: DeviceUserPermissionEntity[]; - + @OneToMany( + () => DeviceNotificationEntity, + (deviceUserNotification) => deviceUserNotification.device, + { + nullable: true, + }, + ) + deviceUserNotification: DeviceNotificationEntity[]; @OneToMany( () => GroupDeviceEntity, (userGroupDevices) => userGroupDevices.device, diff --git a/libs/common/src/modules/user-notification/dtos/index.ts b/libs/common/src/modules/user-notification/dtos/index.ts new file mode 100644 index 0000000..307e6f3 --- /dev/null +++ b/libs/common/src/modules/user-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.notification.dto'; diff --git a/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts b/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts new file mode 100644 index 0000000..4e9f72f --- /dev/null +++ b/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts @@ -0,0 +1,19 @@ +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class UserNotificationDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public subscriptionUuid: string; + + @IsBoolean() + @IsNotEmpty() + public active: boolean; +} diff --git a/libs/common/src/modules/user-notification/entities/index.ts b/libs/common/src/modules/user-notification/entities/index.ts new file mode 100644 index 0000000..1acf5c0 --- /dev/null +++ b/libs/common/src/modules/user-notification/entities/index.ts @@ -0,0 +1 @@ +export * from './user.notification.entity'; diff --git a/libs/common/src/modules/user-notification/entities/user.notification.entity.ts b/libs/common/src/modules/user-notification/entities/user.notification.entity.ts new file mode 100644 index 0000000..aa6ec6b --- /dev/null +++ b/libs/common/src/modules/user-notification/entities/user.notification.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserNotificationDto } from '../dtos'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'user-notification' }) +@Unique(['user', 'subscriptionUuid']) +export class UserNotificationEntity extends AbstractEntity { + @ManyToOne(() => UserEntity, (user) => user.roles, { + nullable: false, + }) + user: UserEntity; + @Column({ + nullable: false, + }) + subscriptionUuid: string; + + @Column({ + nullable: false, + default: true, + }) + active: boolean; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user-notification/repositories/index.ts b/libs/common/src/modules/user-notification/repositories/index.ts new file mode 100644 index 0000000..76d3ac2 --- /dev/null +++ b/libs/common/src/modules/user-notification/repositories/index.ts @@ -0,0 +1 @@ +export * from './user.notification.repository'; diff --git a/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts b/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts new file mode 100644 index 0000000..6862e88 --- /dev/null +++ b/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserNotificationEntity } from '../entities'; + +@Injectable() +export class UserNotificationRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserNotificationEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user-notification/user.notification.repository.module.ts b/libs/common/src/modules/user-notification/user.notification.repository.module.ts new file mode 100644 index 0000000..73997be --- /dev/null +++ b/libs/common/src/modules/user-notification/user.notification.repository.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserNotificationEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserNotificationEntity])], +}) +export class UserNotificationRepositoryModule {} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 17a9268..06c7c91 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -4,6 +4,8 @@ import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user-space/entities'; import { UserRoleEntity } from '../../user-role/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; +import { UserNotificationEntity } from '../../user-notification/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -53,12 +55,21 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) userSpaces: UserSpaceEntity[]; + @OneToMany( + () => UserNotificationEntity, + (userNotification) => userNotification.user, + ) + userNotification: UserNotificationEntity[]; @OneToMany( () => DeviceUserPermissionEntity, (userPermission) => userPermission.user, ) userPermission: DeviceUserPermissionEntity[]; - + @OneToMany( + () => DeviceNotificationEntity, + (deviceUserNotification) => deviceUserNotification.user, + ) + deviceUserNotification: DeviceNotificationEntity[]; @OneToMany(() => UserRoleEntity, (role) => role.user, { nullable: true, }) From 167cb055c10d62bba6e4c451522f168b1da362a8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:34:51 +0300 Subject: [PATCH 205/259] feat: Add User Notification and Device Messages Subscription Modules --- src/app.module.ts | 4 + .../controllers/device-messages.controller.ts | 98 +++++++++++++++++++ src/device-messages/controllers/index.ts | 1 + src/device-messages/device-messages.module.ts | 14 +++ .../dtos/device-messages.dto.ts | 20 ++++ src/device-messages/dtos/index.ts | 1 + .../services/device-messages.service.ts | 75 ++++++++++++++ src/device-messages/services/index.ts | 1 + src/user-notification/controllers/index.ts | 1 + .../user-notification.controller.ts | 94 ++++++++++++++++++ src/user-notification/dtos/index.ts | 1 + .../dtos/user-notification.dto.ts | 44 +++++++++ src/user-notification/services/index.ts | 1 + .../services/user-notification.service.ts | 83 ++++++++++++++++ .../user-notification.module.ts | 14 +++ 15 files changed, 452 insertions(+) create mode 100644 src/device-messages/controllers/device-messages.controller.ts create mode 100644 src/device-messages/controllers/index.ts create mode 100644 src/device-messages/device-messages.module.ts create mode 100644 src/device-messages/dtos/device-messages.dto.ts create mode 100644 src/device-messages/dtos/index.ts create mode 100644 src/device-messages/services/device-messages.service.ts create mode 100644 src/device-messages/services/index.ts create mode 100644 src/user-notification/controllers/index.ts create mode 100644 src/user-notification/controllers/user-notification.controller.ts create mode 100644 src/user-notification/dtos/index.ts create mode 100644 src/user-notification/dtos/user-notification.dto.ts create mode 100644 src/user-notification/services/index.ts create mode 100644 src/user-notification/services/user-notification.service.ts create mode 100644 src/user-notification/user-notification.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6add0be..9a4ef30 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,8 @@ import { FloorModule } from './floor/floor.module'; import { UnitModule } from './unit/unit.module'; import { RoleModule } from './role/role.module'; import { SeederModule } from '@app/common/seed/seeder.module'; +import { UserNotificationModule } from './user-notification/user-notification.module'; +import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -30,7 +32,9 @@ import { SeederModule } from '@app/common/seed/seeder.module'; RoomModule, GroupModule, DeviceModule, + DeviceMessagesSubscriptionModule, UserDevicePermissionModule, + UserNotificationModule, SeederModule, ], controllers: [AuthenticationController], diff --git a/src/device-messages/controllers/device-messages.controller.ts b/src/device-messages/controllers/device-messages.controller.ts new file mode 100644 index 0000000..c8f7834 --- /dev/null +++ b/src/device-messages/controllers/device-messages.controller.ts @@ -0,0 +1,98 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { DeviceMessagesSubscriptionService } from '../services/device-messages.service'; +import { DeviceMessagesAddDto } from '../dtos/device-messages.dto'; + +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Device Messages Status Module') +@Controller({ + version: '1', + path: 'device-messages/subscription', +}) +export class DeviceMessagesSubscriptionController { + constructor( + private readonly deviceMessagesSubscriptionService: DeviceMessagesSubscriptionService, + ) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addDeviceMessagesSubscription( + @Body() deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + const addDetails = + await this.deviceMessagesSubscriptionService.addDeviceMessagesSubscription( + deviceMessagesAddDto, + ); + return { + statusCode: HttpStatus.CREATED, + message: 'Device Messages Subscription Added Successfully', + data: addDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid/user/:userUuid') + async getDeviceMessagesSubscription( + @Param('deviceUuid') deviceUuid: string, + @Param('userUuid') userUuid: string, + ) { + try { + const deviceDetails = + await this.deviceMessagesSubscriptionService.getDeviceMessagesSubscription( + userUuid, + deviceUuid, + ); + return { + statusCode: HttpStatus.OK, + message: 'User Device Subscription fetched Successfully', + data: deviceDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete() + async deleteDeviceMessagesSubscription( + @Body() deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + await this.deviceMessagesSubscriptionService.deleteDeviceMessagesSubscription( + deviceMessagesAddDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User subscription deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/device-messages/controllers/index.ts b/src/device-messages/controllers/index.ts new file mode 100644 index 0000000..01e86df --- /dev/null +++ b/src/device-messages/controllers/index.ts @@ -0,0 +1 @@ +export * from './device-messages.controller'; diff --git a/src/device-messages/device-messages.module.ts b/src/device-messages/device-messages.module.ts new file mode 100644 index 0000000..451f013 --- /dev/null +++ b/src/device-messages/device-messages.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceMessagesSubscriptionController } from './controllers'; +import { DeviceMessagesSubscriptionService } from './services'; +import { DeviceNotificationRepositoryModule } from '@app/common/modules/device-notification/device.notification.module'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; + +@Module({ + imports: [ConfigModule, DeviceNotificationRepositoryModule], + controllers: [DeviceMessagesSubscriptionController], + providers: [DeviceNotificationRepository, DeviceMessagesSubscriptionService], + exports: [DeviceMessagesSubscriptionService], +}) +export class DeviceMessagesSubscriptionModule {} diff --git a/src/device-messages/dtos/device-messages.dto.ts b/src/device-messages/dtos/device-messages.dto.ts new file mode 100644 index 0000000..ae45970 --- /dev/null +++ b/src/device-messages/dtos/device-messages.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceMessagesAddDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'device uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + deviceUuid: string; +} diff --git a/src/device-messages/dtos/index.ts b/src/device-messages/dtos/index.ts new file mode 100644 index 0000000..ad01af8 --- /dev/null +++ b/src/device-messages/dtos/index.ts @@ -0,0 +1 @@ +export * from './device-messages.dto'; diff --git a/src/device-messages/services/device-messages.service.ts b/src/device-messages/services/device-messages.service.ts new file mode 100644 index 0000000..73c8182 --- /dev/null +++ b/src/device-messages/services/device-messages.service.ts @@ -0,0 +1,75 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeviceMessagesAddDto } from '../dtos/device-messages.dto'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; + +@Injectable() +export class DeviceMessagesSubscriptionService { + constructor( + private readonly deviceNotificationRepository: DeviceNotificationRepository, + ) {} + + async addDeviceMessagesSubscription( + deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + return await this.deviceNotificationRepository.save({ + user: { + uuid: deviceMessagesAddDto.userUuid, + }, + device: { + uuid: deviceMessagesAddDto.deviceUuid, + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This User already belongs to this device', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceMessagesSubscription(userUuid: string, deviceUuid: string) { + try { + const deviceUserSubscription = + await this.deviceNotificationRepository.findOne({ + where: { + user: { uuid: userUuid }, + device: { uuid: deviceUuid }, + }, + }); + + return { + uuid: deviceUserSubscription.uuid, + deviceUuid: deviceUserSubscription.deviceUuid, + userUuid: deviceUserSubscription.userUuid, + }; + } catch (error) { + throw new HttpException( + 'User device subscription not found', + HttpStatus.NOT_FOUND, + ); + } + } + async deleteDeviceMessagesSubscription( + deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + const result = await this.deviceNotificationRepository.delete({ + user: { uuid: deviceMessagesAddDto.userUuid }, + device: { uuid: deviceMessagesAddDto.deviceUuid }, + }); + + return result; + } catch (error) { + throw new HttpException( + error.message || 'device not found', + HttpStatus.NOT_FOUND, + ); + } + } +} diff --git a/src/device-messages/services/index.ts b/src/device-messages/services/index.ts new file mode 100644 index 0000000..8420bcb --- /dev/null +++ b/src/device-messages/services/index.ts @@ -0,0 +1 @@ +export * from './device-messages.service'; diff --git a/src/user-notification/controllers/index.ts b/src/user-notification/controllers/index.ts new file mode 100644 index 0000000..dcc9096 --- /dev/null +++ b/src/user-notification/controllers/index.ts @@ -0,0 +1 @@ +export * from './user-notification.controller'; diff --git a/src/user-notification/controllers/user-notification.controller.ts b/src/user-notification/controllers/user-notification.controller.ts new file mode 100644 index 0000000..83fd215 --- /dev/null +++ b/src/user-notification/controllers/user-notification.controller.ts @@ -0,0 +1,94 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { UserNotificationService } from '../services/user-notification.service'; +import { + UserNotificationAddDto, + UserNotificationUpdateDto, +} from '../dtos/user-notification.dto'; + +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('User Notification Module') +@Controller({ + version: '1', + path: 'user-notification/subscription', +}) +export class UserNotificationController { + constructor( + private readonly userNotificationService: UserNotificationService, + ) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addUserSubscription( + @Body() userNotificationAddDto: UserNotificationAddDto, + ) { + try { + const addDetails = await this.userNotificationService.addUserSubscription( + userNotificationAddDto, + ); + return { + statusCode: HttpStatus.CREATED, + message: 'User Notification Added Successfully', + data: addDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':userUuid') + async fetchUserSubscriptions(@Param('userUuid') userUuid: string) { + try { + const userDetails = + await this.userNotificationService.fetchUserSubscriptions(userUuid); + return { + statusCode: HttpStatus.OK, + message: 'User Notification fetched Successfully', + data: { ...userDetails }, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put() + async updateUserSubscription( + @Body() userNotificationUpdateDto: UserNotificationUpdateDto, + ) { + try { + await this.userNotificationService.updateUserSubscription( + userNotificationUpdateDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User subscription updated Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user-notification/dtos/index.ts b/src/user-notification/dtos/index.ts new file mode 100644 index 0000000..d87c96f --- /dev/null +++ b/src/user-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './user-notification.dto'; diff --git a/src/user-notification/dtos/user-notification.dto.ts b/src/user-notification/dtos/user-notification.dto.ts new file mode 100644 index 0000000..46b9d7f --- /dev/null +++ b/src/user-notification/dtos/user-notification.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class UserNotificationAddDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'subscription uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + subscriptionUuid: string; +} +export class UserNotificationUpdateDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'subscription uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + subscriptionUuid: string; + @ApiProperty({ + description: 'active', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + active: boolean; +} diff --git a/src/user-notification/services/index.ts b/src/user-notification/services/index.ts new file mode 100644 index 0000000..73988b4 --- /dev/null +++ b/src/user-notification/services/index.ts @@ -0,0 +1 @@ +export * from './user-notification.service'; diff --git a/src/user-notification/services/user-notification.service.ts b/src/user-notification/services/user-notification.service.ts new file mode 100644 index 0000000..d8992c0 --- /dev/null +++ b/src/user-notification/services/user-notification.service.ts @@ -0,0 +1,83 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + UserNotificationAddDto, + UserNotificationUpdateDto, +} from '../dtos/user-notification.dto'; +import { UserNotificationRepository } from '@app/common/modules/user-notification/repositories'; + +@Injectable() +export class UserNotificationService { + constructor( + private readonly userNotificationRepository: UserNotificationRepository, + ) {} + + async addUserSubscription(userNotificationAddDto: UserNotificationAddDto) { + try { + return await this.userNotificationRepository.save({ + user: { + uuid: userNotificationAddDto.userUuid, + }, + subscriptionUuid: userNotificationAddDto.subscriptionUuid, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This User already has this subscription uuid', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async fetchUserSubscriptions(userUuid: string) { + try { + const userNotifications = await this.userNotificationRepository.find({ + where: { + user: { uuid: userUuid }, + active: true, + }, + }); + return { + userUuid, + subscriptionUuids: [ + ...userNotifications.map((sub) => sub.subscriptionUuid), + ], + }; + } catch (error) { + throw new HttpException( + 'User subscription not found', + HttpStatus.NOT_FOUND, + ); + } + } + async updateUserSubscription( + userNotificationUpdateDto: UserNotificationUpdateDto, + ) { + try { + const result = await this.userNotificationRepository.update( + { + user: { uuid: userNotificationUpdateDto.userUuid }, + subscriptionUuid: userNotificationUpdateDto.subscriptionUuid, + }, + { active: userNotificationUpdateDto.active }, + ); + + if (result.affected === 0) { + throw new HttpException( + 'Subscription uuid not found', + HttpStatus.NOT_FOUND, + ); + } + + return result; + } catch (error) { + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user-notification/user-notification.module.ts b/src/user-notification/user-notification.module.ts new file mode 100644 index 0000000..0b03df1 --- /dev/null +++ b/src/user-notification/user-notification.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { UserNotificationRepositoryModule } from '@app/common/modules/user-notification/user.notification.repository.module'; +import { UserNotificationRepository } from '@app/common/modules/user-notification/repositories'; +import { UserNotificationService } from 'src/user-notification/services'; +import { UserNotificationController } from 'src/user-notification/controllers'; + +@Module({ + imports: [ConfigModule, UserNotificationRepositoryModule], + controllers: [UserNotificationController], + providers: [UserNotificationRepository, UserNotificationService], + exports: [UserNotificationService], +}) +export class UserNotificationModule {} From 8b88d2be9530b9a65c990f15f4283de7e73e84ce Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:35:20 +0300 Subject: [PATCH 206/259] Add OneSignal configuration and service --- libs/common/src/config/onesignal.config.ts | 9 ++++ .../src/helper/services/onesignal.service.ts | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 libs/common/src/config/onesignal.config.ts create mode 100644 libs/common/src/helper/services/onesignal.service.ts diff --git a/libs/common/src/config/onesignal.config.ts b/libs/common/src/config/onesignal.config.ts new file mode 100644 index 0000000..5567fd1 --- /dev/null +++ b/libs/common/src/config/onesignal.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'onesignal-config', + (): Record => ({ + ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID, + ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY, + }), +); diff --git a/libs/common/src/helper/services/onesignal.service.ts b/libs/common/src/helper/services/onesignal.service.ts new file mode 100644 index 0000000..1aa2c38 --- /dev/null +++ b/libs/common/src/helper/services/onesignal.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as OneSignal from 'onesignal-node'; + +@Injectable() +export class OneSignalService { + private client: any; + + constructor(private readonly configService: ConfigService) { + // Initialize OneSignal client here + this.client = new OneSignal.Client( + this.configService.get('onesignal-config.ONESIGNAL_APP_ID'), + this.configService.get('onesignal-config.ONESIGNAL_API_KEY'), + ); + } + + async sendNotification( + content: string, + title: string, + subscriptionIds: string[], + ): Promise { + const notification = { + contents: { + en: content, + }, + headings: { + en: title, + }, + include_subscription_ids: subscriptionIds, + }; + + try { + const response = await this.client.createNotification(notification); + + return response; + } catch (err) { + console.error('Error:', err); + throw new Error('Error sending notification'); + } + } +} From 35f061c907860ff54ca6bf3b925a370e1cba0a70 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:35:34 +0300 Subject: [PATCH 207/259] Add UserNotificationEntity and DeviceNotificationEntity to database module imports --- libs/common/src/database/database.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ce51bae..bfda450 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -16,6 +16,8 @@ import { GroupDeviceEntity } from '../modules/group-device/entities'; import { DeviceUserPermissionEntity } from '../modules/device-user-permission/entities'; 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'; @Module({ imports: [ @@ -46,6 +48,8 @@ import { RoleTypeEntity } from '../modules/role-type/entities'; DeviceUserPermissionEntity, UserRoleEntity, RoleTypeEntity, + UserNotificationEntity, + DeviceNotificationEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), From 7946c5673c10d855f3b379eb8156ad4607472190 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:36:14 +0300 Subject: [PATCH 208/259] Add global configuration to common module --- libs/common/src/common.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index e9d3f01..8c3e78c 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -13,6 +13,7 @@ import { EmailService } from './util/email.service'; imports: [ ConfigModule.forRoot({ load: config, + isGlobal: true, }), DatabaseModule, HelperModule, From 853251304f173e4401c66b4bd0e30e5956c41c8c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:36:37 +0300 Subject: [PATCH 209/259] Add Tuya WebSocket configuration and credentials --- libs/common/src/config/index.ts | 5 +- .../config/tuya-web-socket-config/config.ts | 30 +++ .../config/tuya-web-socket-config/index.ts | 214 ++++++++++++++++++ .../config/tuya-web-socket-config/utils.ts | 51 +++++ libs/common/src/config/tuya.config.ts | 9 + 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/config/tuya-web-socket-config/config.ts create mode 100644 libs/common/src/config/tuya-web-socket-config/index.ts create mode 100644 libs/common/src/config/tuya-web-socket-config/utils.ts create mode 100644 libs/common/src/config/tuya.config.ts diff --git a/libs/common/src/config/index.ts b/libs/common/src/config/index.ts index d4cbbdb..b642e54 100644 --- a/libs/common/src/config/index.ts +++ b/libs/common/src/config/index.ts @@ -1,3 +1,6 @@ import emailConfig from './email.config'; import superAdminConfig from './super.admin.config'; -export default [emailConfig, superAdminConfig]; +import tuyaConfig from './tuya.config'; +import oneSignalConfig from './onesignal.config'; + +export default [emailConfig, superAdminConfig, tuyaConfig, oneSignalConfig]; diff --git a/libs/common/src/config/tuya-web-socket-config/config.ts b/libs/common/src/config/tuya-web-socket-config/config.ts new file mode 100644 index 0000000..5103805 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/config.ts @@ -0,0 +1,30 @@ +export enum TuyaRegionConfigEnum { + CN = 'wss://mqe.tuyacn.com:8285/', + US = 'wss://mqe.tuyaus.com:8285/', + EU = 'wss://mqe.tuyaeu.com:8285/', + IN = 'wss://mqe.tuyain.com:8285/', +} + +export enum TUYA_PASULAR_ENV { + PROD = 'prod', + TEST = 'test', +} + +export const TuyaEnvConfig = Object.freeze({ + [TUYA_PASULAR_ENV.PROD]: { + name: TUYA_PASULAR_ENV.PROD, + value: 'event', + desc: 'online environment', + }, + [TUYA_PASULAR_ENV.TEST]: { + name: TUYA_PASULAR_ENV.TEST, + value: 'event-test', + desc: 'test environment', + }, +}); +type IEnvConfig = typeof TuyaEnvConfig; +export function getTuyaEnvConfig( + env: TUYA_PASULAR_ENV, +): IEnvConfig[K] { + return TuyaEnvConfig[env]; +} diff --git a/libs/common/src/config/tuya-web-socket-config/index.ts b/libs/common/src/config/tuya-web-socket-config/index.ts new file mode 100644 index 0000000..93e15c0 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/index.ts @@ -0,0 +1,214 @@ +import { EventEmitter } from 'events'; +import { WebSocket } from 'ws'; + +import { + TUYA_PASULAR_ENV, + getTuyaEnvConfig, + TuyaRegionConfigEnum, +} from './config'; +import { getTopicUrl, buildQuery, buildPassword, decrypt } from './utils'; + +type LoggerLevel = 'INFO' | 'ERROR'; + +interface IConfig { + accessId: string; + accessKey: string; + env: TUYA_PASULAR_ENV; + url: TuyaRegionConfigEnum; + + timeout?: number; + maxRetryTimes?: number; + retryTimeout?: number; + logger?: (level: LoggerLevel, ...args: any) => void; +} + +class TuyaMessageSubscribeWebsocket { + static URL = TuyaRegionConfigEnum; + static env = TUYA_PASULAR_ENV; + + static data = 'TUTA_DATA'; + static error = 'TUYA_ERROR'; + static open = 'TUYA_OPEN'; + static close = 'TUYA_CLOSE'; + static reconnect = 'TUYA_RECONNECT'; + static ping = 'TUYA_PING'; + static pong = 'TUYA_PONG'; + + private config: IConfig; + private server?: WebSocket; + private timer: any; + private retryTimes: number; + private event: EventEmitter; + + constructor(config: IConfig) { + this.config = Object.assign( + { + ackTimeoutMillis: 3000, + subscriptionType: 'Failover', + retryTimeout: 1000, + maxRetryTimes: 100, + timeout: 30000, + logger: console.log, + }, + config, + ); + this.event = new EventEmitter(); + this.retryTimes = 0; + } + + public start() { + this.server = this._connect(); + } + + public open(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.open, cb); + } + + public message(cb: (ws: WebSocket, message: any) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.data, cb); + } + + public ping(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.ping, cb); + } + + public pong(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.pong, cb); + } + + public reconnect(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.reconnect, cb); + } + + public ackMessage(messageId: string) { + this.server && this.server.send(JSON.stringify({ messageId })); + } + + public error(cb: (ws: WebSocket, error: any) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.error, cb); + } + + public close(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.close, cb); + } + + private _reconnect() { + if ( + this.config.maxRetryTimes && + this.retryTimes < this.config.maxRetryTimes + ) { + const timer = setTimeout(() => { + clearTimeout(timer); + this.retryTimes++; + this._connect(false); + }, this.config.retryTimeout); + } + } + + private _connect(isInit = true) { + const { accessId, accessKey, env, url } = this.config; + const topicUrl = getTopicUrl( + url, + accessId, + getTuyaEnvConfig(env).value, + `?${buildQuery({ subscriptionType: 'Failover', ackTimeoutMillis: 30000 })}`, + ); + const password = buildPassword(accessId, accessKey); + this.server = new WebSocket(topicUrl, { + rejectUnauthorized: false, + headers: { username: accessId, password }, + }); + this.subOpen(this.server, isInit); + this.subMessage(this.server); + this.subPing(this.server); + this.subPong(this.server); + this.subError(this.server); + this.subClose(this.server); + return this.server; + } + + private subOpen(server: WebSocket, isInit = true) { + server.on('open', () => { + if (server.readyState === server.OPEN) { + this.retryTimes = 0; + } + this.keepAlive(server); + this.event.emit( + isInit + ? TuyaMessageSubscribeWebsocket.open + : TuyaMessageSubscribeWebsocket.reconnect, + this.server, + ); + }); + } + + private subPing(server: WebSocket) { + server.on('ping', () => { + this.event.emit(TuyaMessageSubscribeWebsocket.ping, this.server); + this.keepAlive(server); + server.pong(this.config.accessId); + }); + } + + private subPong(server: WebSocket) { + server.on('pong', () => { + this.keepAlive(server); + this.event.emit(TuyaMessageSubscribeWebsocket.pong, this.server); + }); + } + + private subMessage(server: WebSocket) { + server.on('message', (data: any) => { + try { + this.keepAlive(server); + const start = Date.now(); + const obj = this.handleMessage(data); + this.event.emit(TuyaMessageSubscribeWebsocket.data, this.server, obj); + const end = Date.now(); + } catch (e) { + this.logger('ERROR', e); + this.event.emit(TuyaMessageSubscribeWebsocket.error, e); + } + }); + } + + private subClose(server: WebSocket) { + server.on('close', (...data) => { + this._reconnect(); + this.clearKeepAlive(); + this.event.emit(TuyaMessageSubscribeWebsocket.close, ...data); + }); + } + + private subError(server: WebSocket) { + server.on('error', (e) => { + this.event.emit(TuyaMessageSubscribeWebsocket.error, this.server, e); + }); + } + + private clearKeepAlive() { + clearTimeout(this.timer); + } + + private keepAlive(server: WebSocket) { + this.clearKeepAlive(); + this.timer = setTimeout(() => { + server.ping(this.config.accessId); + }, this.config.timeout); + } + + private handleMessage(data: string) { + const { payload, ...others } = JSON.parse(data); + const pStr = Buffer.from(payload, 'base64').toString('utf-8'); + const pJson = JSON.parse(pStr); + pJson.data = decrypt(pJson.data, this.config.accessKey); + return { payload: pJson, ...others }; + } + + private logger(level: LoggerLevel, ...info: any) { + const realInfo = `${Date.now()} `; + this.config.logger && this.config.logger(level, realInfo, ...info); + } +} + +export default TuyaMessageSubscribeWebsocket; diff --git a/libs/common/src/config/tuya-web-socket-config/utils.ts b/libs/common/src/config/tuya-web-socket-config/utils.ts new file mode 100644 index 0000000..5c22043 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/utils.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MD5, AES, enc, mode, pad } from 'crypto-js'; + +export function getTopicUrl( + websocketUrl: string, + accessId: string, + env: string, + query: string, +) { + return `${websocketUrl}ws/v2/consumer/persistent/${accessId}/out/${env}/${accessId}-sub${query}`; +} + +export function buildQuery(query: { [key: string]: number | string }) { + return Object.keys(query) + .map((key) => `${key}=${encodeURIComponent(query[key])}`) + .join('&'); +} + +export function buildPassword(accessId: string, accessKey: string) { + const key = MD5(accessKey).toString(); + return MD5(`${accessId}${key}`).toString().substr(8, 16); +} + +export function decrypt(data: string, accessKey: string) { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const json = AES.decrypt(data, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }); + const dataStr = enc.Utf8.stringify(json).toString(); + return JSON.parse(dataStr); + } catch (e) { + return ''; + } +} + +export function encrypt(data: any, accessKey: string) { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const realData = JSON.stringify(data); + const retData = AES.encrypt(realData, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }).toString(); + return retData; + } catch (e) { + return ''; + } +} diff --git a/libs/common/src/config/tuya.config.ts b/libs/common/src/config/tuya.config.ts new file mode 100644 index 0000000..4745c2e --- /dev/null +++ b/libs/common/src/config/tuya.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'tuya-config', + (): Record => ({ + TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID, + TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY, + }), +); From bb9b043be9a554ca264f582c919e2a8f35907c77 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:37:51 +0300 Subject: [PATCH 210/259] add tuya web socket service --- .../services/tuya.web.socket.service.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 libs/common/src/helper/services/tuya.web.socket.service.ts diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts new file mode 100644 index 0000000..a0358c7 --- /dev/null +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; +import TuyaWebsocket from '../../config/tuya-web-socket-config'; +import { ConfigService } from '@nestjs/config'; +import { OneSignalService } from './onesignal.service'; +import { DeviceMessagesService } from './device.messages.service'; + +@Injectable() +export class TuyaWebSocketService { + private client: any; // Adjust type according to your TuyaWebsocket client + + constructor( + private readonly configService: ConfigService, + private readonly oneSignalService: OneSignalService, + private readonly deviceMessagesService: DeviceMessagesService, + ) { + // Initialize the TuyaWebsocket client + this.client = new TuyaWebsocket({ + accessId: this.configService.get('tuya-config.TUYA_ACCESS_ID'), + accessKey: this.configService.get('tuya-config.TUYA_ACCESS_KEY'), + url: TuyaWebsocket.URL.EU, + env: TuyaWebsocket.env.TEST, + maxRetryTimes: 100, + }); + + // Set up event handlers + this.setupEventHandlers(); + + // Start receiving messages + this.client.start(); + } + + private setupEventHandlers() { + // Event handlers + this.client.open(() => { + console.log('open'); + }); + + this.client.message(async (ws: WebSocket, message: any) => { + try { + await this.deviceMessagesService.getDevicesUserNotifications( + message.payload.data.bizData.devId, + message.payload.data.bizData, + ); + this.client.ackMessage(message.messageId); + } catch (error) { + console.error('Error processing message:', error); + } + }); + + this.client.reconnect(() => { + console.log('reconnect'); + }); + + this.client.ping(() => { + console.log('ping'); + }); + + this.client.pong(() => { + console.log('pong'); + }); + + this.client.close((ws: WebSocket, ...args: any[]) => { + console.log('close', ...args); + }); + + this.client.error((ws: WebSocket, error: any) => { + console.error('WebSocket error:', error); + }); + } +} From cff8ac1c5c92829e29b16d0ea712b81e73fc0f4a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:38:22 +0300 Subject: [PATCH 211/259] add device messages services to send notification --- .../services/device.messages.service.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 libs/common/src/helper/services/device.messages.service.ts diff --git a/libs/common/src/helper/services/device.messages.service.ts b/libs/common/src/helper/services/device.messages.service.ts new file mode 100644 index 0000000..c51f6f4 --- /dev/null +++ b/libs/common/src/helper/services/device.messages.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; +import { OneSignalService } from './onesignal.service'; + +@Injectable() +export class DeviceMessagesService { + constructor( + private readonly deviceNotificationRepository: DeviceNotificationRepository, + private readonly oneSignalService: OneSignalService, + ) {} + + async getDevicesUserNotifications(deviceTuyaUuid: string, bizData: any) { + try { + // Retrieve notifications for the specified device + const notifications = await this.deviceNotificationRepository.find({ + where: { + device: { + deviceTuyaUuid, + }, + }, + relations: ['user', 'user.userNotification'], + }); + + // If notifications are found, send them + if (notifications) { + await this.sendNotifications(notifications, bizData); + } + } catch (error) { + console.error('Error fetching device notifications:', error); + } + } + + private async sendNotifications(notifications: any[], bizData: any) { + const notificationPromises = []; + + // Iterate over each notification and its associated user notifications + notifications.forEach((notification) => { + notification.user.userNotification.forEach((userNotification) => { + // Queue notification sending without awaiting + notificationPromises.push( + this.oneSignalService.sendNotification( + JSON.stringify(bizData), + 'device-status', + [userNotification.subscriptionUuid], + ), + ); + }); + }); + + // Wait for all notification sending operations to complete + await Promise.all(notificationPromises); + } +} From c3a71cedb31980c4b5863441b9286518cab180f7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:39:11 +0300 Subject: [PATCH 212/259] import the services in Helper Module --- libs/common/src/helper/helper.module.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index baa8f07..3378051 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -3,12 +3,26 @@ import { HelperHashService } from './services'; import { SpacePermissionService } from './services/space.permission.service'; import { SpaceRepository } from '../modules/space/repositories'; import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; +import { TuyaWebSocketService } from './services/tuya.web.socket.service'; + +import { OneSignalService } from './services/onesignal.service'; +import { DeviceMessagesService } from './services/device.messages.service'; +import { DeviceNotificationRepositoryModule } from '../modules/device-notification/device.notification.module'; +import { DeviceNotificationRepository } from '../modules/device-notification/repositories'; @Global() @Module({ - providers: [HelperHashService, SpacePermissionService, SpaceRepository], + providers: [ + HelperHashService, + SpacePermissionService, + SpaceRepository, + TuyaWebSocketService, + OneSignalService, + DeviceMessagesService, + DeviceNotificationRepository, + ], exports: [HelperHashService, SpacePermissionService], controllers: [], - imports: [SpaceRepositoryModule], + imports: [SpaceRepositoryModule, DeviceNotificationRepositoryModule], }) export class HelperModule {} From 1ad51fb391ab7e12bedf671cf099eb818e874ab5 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 00:39:35 +0300 Subject: [PATCH 213/259] install ws,onesignal-node --- package-lock.json | 454 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- 2 files changed, 451 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f333ed5..0fd8ba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.8", "@tuya/tuya-connector-nodejs": "^2.1.2", "argon2": "^0.40.1", "axios": "^1.6.7", @@ -29,11 +30,13 @@ "ioredis": "^5.3.2", "morgan": "^1.10.0", "nodemailer": "^6.9.10", + "onesignal-node": "^3.4.0", "passport-jwt": "^4.0.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "ws": "^8.17.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", @@ -2060,6 +2063,28 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.8.tgz", + "integrity": "sha512-DTSCK+FYtSTljT6XjVUUZhf1cPxKEJf1AG1y2n+ERnd0vzMpnYpMFgGkDlXqa3uC+LAMcOcx1EyTCcHsSHrOVg==", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.6.2" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3086,11 +3111,40 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" + }, "node_modules/axios": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", @@ -3257,6 +3311,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -3296,6 +3358,11 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -3536,6 +3603,11 @@ } ] }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4076,6 +4148,17 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4288,6 +4371,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -4852,6 +4944,11 @@ "node": ">= 0.8" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4866,11 +4963,18 @@ "node": ">=4" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -4897,8 +5001,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -5134,6 +5237,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", @@ -5332,6 +5443,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -5429,6 +5548,47 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5536,6 +5696,20 @@ "node": ">= 0.8" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5816,6 +5990,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5838,6 +6017,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -6625,6 +6809,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6649,6 +6838,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -6661,6 +6855,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6712,6 +6911,20 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -7278,6 +7491,14 @@ "node": ">=8" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7286,6 +7507,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -7322,6 +7551,18 @@ "wrappy": "1" } }, + "node_modules/onesignal-node": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/onesignal-node/-/onesignal-node-3.4.0.tgz", + "integrity": "sha512-9dNpfU5Xp6VhJLkdZT4kVqmOaU36RJOgp+6REQHyv+hLOcgqqa4/FRXxuHbjRCE51x9BK4jIC/gn2Mnw0gQgFQ==", + "dependencies": { + "request": "^2.88.2", + "request-promise": "^4.2.6" + }, + "engines": { + "node": ">=8.13.0" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -7596,6 +7837,11 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", "peer": true }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/pg": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", @@ -7911,11 +8157,15 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -8101,6 +8351,99 @@ "node": ">=0.10" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8681,6 +9024,30 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -8715,6 +9082,14 @@ "node": ">= 0.8" } }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9158,6 +9533,18 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9332,6 +9719,22 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9604,7 +10007,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -9670,6 +10072,24 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -9875,6 +10295,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index a8502be..0136b92 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.8", "@tuya/tuya-connector-nodejs": "^2.1.2", "argon2": "^0.40.1", "axios": "^1.6.7", @@ -40,11 +41,13 @@ "ioredis": "^5.3.2", "morgan": "^1.10.0", "nodemailer": "^6.9.10", + "onesignal-node": "^3.4.0", "passport-jwt": "^4.0.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "ws": "^8.17.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", From 0a830ddf0599bde732999550661aec055d955ec8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 26 May 2024 10:42:00 +0300 Subject: [PATCH 214/259] Refactor TuyaMessageSubscribeWebsocket class --- libs/common/src/config/tuya-web-socket-config/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/common/src/config/tuya-web-socket-config/index.ts b/libs/common/src/config/tuya-web-socket-config/index.ts index 93e15c0..0b41b23 100644 --- a/libs/common/src/config/tuya-web-socket-config/index.ts +++ b/libs/common/src/config/tuya-web-socket-config/index.ts @@ -161,10 +161,8 @@ class TuyaMessageSubscribeWebsocket { server.on('message', (data: any) => { try { this.keepAlive(server); - const start = Date.now(); const obj = this.handleMessage(data); this.event.emit(TuyaMessageSubscribeWebsocket.data, this.server, obj); - const end = Date.now(); } catch (e) { this.logger('ERROR', e); this.event.emit(TuyaMessageSubscribeWebsocket.error, e); From 423533df0428c8ee1afb3dd880fc24e2e9a5148e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 29 May 2024 23:29:53 +0300 Subject: [PATCH 215/259] feat(device): add endpoint to add device to user --- .../src/modules/device/dtos/device.dto.ts | 4 + .../modules/device/entities/device.entity.ts | 8 +- .../src/modules/user/entities/user.entity.ts | 4 + src/device/controllers/device.controller.ts | 37 ++++++-- src/device/dtos/add.device.dto.ts | 19 +++- src/device/services/device.service.ts | 91 ++++++++++++------- src/guards/device.guard.ts | 89 ++++++++++++++++++ src/guards/group.guard.ts | 2 +- src/guards/room.guard.ts | 34 +++---- 9 files changed, 223 insertions(+), 65 deletions(-) create mode 100644 src/guards/device.guard.ts diff --git a/libs/common/src/modules/device/dtos/device.dto.ts b/libs/common/src/modules/device/dtos/device.dto.ts index 8873f31..7a6ba0c 100644 --- a/libs/common/src/modules/device/dtos/device.dto.ts +++ b/libs/common/src/modules/device/dtos/device.dto.ts @@ -9,6 +9,10 @@ export class DeviceDto { @IsNotEmpty() spaceUuid: string; + @IsString() + @IsNotEmpty() + userUuid: string; + @IsString() @IsNotEmpty() deviceTuyaUuid: string; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 53dbbb3..c6b1859 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -5,9 +5,10 @@ import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; +import { UserEntity } from '../../user/entities'; @Entity({ name: 'device' }) -@Unique(['spaceDevice', 'deviceTuyaUuid']) +@Unique(['deviceTuyaUuid']) export class DeviceEntity extends AbstractEntity { @Column({ nullable: false, @@ -20,6 +21,9 @@ export class DeviceEntity extends AbstractEntity { }) isActive: true; + @ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) + user: UserEntity; + @OneToMany( () => DeviceUserPermissionEntity, (permission) => permission.device, @@ -36,7 +40,7 @@ export class DeviceEntity extends AbstractEntity { userGroupDevices: GroupDeviceEntity[]; @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { - nullable: false, + nullable: true, }) spaceDevice: SpaceEntity; diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 17a9268..9d93671 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -4,6 +4,7 @@ import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user-space/entities'; import { UserRoleEntity } from '../../user-role/entities'; +import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -53,6 +54,9 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) userSpaces: UserSpaceEntity[]; + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + @OneToMany( () => DeviceUserPermissionEntity, (userPermission) => userPermission.user, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index c102775..9d16e2b 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -10,11 +10,13 @@ import { HttpStatus, UseGuards, Req, + Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { + AddDeviceDto, AddDeviceInGroupDto, - AddDeviceInRoomDto, + UpdateDeviceInRoomDto, } from '../dtos/add.device.dto'; import { GetDeviceByGroupIdDto, @@ -26,6 +28,7 @@ import { CheckGroupGuard } from 'src/guards/group.guard'; import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { CheckDeviceGuard } from 'src/guards/device.guard'; @ApiTags('Device Module') @Controller({ @@ -34,7 +37,26 @@ import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; }) export class DeviceController { constructor(private readonly deviceService: DeviceService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckDeviceGuard) + @Post() + async addDevice(@Body() addDeviceDto: AddDeviceDto) { + try { + const device = await this.deviceService.addDevice(addDeviceDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device added successfully', + data: device, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') @@ -58,16 +80,19 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) - @Post('room') - async addDeviceInRoom(@Body() addDeviceInRoomDto: AddDeviceInRoomDto) { + @Put('room') + async updateDeviceInRoom( + @Body() updateDeviceInRoomDto: UpdateDeviceInRoomDto, + ) { try { - const device = - await this.deviceService.addDeviceInRoom(addDeviceInRoomDto); + const device = await this.deviceService.updateDeviceInRoom( + updateDeviceInRoomDto, + ); return { statusCode: HttpStatus.CREATED, success: true, - message: 'device added in room successfully', + message: 'device updated in room successfully', data: device, }; } catch (error) { diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index 4adc470..88c3712 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; -export class AddDeviceInRoomDto { +export class AddDeviceDto { @ApiProperty({ description: 'deviceTuyaUuid', required: true, @@ -10,6 +10,23 @@ export class AddDeviceInRoomDto { @IsNotEmpty() public deviceTuyaUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} +export class UpdateDeviceInRoomDto { + @ApiProperty({ + description: 'deviceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceUuid: string; + @ApiProperty({ description: 'roomUuid', required: true, diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 5f59d59..b9e232e 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -8,8 +8,9 @@ import { import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { + AddDeviceDto, AddDeviceInGroupDto, - AddDeviceInRoomDto, + UpdateDeviceInRoomDto, } from '../dtos/add.device.dto'; import { DeviceInstructionResponse, @@ -47,6 +48,38 @@ export class DeviceService { }); } + async addDevice(addDeviceDto: AddDeviceDto) { + try { + const device = await this.getDeviceDetailsByDeviceIdTuya( + addDeviceDto.deviceTuyaUuid, + ); + + if (!device.productUuid) { + throw new Error('Product UUID is missing for the device.'); + } + + return await this.deviceRepository.save({ + deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, + productDevice: { uuid: device.productUuid }, + user: { + uuid: addDeviceDto.userUuid, + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'Device already exists', + HttpStatus.BAD_REQUEST, + ); + } else { + throw new HttpException( + error.message || 'Failed to add device in room', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, userUuid: string, @@ -92,7 +125,31 @@ export class DeviceService { ); } } - + async updateDeviceInRoom(updateDeviceInRoomDto: UpdateDeviceInRoomDto) { + try { + await this.deviceRepository.update( + { uuid: updateDeviceInRoomDto.deviceUuid }, + { + spaceDevice: { uuid: updateDeviceInRoomDto.roomUuid }, + }, + ); + const device = await this.deviceRepository.findOne({ + where: { + uuid: updateDeviceInRoomDto.deviceUuid, + }, + relations: ['spaceDevice'], + }); + return { + uuid: device.uuid, + roomUuid: device.spaceDevice.uuid, + }; + } catch (error) { + throw new HttpException( + 'Failed to add device in room', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDevicesByGroupId( getDeviceByGroupIdDto: GetDeviceByGroupIdDto, userUuid: string, @@ -126,7 +183,6 @@ export class DeviceService { uuid: device.device.uuid, productUuid: device.device.productDevice.uuid, productType: device.device.productDevice.prodType, - permissionType: device.device.permission[0].permissionType.type, } as GetDeviceDetailsInterface; }), ); @@ -139,35 +195,6 @@ export class DeviceService { ); } } - async addDeviceInRoom(addDeviceInRoomDto: AddDeviceInRoomDto) { - try { - const device = await this.getDeviceDetailsByDeviceIdTuya( - addDeviceInRoomDto.deviceTuyaUuid, - ); - - if (!device.productUuid) { - throw new Error('Product UUID is missing for the device.'); - } - - return await this.deviceRepository.save({ - deviceTuyaUuid: addDeviceInRoomDto.deviceTuyaUuid, - spaceDevice: { uuid: addDeviceInRoomDto.roomUuid }, - productDevice: { uuid: device.productUuid }, - }); - } catch (error) { - if (error.code === '23505') { - throw new HttpException( - 'Device already exists in the room', - HttpStatus.BAD_REQUEST, - ); - } else { - throw new HttpException( - 'Failed to add device in room', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - } async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { try { diff --git a/src/guards/device.guard.ts b/src/guards/device.guard.ts new file mode 100644 index 0000000..5d08598 --- /dev/null +++ b/src/guards/device.guard.ts @@ -0,0 +1,89 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ConfigService } from '@nestjs/config'; +import { UserRepository } from '@app/common/modules/user/repositories'; + +@Injectable() +export class CheckDeviceGuard implements CanActivate { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + private readonly deviceRepository: DeviceRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + if (req.body && req.body.userUuid && req.body.deviceTuyaUuid) { + const { userUuid, deviceTuyaUuid } = req.body; + await this.checkUserIsFound(userUuid); + await this.checkDeviceIsFoundFromTuya(deviceTuyaUuid); + } else { + throw new BadRequestException('Invalid request parameters'); + } + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + private async checkUserIsFound(userUuid: string) { + const user = await this.userRepository.findOne({ + where: { + uuid: userUuid, + }, + }); + if (!user) { + throw new NotFoundException('User not found'); + } + } + async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { + const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + if (!response.success) { + throw new NotFoundException('Device not found from Tuya'); + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + if (error instanceof NotFoundException) { + response + .status(HttpStatus.NOT_FOUND) + .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); + } else if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + message: error.message || 'Invalid UUID', + }); + } + } +} diff --git a/src/guards/group.guard.ts b/src/guards/group.guard.ts index 1fe70e4..d2d994b 100644 --- a/src/guards/group.guard.ts +++ b/src/guards/group.guard.ts @@ -74,7 +74,7 @@ export class CheckGroupGuard implements CanActivate { } else { response.status(HttpStatus.BAD_REQUEST).json({ statusCode: HttpStatus.BAD_REQUEST, - message: 'Invalid UUID', + message: error.message || 'Invalid UUID', }); } } diff --git a/src/guards/room.guard.ts b/src/guards/room.guard.ts index 15b34b5..c5ed514 100644 --- a/src/guards/room.guard.ts +++ b/src/guards/room.guard.ts @@ -4,29 +4,17 @@ import { Injectable, HttpStatus, } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class CheckRoomGuard implements CanActivate { - private tuya: TuyaContext; constructor( - private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly deviceRepository: DeviceRepository, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', - accessKey, - secretKey, - }); - } + ) {} async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); @@ -35,10 +23,10 @@ export class CheckRoomGuard implements CanActivate { if (req.query && req.query.roomUuid) { const { roomUuid } = req.query; await this.checkRoomIsFound(roomUuid); - } else if (req.body && req.body.roomUuid && req.body.deviceTuyaUuid) { - const { roomUuid, deviceTuyaUuid } = req.body; + } else if (req.body && req.body.roomUuid && req.body.deviceUuid) { + const { roomUuid, deviceUuid } = req.body; await this.checkRoomIsFound(roomUuid); - await this.checkDeviceIsFoundFromTuya(deviceTuyaUuid); + await this.checkDeviceIsFound(deviceUuid); } else { throw new BadRequestException('Invalid request parameters'); } @@ -63,14 +51,14 @@ export class CheckRoomGuard implements CanActivate { throw new NotFoundException('Room not found'); } } - async checkDeviceIsFoundFromTuya(deviceTuyaUuid: string) { - const path = `/v1.1/iot-03/devices/${deviceTuyaUuid}`; - const response = await this.tuya.request({ - method: 'GET', - path, + async checkDeviceIsFound(deviceUuid: string) { + const response = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, }); - if (!response.success) { + if (!response.uuid) { throw new NotFoundException('Device not found'); } } @@ -88,7 +76,7 @@ export class CheckRoomGuard implements CanActivate { } else { response.status(HttpStatus.BAD_REQUEST).json({ statusCode: HttpStatus.BAD_REQUEST, - message: 'Invalid UUID', + message: error.message || 'Invalid UUID', }); } } From 45071ab927fcc35ff7dc95755b3ea160599f57af Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 30 May 2024 00:18:53 +0300 Subject: [PATCH 216/259] Refactor device controller and service --- src/device/controllers/device.controller.ts | 17 +++++++- src/device/services/device.service.ts | 45 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 9d16e2b..f9f9fab 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -40,9 +40,9 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckDeviceGuard) @Post() - async addDevice(@Body() addDeviceDto: AddDeviceDto) { + async addDeviceUser(@Body() addDeviceDto: AddDeviceDto) { try { - const device = await this.deviceService.addDevice(addDeviceDto); + const device = await this.deviceService.addDeviceUser(addDeviceDto); return { statusCode: HttpStatus.CREATED, @@ -58,6 +58,19 @@ export class DeviceController { } } @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':userUuid') + async getDevicesByUser(@Param('userUuid') userUuid: string) { + try { + return await this.deviceService.getDevicesByUser(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) @Get('room') async getDevicesByRoomId( diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index b9e232e..bb90b98 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -48,7 +48,7 @@ export class DeviceService { }); } - async addDevice(addDeviceDto: AddDeviceDto) { + async addDeviceUser(addDeviceDto: AddDeviceDto) { try { const device = await this.getDeviceDetailsByDeviceIdTuya( addDeviceDto.deviceTuyaUuid, @@ -79,7 +79,50 @@ export class DeviceService { } } } + async getDevicesByUser( + userUuid: string, + ): Promise { + try { + const devices = await this.deviceRepository.find({ + where: { + user: { uuid: userUuid }, + permission: { + userUuid, + permissionType: { + type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), + }, + }, + }, + relations: [ + 'spaceDevice', + 'productDevice', + 'permission', + 'permission.permissionType', + ], + }); + const devicesData = await Promise.all( + devices.map(async (device) => { + return { + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, + } as GetDeviceDetailsInterface; + }), + ); + return devicesData; + } catch (error) { + // Handle the error here + throw new HttpException( + 'User does not have any devices', + HttpStatus.NOT_FOUND, + ); + } + } async getDevicesByRoomId( getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, userUuid: string, From 1e29a59fa341d86ec25d5c7e1a20b93c3240261d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 30 May 2024 15:01:49 +0300 Subject: [PATCH 217/259] Add SuperAdminRoleGuard to DeviceController and include haveRoom property in devicesData --- src/device/controllers/device.controller.ts | 3 ++- src/device/services/device.service.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index f9f9fab..1e9aa5d 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -29,6 +29,7 @@ import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { CheckDeviceGuard } from 'src/guards/device.guard'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; @ApiTags('Device Module') @Controller({ @@ -38,7 +39,7 @@ import { CheckDeviceGuard } from 'src/guards/device.guard'; export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckDeviceGuard) + @UseGuards(SuperAdminRoleGuard, CheckDeviceGuard) @Post() async addDeviceUser(@Body() addDeviceDto: AddDeviceDto) { try { diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index bb90b98..b6e1b6c 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -103,13 +103,14 @@ export class DeviceService { const devicesData = await Promise.all( devices.map(async (device) => { return { - ...(await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - )), uuid: device.uuid, + haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: device.permission[0].permissionType.type, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), } as GetDeviceDetailsInterface; }), ); @@ -148,13 +149,14 @@ export class DeviceService { const devicesData = await Promise.all( devices.map(async (device) => { return { - ...(await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - )), uuid: device.uuid, + haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: device.permission[0].permissionType.type, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), } as GetDeviceDetailsInterface; }), ); From 483fc6375f546ba201f6997ecd9c962bbef7449e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:56:07 +0300 Subject: [PATCH 218/259] feat: Add unit invitation code functionality and user verification using code --- .../src/modules/space/dtos/space.dto.ts | 4 ++ .../modules/space/entities/space.entity.ts | 8 ++- package-lock.json | 18 ++++++ package.json | 1 + .../controllers/building.controller.ts | 2 +- .../controllers/community.controller.ts | 2 +- src/floor/controllers/floor.controller.ts | 2 +- src/room/controllers/room.controller.ts | 2 +- src/unit/controllers/unit.controller.ts | 43 ++++++++++++- src/unit/dtos/add.unit.dto.ts | 26 ++++++++ src/unit/services/unit.service.ts | 62 ++++++++++++++++++- 11 files changed, 161 insertions(+), 9 deletions(-) diff --git a/libs/common/src/modules/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts index cc5ad83..98706d0 100644 --- a/libs/common/src/modules/space/dtos/space.dto.ts +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -16,4 +16,8 @@ export class SpaceDto { @IsString() @IsNotEmpty() public spaceTypeUuid: string; + + @IsString() + @IsNotEmpty() + public invitationCode: string; } diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 56f7010..b337e91 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceTypeEntity } from '../../space-type/entities'; @@ -6,6 +6,7 @@ import { UserSpaceEntity } from '../../user-space/entities'; import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'space' }) +@Unique(['invitationCode']) export class SpaceEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -18,6 +19,11 @@ export class SpaceEntity extends AbstractEntity { nullable: false, }) public spaceName: string; + + @Column({ + nullable: true, + }) + public invitationCode: string; @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) parent: SpaceEntity; diff --git a/package-lock.json b/package-lock.json index f333ed5..a629215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", + "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", @@ -7165,6 +7166,23 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index a8502be..a5d2431 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", + "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 34c973a..a76d620 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -30,7 +30,7 @@ export class BuildingController { constructor(private readonly buildingService: BuildingService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckCommunityTypeGuard) + @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index df59979..c88acf1 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -32,7 +32,7 @@ export class CommunityController { constructor(private readonly communityService: CommunityService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(JwtAuthGuard) @Post() async addCommunity(@Body() addCommunityDto: AddCommunityDto) { try { diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 0df9efc..b4940fe 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -30,7 +30,7 @@ export class FloorController { constructor(private readonly floorService: FloorService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckBuildingTypeGuard) + @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 434370d..0a92e57 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -28,7 +28,7 @@ export class RoomController { constructor(private readonly roomService: RoomService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUnitTypeGuard) + @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 7c410b6..5c55bc7 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -12,7 +12,11 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddUnitDto, AddUserUnitDto } from '../dtos/add.unit.dto'; +import { + AddUnitDto, + AddUserUnitDto, + AddUserUnitUsingCodeDto, +} from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @@ -30,7 +34,7 @@ export class UnitController { constructor(private readonly unitService: UnitService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckFloorTypeGuard) + @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { @@ -147,4 +151,39 @@ export class UnitController { ); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid/invitation-code') + async getUnitInvitationCode(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitInvitationCode(unitUuid); + return unit; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('user/verify-code') + async verifyCodeAndAddUserUnit( + @Body() addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + await this.unitService.verifyCodeAndAddUserUnit(addUserUnitUsingCodeDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user unit added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts index e42d1bb..6d6c52a 100644 --- a/src/unit/dtos/add.unit.dto.ts +++ b/src/unit/dtos/add.unit.dto.ts @@ -40,3 +40,29 @@ export class AddUserUnitDto { Object.assign(this, dto); } } +export class AddUserUnitUsingCodeDto { + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 11d654f..d6d93b6 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -1,4 +1,5 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { nanoid } from 'nanoid'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, @@ -7,7 +8,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddUnitDto, AddUserUnitDto } from '../dtos'; +import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, @@ -227,7 +228,7 @@ export class UnitService { async addUserUnit(addUserUnitDto: AddUserUnitDto) { try { - await this.userSpaceRepository.save({ + return await this.userSpaceRepository.save({ user: { uuid: addUserUnitDto.userUuid }, space: { uuid: addUserUnitDto.unitUuid }, }); @@ -282,4 +283,61 @@ export class UnitService { } } } + async getUnitInvitationCode(unitUuid: string): Promise { + try { + // Generate a 6-character random invitation code + const invitationCode = nanoid(6); + + // Update the unit with the new invitation code + await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); + + // Fetch the updated unit + const updatedUnit = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedUnit.uuid, + invitationCode: updatedUnit.invitationCode, + type: updatedUnit.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async verifyCodeAndAddUserUnit( + addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + const unit = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: addUserUnitUsingCodeDto.inviteCode, + spaceType: { type: 'unit' }, + }, + relations: ['spaceType'], + }); + if (unit.invitationCode) { + const user = await this.addUserUnit({ + userUuid: addUserUnitUsingCodeDto.userUuid, + unitUuid: unit.uuid, + }); + if (user.uuid) { + await this.spaceRepository.update( + { uuid: unit.uuid }, + { invitationCode: null }, + ); + } + } + } catch (err) { + throw new HttpException( + 'Invalid invitation code', + HttpStatus.BAD_REQUEST, + ); + } + } } From 0e99df8037aa9528a751bb0ecfc1be1f509d9804 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:04:04 +0300 Subject: [PATCH 219/259] Refactor getUnitInvitationCode method to use dynamic import for nanoid library --- src/unit/services/unit.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index d6d93b6..8051469 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -1,5 +1,4 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; -import { nanoid } from 'nanoid'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, @@ -285,6 +284,8 @@ export class UnitService { } async getUnitInvitationCode(unitUuid: string): Promise { try { + const { nanoid } = await import('nanoid'); + // Generate a 6-character random invitation code const invitationCode = nanoid(6); From f0606f81e7075c2c395aeea6d0aad6f200411c5b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:25:05 +0300 Subject: [PATCH 220/259] Add generateRandomString helper function and remove nanoid dependency --- libs/common/src/helper/randomString.ts | 10 ++++++++++ package-lock.json | 15 --------------- package.json | 1 - src/unit/services/unit.service.ts | 7 ++++--- 4 files changed, 14 insertions(+), 19 deletions(-) create mode 100644 libs/common/src/helper/randomString.ts diff --git a/libs/common/src/helper/randomString.ts b/libs/common/src/helper/randomString.ts new file mode 100644 index 0000000..5a664e3 --- /dev/null +++ b/libs/common/src/helper/randomString.ts @@ -0,0 +1,10 @@ +export function generateRandomString(length: number): string { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let randomString = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + randomString += characters.charAt(randomIndex); + } + return randomString; +} diff --git a/package-lock.json b/package-lock.json index a629215..47268ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", - "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", @@ -5255,20 +5254,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/package.json b/package.json index a5d2431..a8502be 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", - "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 8051469..449aaa1 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -18,6 +18,7 @@ import { import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; +import { generateRandomString } from '@app/common/helper/randomString'; @Injectable() export class UnitService { @@ -284,10 +285,8 @@ export class UnitService { } async getUnitInvitationCode(unitUuid: string): Promise { try { - const { nanoid } = await import('nanoid'); - // Generate a 6-character random invitation code - const invitationCode = nanoid(6); + const invitationCode = generateRandomString(6); // Update the unit with the new invitation code await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); @@ -304,6 +303,8 @@ export class UnitService { type: updatedUnit.spaceType.type, }; } catch (err) { + console.log('err', err); + if (err instanceof BadRequestException) { throw err; } else { From 57c4a899d179ab3c5c2655dd48fa57d201afafb6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:06:39 +0300 Subject: [PATCH 221/259] Refactor device controller endpoint to use 'user' prefix in route path --- src/device/controllers/device.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 1e9aa5d..3276e39 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -60,7 +60,7 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':userUuid') + @Get('user/:userUuid') async getDevicesByUser(@Param('userUuid') userUuid: string) { try { return await this.deviceService.getDevicesByUser(userUuid); From 42b849b3c5a48bdcc064470fdcfee490c9ef154a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:21:07 +0300 Subject: [PATCH 222/259] Refactor code to remove duplicate code and improve readability --- src/device/services/device.service.ts | 4 ++-- src/unit/services/unit.service.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index b6e1b6c..d48a65e 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -103,7 +103,6 @@ export class DeviceService { const devicesData = await Promise.all( devices.map(async (device) => { return { - uuid: device.uuid, haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, @@ -111,6 +110,7 @@ export class DeviceService { ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), + uuid: device.uuid, } as GetDeviceDetailsInterface; }), ); @@ -149,7 +149,6 @@ export class DeviceService { const devicesData = await Promise.all( devices.map(async (device) => { return { - uuid: device.uuid, haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, @@ -157,6 +156,7 @@ export class DeviceService { ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), + uuid: device.uuid, } as GetDeviceDetailsInterface; }), ); diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 449aaa1..f0472af 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -303,8 +303,6 @@ export class UnitService { type: updatedUnit.spaceType.type, }; } catch (err) { - console.log('err', err); - if (err instanceof BadRequestException) { throw err; } else { From 8cf8a2da902a7dc1fabaf1e878006f00632cc9b6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:30:51 +0300 Subject: [PATCH 223/259] Add conditional check for Tuya web socket service --- libs/common/src/config/tuya.config.ts | 2 ++ .../src/helper/services/tuya.web.socket.service.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/common/src/config/tuya.config.ts b/libs/common/src/config/tuya.config.ts index 4745c2e..ca4dd73 100644 --- a/libs/common/src/config/tuya.config.ts +++ b/libs/common/src/config/tuya.config.ts @@ -5,5 +5,7 @@ export default registerAs( (): Record => ({ TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID, TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY, + TRUN_ON_TUYA_SOCKET: + process.env.TRUN_ON_TUYA_SOCKET === 'true' ? true : false, }), ); diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts index a0358c7..cca7fc6 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -22,11 +22,13 @@ export class TuyaWebSocketService { maxRetryTimes: 100, }); - // Set up event handlers - this.setupEventHandlers(); + if (this.configService.get('tuya-config.TRUN_ON_TUYA_SOCKET')) { + // Set up event handlers + this.setupEventHandlers(); - // Start receiving messages - this.client.start(); + // Start receiving messages + this.client.start(); + } } private setupEventHandlers() { From 4531a1f87b12d80924a9aac908f5cb8b43ad00ab Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:30:59 +0300 Subject: [PATCH 224/259] Remove duplicate code and unnecessary dependency in user entity and package-lock.json respectively --- .../src/modules/user/entities/user.entity.ts | 6 ------ package-lock.json | 17 ----------------- 2 files changed, 23 deletions(-) diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 581955f..c432751 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -59,12 +59,6 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) userDevice: DeviceEntity[]; - @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) - userDevice: DeviceEntity[]; - - @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) - userDevice: DeviceEntity[]; - @OneToMany( () => UserNotificationEntity, (userNotification) => userNotification.user, diff --git a/package-lock.json b/package-lock.json index 22aab51..7267182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7364,23 +7364,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", - "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", From 4d1861d137ccf518a0d61d324308d1f6c6afdad1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:12:37 +0300 Subject: [PATCH 225/259] Refactor group and device modules --- src/device/controllers/device.controller.ts | 46 +-- src/device/device.module.ts | 12 +- src/device/dtos/add.device.dto.ts | 17 - src/device/dtos/get.device.dto.ts | 9 - src/device/services/device.service.ts | 80 +---- src/group/controllers/group.controller.ts | 93 ++---- src/group/dtos/add.group.dto.ts | 20 -- src/group/dtos/control.group.dto.ts | 26 -- src/group/dtos/index.ts | 1 - src/group/dtos/rename.group.dto copy.ts | 12 - src/group/group.module.ts | 20 +- src/group/interfaces/get.group.interface.ts | 16 - src/group/services/group.service.ts | 326 +++++++------------- src/guards/group.guard.ts | 81 ----- 14 files changed, 137 insertions(+), 622 deletions(-) delete mode 100644 src/group/dtos/add.group.dto.ts delete mode 100644 src/group/dtos/control.group.dto.ts delete mode 100644 src/group/dtos/index.ts delete mode 100644 src/group/dtos/rename.group.dto copy.ts delete mode 100644 src/group/interfaces/get.group.interface.ts delete mode 100644 src/guards/group.guard.ts diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 3276e39..bb8dae8 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -13,18 +13,10 @@ import { Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { - AddDeviceDto, - AddDeviceInGroupDto, - UpdateDeviceInRoomDto, -} from '../dtos/add.device.dto'; -import { - GetDeviceByGroupIdDto, - GetDeviceByRoomUuidDto, -} from '../dtos/get.device.dto'; +import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto'; +import { GetDeviceByRoomUuidDto } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; import { CheckRoomGuard } from 'src/guards/room.guard'; -import { CheckGroupGuard } from 'src/guards/group.guard'; import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @@ -116,39 +108,7 @@ export class DeviceController { ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckGroupGuard) - @Get('group') - async getDevicesByGroupId( - @Query() getDeviceByGroupIdDto: GetDeviceByGroupIdDto, - @Req() req: any, - ) { - try { - const userUuid = req.user.uuid; - return await this.deviceService.getDevicesByGroupId( - getDeviceByGroupIdDto, - userUuid, - ); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckGroupGuard) - @Post('group') - async addDeviceInGroup(@Body() addDeviceInGroupDto: AddDeviceInGroupDto) { - try { - return await this.deviceService.addDeviceInGroup(addDeviceInGroupDto); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } + @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckUserHavePermission) @Get(':deviceUuid') diff --git a/src/device/device.module.ts b/src/device/device.module.ts index e48861b..07d1ad0 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -8,18 +8,10 @@ import { DeviceRepositoryModule } from '@app/common/modules/device'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; -import { GroupRepository } from '@app/common/modules/group/repositories'; -import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; import { UserRepository } from '@app/common/modules/user/repositories'; @Module({ - imports: [ - ConfigModule, - ProductRepositoryModule, - DeviceRepositoryModule, - GroupRepositoryModule, - ], + imports: [ConfigModule, ProductRepositoryModule, DeviceRepositoryModule], controllers: [DeviceController], providers: [ DeviceService, @@ -28,8 +20,6 @@ import { UserRepository } from '@app/common/modules/user/repositories'; PermissionTypeRepository, SpaceRepository, DeviceRepository, - GroupDeviceRepository, - GroupRepository, UserRepository, ], exports: [DeviceService], diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index 88c3712..f732317 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -35,20 +35,3 @@ export class UpdateDeviceInRoomDto { @IsNotEmpty() public roomUuid: string; } -export class AddDeviceInGroupDto { - @ApiProperty({ - description: 'deviceUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceUuid: string; - - @ApiProperty({ - description: 'groupUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public groupUuid: string; -} diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 5b5200e..26002bb 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -10,12 +10,3 @@ export class GetDeviceByRoomUuidDto { @IsNotEmpty() public roomUuid: string; } -export class GetDeviceByGroupIdDto { - @ApiProperty({ - description: 'groupUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public groupUuid: string; -} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index d48a65e..8e2e79c 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -7,11 +7,7 @@ import { } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { - AddDeviceDto, - AddDeviceInGroupDto, - UpdateDeviceInRoomDto, -} from '../dtos/add.device.dto'; +import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto'; import { DeviceInstructionResponse, GetDeviceDetailsFunctionsInterface, @@ -19,14 +15,10 @@ import { GetDeviceDetailsInterface, controlDeviceInterface, } from '../interfaces/get.device.interface'; -import { - GetDeviceByGroupIdDto, - GetDeviceByRoomUuidDto, -} from '../dtos/get.device.dto'; +import { GetDeviceByRoomUuidDto } from '../dtos/get.device.dto'; import { ControlDeviceDto } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { In } from 'typeorm'; @@ -36,7 +28,6 @@ export class DeviceService { constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, - private readonly groupDeviceRepository: GroupDeviceRepository, private readonly productRepository: ProductRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -195,73 +186,6 @@ export class DeviceService { ); } } - async getDevicesByGroupId( - getDeviceByGroupIdDto: GetDeviceByGroupIdDto, - userUuid: string, - ) { - try { - const groupDevices = await this.groupDeviceRepository.find({ - where: { - group: { uuid: getDeviceByGroupIdDto.groupUuid }, - device: { - permission: { - userUuid, - permissionType: { - type: PermissionType.READ || PermissionType.CONTROLLABLE, - }, - }, - }, - }, - relations: [ - 'device', - 'device.productDevice', - 'device.permission', - 'device.permission.permissionType', - ], - }); - const devicesData = await Promise.all( - groupDevices.map(async (device) => { - return { - ...(await this.getDeviceDetailsByDeviceIdTuya( - device.device.deviceTuyaUuid, - )), - uuid: device.device.uuid, - productUuid: device.device.productDevice.uuid, - productType: device.device.productDevice.prodType, - } as GetDeviceDetailsInterface; - }), - ); - - return devicesData; - } catch (error) { - throw new HttpException( - 'Error fetching devices by group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async addDeviceInGroup(addDeviceInGroupDto: AddDeviceInGroupDto) { - try { - await this.groupDeviceRepository.save({ - device: { uuid: addDeviceInGroupDto.deviceUuid }, - group: { uuid: addDeviceInGroupDto.groupUuid }, - }); - return { message: 'device added in group successfully' }; - } catch (error) { - if (error.code === '23505') { - throw new HttpException( - 'Device already exists in the group', - HttpStatus.BAD_REQUEST, - ); - } else { - throw new HttpException( - 'Failed to add device in group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - } async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index e936e06..ce6fae8 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -1,22 +1,16 @@ import { GroupService } from '../services/group.service'; import { - Body, Controller, Get, - Post, UseGuards, Param, - Put, - Delete, HttpException, HttpStatus, + Req, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddGroupDto } from '../dtos/add.group.dto'; -import { ControlGroupDto } from '../dtos/control.group.dto'; -import { RenameGroupDto } from '../dtos/rename.group.dto copy'; -import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; @ApiTags('Group Module') @Controller({ @@ -27,11 +21,11 @@ export class GroupController { constructor(private readonly groupService: GroupService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('space/:spaceUuid') - async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid') + async getGroupsBySpaceUuid(@Param('unitUuid') unitUuid: string) { try { - return await this.groupService.getGroupsBySpaceUuid(spaceUuid); + return await this.groupService.getGroupsByUnitUuid(unitUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -40,72 +34,21 @@ export class GroupController { } } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get(':groupUuid') - async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { - try { - return await this.groupService.getGroupsByGroupUuid(groupUuid); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckProductUuidForAllDevicesGuard) - @Post() - async addGroup(@Body() addGroupDto: AddGroupDto) { - try { - return await this.groupService.addGroup(addGroupDto); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('control') - async controlGroup(@Body() controlGroupDto: ControlGroupDto) { - try { - return await this.groupService.controlGroup(controlGroupDto); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put('rename/:groupUuid') - async renameGroupByUuid( - @Param('groupUuid') groupUuid: string, - @Body() renameGroupDto: RenameGroupDto, + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid/devices/:groupName') + async getUnitDevicesByGroupName( + @Param('unitUuid') unitUuid: string, + @Param('groupName') groupName: string, + @Req() req: any, ) { try { - return await this.groupService.renameGroupByUuid( - groupUuid, - renameGroupDto, - ); - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } + const userUuid = req.user.uuid; - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Delete(':groupUuid') - async deleteGroup(@Param('groupUuid') groupUuid: string) { - try { - return await this.groupService.deleteGroup(groupUuid); + return await this.groupService.getUnitDevicesByGroupName( + unitUuid, + groupName, + userUuid, + ); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/group/dtos/add.group.dto.ts b/src/group/dtos/add.group.dto.ts deleted file mode 100644 index aa2a562..0000000 --- a/src/group/dtos/add.group.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsArray } from 'class-validator'; - -export class AddGroupDto { - @ApiProperty({ - description: 'groupName', - required: true, - }) - @IsString() - @IsNotEmpty() - public groupName: string; - - @ApiProperty({ - description: 'deviceUuids', - required: true, - }) - @IsArray() - @IsNotEmpty() - public deviceUuids: [string]; -} diff --git a/src/group/dtos/control.group.dto.ts b/src/group/dtos/control.group.dto.ts deleted file mode 100644 index e3b48e9..0000000 --- a/src/group/dtos/control.group.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ControlGroupDto { - @ApiProperty({ - description: 'groupUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public groupUuid: string; - - @ApiProperty({ - description: 'code', - required: true, - }) - @IsString() - @IsNotEmpty() - public code: string; - @ApiProperty({ - description: 'value', - required: true, - }) - @IsNotEmpty() - public value: any; -} diff --git a/src/group/dtos/index.ts b/src/group/dtos/index.ts deleted file mode 100644 index 61cffa2..0000000 --- a/src/group/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.group.dto'; diff --git a/src/group/dtos/rename.group.dto copy.ts b/src/group/dtos/rename.group.dto copy.ts deleted file mode 100644 index f2b0c00..0000000 --- a/src/group/dtos/rename.group.dto copy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class RenameGroupDto { - @ApiProperty({ - description: 'groupName', - required: true, - }) - @IsString() - @IsNotEmpty() - public groupName: string; -} diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 61d95ab..b5ade2d 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -1,26 +1,20 @@ +import { DeviceRepository } from './../../libs/common/src/modules/device/repositories/device.repository'; import { Module } from '@nestjs/common'; import { GroupService } from './services/group.service'; import { GroupController } from './controllers/group.controller'; import { ConfigModule } from '@nestjs/config'; -import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; -import { GroupRepository } from '@app/common/modules/group/repositories'; -import { GroupDeviceRepositoryModule } from '@app/common/modules/group-device/group.device.repository.module'; -import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; import { DeviceRepositoryModule } from '@app/common/modules/device'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; @Module({ - imports: [ - ConfigModule, - GroupRepositoryModule, - GroupDeviceRepositoryModule, - DeviceRepositoryModule, - ], + imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], providers: [ GroupService, - GroupRepository, - GroupDeviceRepository, DeviceRepository, + SpaceRepository, + DeviceRepository, + ProductRepository, ], exports: [GroupService], }) diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts deleted file mode 100644 index 525fa04..0000000 --- a/src/group/interfaces/get.group.interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface GetGroupDetailsInterface { - groupUuid: string; - groupName: string; - createdAt: Date; - updatedAt: Date; -} -export interface GetGroupsBySpaceUuidInterface { - groupUuid: string; - groupName: string; -} - -export interface controlGroupInterface { - success: boolean; - result: boolean; - msg: string; -} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 9f6c3ac..94cf8e0 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -1,33 +1,23 @@ -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { AddGroupDto } from '../dtos/add.group.dto'; -import { - GetGroupDetailsInterface, - GetGroupsBySpaceUuidInterface, -} from '../interfaces/get.group.interface'; -import { ControlGroupDto } from '../dtos/control.group.dto'; -import { RenameGroupDto } from '../dtos/rename.group.dto copy'; -import { GroupRepository } from '@app/common/modules/group/repositories'; -import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; -import { controlDeviceInterface } from 'src/device/interfaces/get.device.interface'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { In } from 'typeorm'; @Injectable() export class GroupService { private tuya: TuyaContext; constructor( private readonly configService: ConfigService, - private readonly groupRepository: GroupRepository, - private readonly groupDeviceRepository: GroupDeviceRepository, + private readonly productRepository: ProductRepository, + private readonly spaceRepository: SpaceRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); this.tuya = new TuyaContext({ baseUrl: 'https://openapi.tuyaeu.com', accessKey, @@ -35,230 +25,126 @@ export class GroupService { }); } - async getGroupsBySpaceUuid( - spaceUuid: string, - ): Promise { + async getGroupsByUnitUuid(unitUuid: string) { try { - const groupDevices = await this.groupDeviceRepository.find({ - relations: ['group', 'device'], + const spaces = await this.spaceRepository.find({ where: { - device: { spaceDevice: { uuid: spaceUuid } }, - isActive: true, - }, - }); - - // Extract and return only the group entities - const groups = groupDevices.map((groupDevice) => { - return { - groupUuid: groupDevice.uuid, - groupName: groupDevice.group.groupName, - }; - }); - if (groups.length > 0) { - return groups; - } else { - throw new HttpException( - 'this space has no groups', - HttpStatus.NOT_FOUND, - ); - } - } catch (error) { - throw new HttpException( - error.message || 'Error fetching groups', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async addGroup(addGroupDto: AddGroupDto) { - try { - const group = await this.groupRepository.save({ - groupName: addGroupDto.groupName, - }); - - const groupDevicePromises = addGroupDto.deviceUuids.map( - async (deviceUuid) => { - await this.saveGroupDevice(group.uuid, deviceUuid); - }, - ); - - await Promise.all(groupDevicePromises); - return { message: 'Group added successfully' }; - } catch (err) { - if (err.code === '23505') { - throw new HttpException( - 'User already belongs to this group', - HttpStatus.BAD_REQUEST, - ); - } - throw new HttpException( - err.message || 'Internal Server Error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - private async saveGroupDevice(groupUuid: string, deviceUuid: string) { - try { - await this.groupDeviceRepository.save({ - group: { - uuid: groupUuid, - }, - device: { - uuid: deviceUuid, - }, - }); - } catch (error) { - throw error; - } - } - async getDevicesByGroupUuid(groupUuid: string) { - try { - const devices = await this.groupDeviceRepository.find({ - relations: ['device'], - where: { - group: { - uuid: groupUuid, + parent: { + uuid: unitUuid, }, - isActive: true, }, + relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], }); - return devices; - } catch (error) { - throw error; - } - } - async controlDevice(deviceUuid: string, code: string, value: any) { - try { - const response = await this.controlDeviceTuya(deviceUuid, code, value); - if (response.success) { - return response; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, + const groupNames = spaces.flatMap((space) => { + return space.devicesSpaceEntity.map( + (device) => device.productDevice.prodType, ); - } - } catch (error) { - throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); - } - } - async controlDeviceTuya( - deviceUuid: string, - code: string, - value: any, - ): Promise { - try { - const path = `/v1.0/iot-03/devices/${deviceUuid}/commands`; - const response = await this.tuya.request({ - method: 'POST', - path, - body: { - commands: [{ code, value: value }], - }, }); - return response as controlDeviceInterface; + const uniqueGroupNames = [...new Set(groupNames)]; + + return uniqueGroupNames.map((groupName) => ({ groupName })); } catch (error) { throw new HttpException( - 'Error control device from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, + 'This unit does not have any groups', + HttpStatus.NOT_FOUND, ); } } - async controlGroup(controlGroupDto: ControlGroupDto) { - const devices = await this.getDevicesByGroupUuid(controlGroupDto.groupUuid); + async getUnitDevicesByGroupName( + unitUuid: string, + groupName: string, + userUuid: string, + ) { try { - await Promise.all( - devices.map(async (device) => { - return this.controlDevice( - device.device.deviceTuyaUuid, - controlGroupDto.code, - controlGroupDto.value, + const spaces = await this.spaceRepository.find({ + where: { + parent: { + uuid: unitUuid, + }, + devicesSpaceEntity: { + productDevice: { + prodType: groupName, + }, + permission: { + userUuid, + permissionType: { + type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), + }, + }, + }, + }, + relations: [ + 'devicesSpaceEntity', + 'devicesSpaceEntity.productDevice', + 'devicesSpaceEntity.spaceDevice', + 'devicesSpaceEntity.permission', + 'devicesSpaceEntity.permission.permissionType', + ], + }); + + const devices = await Promise.all( + spaces.flatMap(async (space) => { + return await Promise.all( + space.devicesSpaceEntity.map(async (device) => { + const deviceDetails = await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + ); + return { + haveRoom: !!device.spaceDevice, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0]?.permissionType?.type, + ...deviceDetails, + uuid: device.uuid, + }; + }), ); }), ); - return { message: 'Group controlled successfully', success: true }; + if (devices.length === 0) + throw new HttpException('No devices found', HttpStatus.NOT_FOUND); + return devices.flat(); // Flatten the array since flatMap was used } catch (error) { throw new HttpException( - 'Error controlling devices', + 'This unit does not have any devices for the specified group name', + HttpStatus.NOT_FOUND, + ); + } + } + + async getDeviceDetailsByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.1/iot-03/devices/${deviceId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + // Convert keys to camel case + const camelCaseResponse = convertKeysToCamelCase(response); + const product = await this.productRepository.findOne({ + where: { + prodId: camelCaseResponse.result.productId, + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { productName, productId, ...rest } = camelCaseResponse.result; + + return { + ...rest, + productUuid: product.uuid, + } as GetDeviceDetailsInterface; + } catch (error) { + throw new HttpException( + 'Error fetching device details from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - - async renameGroupByUuid( - groupUuid: string, - renameGroupDto: RenameGroupDto, - ): Promise { - try { - await this.groupRepository.update( - { uuid: groupUuid }, - { groupName: renameGroupDto.groupName }, - ); - - // Fetch the updated floor - const updatedGroup = await this.groupRepository.findOneOrFail({ - where: { uuid: groupUuid }, - }); - return { - groupUuid: updatedGroup.uuid, - groupName: updatedGroup.groupName, - }; - } catch (error) { - throw new HttpException('Group not found', HttpStatus.NOT_FOUND); - } - } - - async deleteGroup(groupUuid: string) { - try { - const group = await this.getGroupsByGroupUuid(groupUuid); - - if (!group) { - throw new HttpException('Group not found', HttpStatus.NOT_FOUND); - } - - await this.groupRepository.update( - { uuid: groupUuid }, - { isActive: false }, - ); - - return { message: 'Group deleted successfully' }; - } catch (error) { - throw new HttpException( - error.message || 'Error deleting group', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getGroupsByGroupUuid( - groupUuid: string, - ): Promise { - try { - const group = await this.groupRepository.findOne({ - where: { - uuid: groupUuid, - isActive: true, - }, - }); - if (!group) { - throw new BadRequestException('Invalid group UUID'); - } - return { - groupUuid: group.uuid, - groupName: group.groupName, - createdAt: group.createdAt, - updatedAt: group.updatedAt, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Group not found', HttpStatus.NOT_FOUND); - } - } - } } diff --git a/src/guards/group.guard.ts b/src/guards/group.guard.ts deleted file mode 100644 index d2d994b..0000000 --- a/src/guards/group.guard.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; - -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { GroupRepository } from '@app/common/modules/group/repositories'; - -@Injectable() -export class CheckGroupGuard implements CanActivate { - constructor( - private readonly groupRepository: GroupRepository, - private readonly deviceRepository: DeviceRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - if (req.query && req.query.groupUuid) { - const { groupUuid } = req.query; - await this.checkGroupIsFound(groupUuid); - } else if (req.body && req.body.groupUuid && req.body.deviceUuid) { - const { groupUuid, deviceUuid } = req.body; - await this.checkGroupIsFound(groupUuid); - await this.checkDeviceIsFound(deviceUuid); - } else { - throw new BadRequestException('Invalid request parameters'); - } - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkGroupIsFound(groupUuid: string) { - const group = await this.groupRepository.findOne({ - where: { - uuid: groupUuid, - }, - }); - - if (!group) { - throw new NotFoundException('Group not found'); - } - } - async checkDeviceIsFound(deviceUuid: string) { - const device = await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - }); - - if (!device) { - throw new NotFoundException('Device not found'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - if (error instanceof NotFoundException) { - response - .status(HttpStatus.NOT_FOUND) - .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); - } else if (error instanceof BadRequestException) { - response - .status(HttpStatus.BAD_REQUEST) - .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); - } else { - response.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: error.message || 'Invalid UUID', - }); - } - } -} From bcab5a00d88492d788b4fc252efb9320ba7b3054 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:12:50 +0300 Subject: [PATCH 226/259] Refactor: Remove group and group-device modules --- libs/common/src/database/database.module.ts | 4 -- .../modules/device/entities/device.entity.ts | 6 --- .../group-device/dtos/group.device.dto.ts | 15 ------ .../src/modules/group-device/dtos/index.ts | 1 - .../entities/group.device.entity.ts | 48 ------------------- .../modules/group-device/entities/index.ts | 1 - .../group.device.repository.module.ts | 11 ----- .../repositories/group.device.repository.ts | 10 ---- .../group-device/repositories/index.ts | 1 - .../src/modules/group/dtos/group.dto.ts | 11 ----- libs/common/src/modules/group/dtos/index.ts | 1 - .../modules/group/entities/group.entity.ts | 34 ------------- .../src/modules/group/entities/index.ts | 1 - .../modules/group/group.repository.module.ts | 11 ----- .../group/repositories/group.repository.ts | 10 ---- .../src/modules/group/repositories/index.ts | 1 - 16 files changed, 166 deletions(-) delete mode 100644 libs/common/src/modules/group-device/dtos/group.device.dto.ts delete mode 100644 libs/common/src/modules/group-device/dtos/index.ts delete mode 100644 libs/common/src/modules/group-device/entities/group.device.entity.ts delete mode 100644 libs/common/src/modules/group-device/entities/index.ts delete mode 100644 libs/common/src/modules/group-device/group.device.repository.module.ts delete mode 100644 libs/common/src/modules/group-device/repositories/group.device.repository.ts delete mode 100644 libs/common/src/modules/group-device/repositories/index.ts delete mode 100644 libs/common/src/modules/group/dtos/group.dto.ts delete mode 100644 libs/common/src/modules/group/dtos/index.ts delete mode 100644 libs/common/src/modules/group/entities/group.entity.ts delete mode 100644 libs/common/src/modules/group/entities/index.ts delete mode 100644 libs/common/src/modules/group/group.repository.module.ts delete mode 100644 libs/common/src/modules/group/repositories/group.repository.ts delete mode 100644 libs/common/src/modules/group/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index bfda450..ef5a0a1 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -11,8 +11,6 @@ import { PermissionTypeEntity } from '../modules/permission/entities'; import { SpaceEntity } from '../modules/space/entities'; import { SpaceTypeEntity } from '../modules/space-type/entities'; import { UserSpaceEntity } from '../modules/user-space/entities'; -import { GroupEntity } from '../modules/group/entities'; -import { GroupDeviceEntity } from '../modules/group-device/entities'; import { DeviceUserPermissionEntity } from '../modules/device-user-permission/entities'; import { UserRoleEntity } from '../modules/user-role/entities'; import { RoleTypeEntity } from '../modules/role-type/entities'; @@ -43,8 +41,6 @@ import { DeviceNotificationEntity } from '../modules/device-notification/entitie SpaceEntity, SpaceTypeEntity, UserSpaceEntity, - GroupEntity, - GroupDeviceEntity, DeviceUserPermissionEntity, UserRoleEntity, RoleTypeEntity, diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 3389fec..3a46e07 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,7 +1,6 @@ import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; -import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; @@ -41,11 +40,6 @@ export class DeviceEntity extends AbstractEntity { }, ) deviceUserNotification: DeviceNotificationEntity[]; - @OneToMany( - () => GroupDeviceEntity, - (userGroupDevices) => userGroupDevices.device, - ) - userGroupDevices: GroupDeviceEntity[]; @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { nullable: true, diff --git a/libs/common/src/modules/group-device/dtos/group.device.dto.ts b/libs/common/src/modules/group-device/dtos/group.device.dto.ts deleted file mode 100644 index 1a4d51c..0000000 --- a/libs/common/src/modules/group-device/dtos/group.device.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class GroupDeviceDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public deviceUuid: string; - - @IsString() - @IsNotEmpty() - public groupUuid: string; -} diff --git a/libs/common/src/modules/group-device/dtos/index.ts b/libs/common/src/modules/group-device/dtos/index.ts deleted file mode 100644 index 66bc84a..0000000 --- a/libs/common/src/modules/group-device/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.device.dto'; diff --git a/libs/common/src/modules/group-device/entities/group.device.entity.ts b/libs/common/src/modules/group-device/entities/group.device.entity.ts deleted file mode 100644 index 8a39dc7..0000000 --- a/libs/common/src/modules/group-device/entities/group.device.entity.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, Entity, ManyToOne, Unique } from 'typeorm'; -import { GroupDeviceDto } from '../dtos'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { DeviceEntity } from '../../device/entities'; -import { GroupEntity } from '../../group/entities'; - -@Entity({ name: 'group-device' }) -@Unique(['device', 'group']) -export class GroupDeviceEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value - nullable: false, - }) - public uuid: string; - - @Column({ - type: 'string', - nullable: false, - }) - deviceUuid: string; - - @Column({ - type: 'string', - nullable: false, - }) - groupUuid: string; - - @ManyToOne(() => DeviceEntity, (device) => device.userGroupDevices, { - nullable: false, - }) - device: DeviceEntity; - - @ManyToOne(() => GroupEntity, (group) => group.groupDevices, { - nullable: false, - }) - group: GroupEntity; - - @Column({ - nullable: true, - default: true, - }) - public isActive: boolean; - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} diff --git a/libs/common/src/modules/group-device/entities/index.ts b/libs/common/src/modules/group-device/entities/index.ts deleted file mode 100644 index 6b96f11..0000000 --- a/libs/common/src/modules/group-device/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.device.entity'; diff --git a/libs/common/src/modules/group-device/group.device.repository.module.ts b/libs/common/src/modules/group-device/group.device.repository.module.ts deleted file mode 100644 index a3af56d..0000000 --- a/libs/common/src/modules/group-device/group.device.repository.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { GroupDeviceEntity } from './entities/group.device.entity'; - -@Module({ - providers: [], - exports: [], - controllers: [], - imports: [TypeOrmModule.forFeature([GroupDeviceEntity])], -}) -export class GroupDeviceRepositoryModule {} diff --git a/libs/common/src/modules/group-device/repositories/group.device.repository.ts b/libs/common/src/modules/group-device/repositories/group.device.repository.ts deleted file mode 100644 index 472c5aa..0000000 --- a/libs/common/src/modules/group-device/repositories/group.device.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { GroupDeviceEntity } from '../entities/group.device.entity'; - -@Injectable() -export class GroupDeviceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(GroupDeviceEntity, dataSource.createEntityManager()); - } -} diff --git a/libs/common/src/modules/group-device/repositories/index.ts b/libs/common/src/modules/group-device/repositories/index.ts deleted file mode 100644 index 2b40191..0000000 --- a/libs/common/src/modules/group-device/repositories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.device.repository'; diff --git a/libs/common/src/modules/group/dtos/group.dto.ts b/libs/common/src/modules/group/dtos/group.dto.ts deleted file mode 100644 index d3696b8..0000000 --- a/libs/common/src/modules/group/dtos/group.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class GroupDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public groupName: string; -} diff --git a/libs/common/src/modules/group/dtos/index.ts b/libs/common/src/modules/group/dtos/index.ts deleted file mode 100644 index ba43fbc..0000000 --- a/libs/common/src/modules/group/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.dto'; diff --git a/libs/common/src/modules/group/entities/group.entity.ts b/libs/common/src/modules/group/entities/group.entity.ts deleted file mode 100644 index 525f84d..0000000 --- a/libs/common/src/modules/group/entities/group.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Column, Entity, OneToMany } from 'typeorm'; -import { GroupDto } from '../dtos'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { GroupDeviceEntity } from '../../group-device/entities'; - -@Entity({ name: 'group' }) -export class GroupEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - public groupName: string; - - @OneToMany(() => GroupDeviceEntity, (groupDevice) => groupDevice.group, { - cascade: true, - }) - groupDevices: GroupDeviceEntity[]; - - @Column({ - nullable: true, - default: true, - }) - public isActive: boolean; - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} diff --git a/libs/common/src/modules/group/entities/index.ts b/libs/common/src/modules/group/entities/index.ts deleted file mode 100644 index 50f4201..0000000 --- a/libs/common/src/modules/group/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.entity'; diff --git a/libs/common/src/modules/group/group.repository.module.ts b/libs/common/src/modules/group/group.repository.module.ts deleted file mode 100644 index 5b711e5..0000000 --- a/libs/common/src/modules/group/group.repository.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { GroupEntity } from './entities/group.entity'; - -@Module({ - providers: [], - exports: [], - controllers: [], - imports: [TypeOrmModule.forFeature([GroupEntity])], -}) -export class GroupRepositoryModule {} diff --git a/libs/common/src/modules/group/repositories/group.repository.ts b/libs/common/src/modules/group/repositories/group.repository.ts deleted file mode 100644 index 824d671..0000000 --- a/libs/common/src/modules/group/repositories/group.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { GroupEntity } from '../entities/group.entity'; - -@Injectable() -export class GroupRepository extends Repository { - constructor(private dataSource: DataSource) { - super(GroupEntity, dataSource.createEntityManager()); - } -} diff --git a/libs/common/src/modules/group/repositories/index.ts b/libs/common/src/modules/group/repositories/index.ts deleted file mode 100644 index 7018977..0000000 --- a/libs/common/src/modules/group/repositories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './group.repository'; From 4d679c7300be1b5c60ed355df95cc6b874be4442 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 2 Jun 2024 23:12:25 +0300 Subject: [PATCH 227/259] Refactor authentication guard in UnitController's addUserUnit method --- src/unit/controllers/unit.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 5c55bc7..eedf62d 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -100,7 +100,7 @@ export class UnitController { } } @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUserUnitGuard) + @UseGuards(JwtAuthGuard, CheckUserUnitGuard) @Post('user') async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { try { From d3e63adf54b20c600c5a451196c78fd14b89e4db Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:31:00 +0300 Subject: [PATCH 228/259] Remove unnecessary code and properties from unit controller and add.unit.dto --- src/unit/controllers/unit.controller.ts | 1 - src/unit/dtos/add.unit.dto.ts | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index eedf62d..1d5cbd3 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -21,7 +21,6 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts index 6d6c52a..9896c37 100644 --- a/src/unit/dtos/add.unit.dto.ts +++ b/src/unit/dtos/add.unit.dto.ts @@ -41,13 +41,6 @@ export class AddUserUnitDto { } } export class AddUserUnitUsingCodeDto { - @ApiProperty({ - description: 'unitUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitUuid: string; @ApiProperty({ description: 'userUuid', required: true, From dfca913bccac208ad362bb175114f75df5c6f031 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 5 Jun 2024 12:37:09 +0300 Subject: [PATCH 229/259] Add user permissions to devices when joining a unit --- src/unit/services/unit.service.ts | 102 ++++++++++++++++++++++++------ src/unit/unit.module.ts | 6 ++ 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index f0472af..e7511c6 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -19,6 +19,8 @@ import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; import { generateRandomString } from '@app/common/helper/randomString'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; @Injectable() export class UnitService { @@ -26,6 +28,7 @@ export class UnitService { private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, private readonly userSpaceRepository: UserSpaceRepository, + private readonly userDevicePermissionService: UserDevicePermissionService, ) {} async addUnit(addUnitDto: AddUnitDto) { @@ -314,25 +317,20 @@ export class UnitService { addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, ) { try { - const unit = await this.spaceRepository.findOneOrFail({ - where: { - invitationCode: addUserUnitUsingCodeDto.inviteCode, - spaceType: { type: 'unit' }, - }, - relations: ['spaceType'], - }); - if (unit.invitationCode) { - const user = await this.addUserUnit({ - userUuid: addUserUnitUsingCodeDto.userUuid, - unitUuid: unit.uuid, - }); - if (user.uuid) { - await this.spaceRepository.update( - { uuid: unit.uuid }, - { invitationCode: null }, - ); - } - } + const unit = await this.findUnitByInviteCode( + addUserUnitUsingCodeDto.inviteCode, + ); + + await this.addUserToUnit(addUserUnitUsingCodeDto.userUuid, unit.uuid); + + await this.clearUnitInvitationCode(unit.uuid); + + const deviceUUIDs = await this.getDeviceUUIDsForUnit(unit.uuid); + + await this.addUserPermissionsToDevices( + addUserUnitUsingCodeDto.userUuid, + deviceUUIDs, + ); } catch (err) { throw new HttpException( 'Invalid invitation code', @@ -340,4 +338,70 @@ export class UnitService { ); } } + + private async findUnitByInviteCode(inviteCode: string): Promise { + const unit = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: inviteCode, + spaceType: { type: 'unit' }, + }, + relations: ['spaceType'], + }); + + return unit; + } + + private async addUserToUnit(userUuid: string, unitUuid: string) { + const user = await this.addUserUnit({ userUuid, unitUuid }); + + if (user.uuid) { + return user; + } else { + throw new HttpException( + 'Failed to add user to unit', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async clearUnitInvitationCode(unitUuid: string) { + await this.spaceRepository.update( + { uuid: unitUuid }, + { invitationCode: null }, + ); + } + + private async getDeviceUUIDsForUnit( + unitUuid: string, + ): Promise<{ uuid: string }[]> { + const devices = await this.spaceRepository.find({ + where: { parent: { uuid: unitUuid } }, + relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + }); + + const allDevices = devices.flatMap((space) => space.devicesSpaceEntity); + + return allDevices.map((device) => ({ uuid: device.uuid })); + } + + private async addUserPermissionsToDevices( + userUuid: string, + deviceUUIDs: { uuid: string }[], + ): Promise { + const permissionPromises = deviceUUIDs.map(async (device) => { + try { + await this.userDevicePermissionService.addUserPermission({ + userUuid, + deviceUuid: device.uuid, + permissionType: PermissionType.CONTROLLABLE, + }); + } catch (error) { + console.error( + `Failed to add permission for device ${device.uuid}: ${error.message}`, + ); + } + }); + + await Promise.all(permissionPromises); + } } diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts index 569ef39..7924e4a 100644 --- a/src/unit/unit.module.ts +++ b/src/unit/unit.module.ts @@ -10,6 +10,9 @@ import { UserSpaceRepositoryModule } from '@app/common/modules/user-space/user.s import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ imports: [ @@ -26,6 +29,9 @@ import { UserRepository } from '@app/common/modules/user/repositories'; SpaceTypeRepository, UserSpaceRepository, UserRepository, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, ], exports: [UnitService], }) From 720b9ce23f05e9e5223c22e8164981929af6d88f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:28:21 +0300 Subject: [PATCH 230/259] Add snake case converter helper function --- libs/common/src/helper/snakeCaseConverter.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 libs/common/src/helper/snakeCaseConverter.ts diff --git a/libs/common/src/helper/snakeCaseConverter.ts b/libs/common/src/helper/snakeCaseConverter.ts new file mode 100644 index 0000000..3b9ea55 --- /dev/null +++ b/libs/common/src/helper/snakeCaseConverter.ts @@ -0,0 +1,16 @@ +function toSnakeCase(str) { + return str.replace(/([A-Z])/g, '_$1').toLowerCase(); +} + +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) => { + const snakeKey = toSnakeCase(key); + result[snakeKey] = convertKeysToSnakeCase(obj[key]); + return result; + }, {}); + } + return obj; +} From 5319d76d41987946117b67d38bfadf31cadd3fc0 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:28:54 +0300 Subject: [PATCH 231/259] Add spaceTuyaUuid field to SpaceEntity --- libs/common/src/modules/space/entities/space.entity.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index b337e91..030db57 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -14,6 +14,10 @@ export class SpaceEntity extends AbstractEntity { nullable: false, }) public uuid: string; + @Column({ + nullable: true, + }) + public spaceTuyaUuid: string; @Column({ nullable: false, From b8a92c816a15f3dec344ac3942d77f36480d5318 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:29:03 +0300 Subject: [PATCH 232/259] Refactor DeviceService's getDeviceByDeviceUuid method --- src/device/services/device.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index a7cbaae..cacf266 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -41,7 +41,7 @@ export class DeviceService { secretKey, }); } - private async getDeviceByDeviceUuid( + async getDeviceByDeviceUuid( deviceUuid: string, withProductDevice: boolean = true, ) { From 4ffa8f05cfd1985e75817ff314fc5b116152fd7b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:29:32 +0300 Subject: [PATCH 233/259] Add Tuya integration to UnitService --- src/unit/interface/unit.interface.ts | 6 +++++ src/unit/services/unit.service.ts | 40 +++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts index 39fac9a..635f38e 100644 --- a/src/unit/interface/unit.interface.ts +++ b/src/unit/interface/unit.interface.ts @@ -4,6 +4,7 @@ export interface GetUnitByUuidInterface { updatedAt: Date; name: string; type: string; + spaceTuyaUuid: string; } export interface UnitChildInterface { @@ -29,3 +30,8 @@ export interface GetUnitByUserUuidInterface { name: string; type: string; } +export interface addTuyaSpaceInterface { + success: boolean; + result: string; + msg: string; +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index e7511c6..3279a05 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -14,6 +14,7 @@ import { GetUnitByUuidInterface, RenameUnitByUuidInterface, GetUnitByUserUuidInterface, + addTuyaSpaceInterface, } from '../interface/unit.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; @@ -21,15 +22,27 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories import { generateRandomString } from '@app/common/helper/randomString'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class UnitService { + private tuya: TuyaContext; constructor( + private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, private readonly userSpaceRepository: UserSpaceRepository, private readonly userDevicePermissionService: UserDevicePermissionService, - ) {} + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } async addUnit(addUnitDto: AddUnitDto) { try { @@ -38,17 +51,41 @@ export class UnitService { type: 'unit', }, }); + const tuyaUnit = await this.addUnitTuya(addUnitDto.unitName); + if (!tuyaUnit.result) { + throw new HttpException('Error creating unit', HttpStatus.BAD_REQUEST); + } const unit = await this.spaceRepository.save({ spaceName: addUnitDto.unitName, parent: { uuid: addUnitDto.floorUuid }, spaceType: { uuid: spaceType.uuid }, + spaceTuyaUuid: tuyaUnit.result, }); return unit; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } } + async addUnitTuya(unitName: string): Promise { + try { + const path = `/v2.0/cloud/space/creation`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: unitName, + }, + }); + + return response as addTuyaSpaceInterface; + } catch (error) { + throw new HttpException( + 'Error creating unit from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getUnitByUuid(unitUuid: string): Promise { try { @@ -70,6 +107,7 @@ export class UnitService { updatedAt: unit.updatedAt, name: unit.spaceName, type: unit.spaceType.type, + spaceTuyaUuid: unit.spaceTuyaUuid, }; } catch (err) { if (err instanceof BadRequestException) { From cf60404e02d8a2672d8641d9c5218a86774860d6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:30:03 +0300 Subject: [PATCH 234/259] feat(scene): add tap-to-run scene functionality --- src/app.module.ts | 2 + src/scene/controllers/index.ts | 1 + src/scene/controllers/scene.controller.ts | 114 +++++++++++ src/scene/dtos/add.scene.dto.ts | 103 ++++++++++ src/scene/dtos/index.ts | 1 + src/scene/interface/scene.interface.ts | 23 +++ src/scene/scene.module.ts | 23 +++ src/scene/services/index.ts | 1 + src/scene/services/scene.service.ts | 239 ++++++++++++++++++++++ 9 files changed, 507 insertions(+) create mode 100644 src/scene/controllers/index.ts create mode 100644 src/scene/controllers/scene.controller.ts create mode 100644 src/scene/dtos/add.scene.dto.ts create mode 100644 src/scene/dtos/index.ts create mode 100644 src/scene/interface/scene.interface.ts create mode 100644 src/scene/scene.module.ts create mode 100644 src/scene/services/index.ts create mode 100644 src/scene/services/scene.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 9a4ef30..59b4275 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { RoleModule } from './role/role.module'; import { SeederModule } from '@app/common/seed/seeder.module'; import { UserNotificationModule } from './user-notification/user-notification.module'; import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; +import { SceneModule } from './scene/scene.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -36,6 +37,7 @@ import { DeviceMessagesSubscriptionModule } from './device-messages/device-messa UserDevicePermissionModule, UserNotificationModule, SeederModule, + SceneModule, ], controllers: [AuthenticationController], }) diff --git a/src/scene/controllers/index.ts b/src/scene/controllers/index.ts new file mode 100644 index 0000000..10059c1 --- /dev/null +++ b/src/scene/controllers/index.ts @@ -0,0 +1 @@ +export * from './scene.controller'; diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts new file mode 100644 index 0000000..14712b9 --- /dev/null +++ b/src/scene/controllers/scene.controller.ts @@ -0,0 +1,114 @@ +import { SceneService } from '../services/scene.service'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AddSceneTapToRunDto } from '../dtos/add.scene.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Scene Module') +@Controller({ + version: '1', + path: 'scene', +}) +export class SceneController { + constructor(private readonly sceneService: SceneService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('tap-to-run') + async addTapToRunScene(@Body() addSceneTapToRunDto: AddSceneTapToRunDto) { + try { + const tapToRunScene = + await this.sceneService.addTapToRunScene(addSceneTapToRunDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene added successfully', + data: tapToRunScene, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('tap-to-run/:unitUuid') + async getTapToRunSceneByUnit(@Param('unitUuid') unitUuid: string) { + try { + const tapToRunScenes = + await this.sceneService.getTapToRunSceneByUnit(unitUuid); + return tapToRunScenes; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('tap-to-run/:unitUuid/:sceneId') + async deleteTapToRunScene( + @Param('unitUuid') unitUuid: string, + @Param('sceneId') sceneId: string, + ) { + try { + await this.sceneService.deleteTapToRunScene(unitUuid, sceneId); + return { + statusCode: HttpStatus.OK, + message: 'Scene Deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('tap-to-run/trigger/:sceneId') + async triggerTapToRunScene(@Param('sceneId') sceneId: string) { + try { + await this.sceneService.triggerTapToRunScene(sceneId); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene trigger successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('tap-to-run/details/:sceneId') + async triggerTapToRunSceneDetails(@Param('sceneId') sceneId: string) { + try { + const tapToRunScenes = + await this.sceneService.triggerTapToRunSceneDetails(sceneId); + return tapToRunScenes; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/scene/dtos/add.scene.dto.ts b/src/scene/dtos/add.scene.dto.ts new file mode 100644 index 0000000..d6b531d --- /dev/null +++ b/src/scene/dtos/add.scene.dto.ts @@ -0,0 +1,103 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsOptional, + IsNumber, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +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, // Set required to false + type: ExecutorProperty, + }) + @ValidateNested() + @Type(() => ExecutorProperty) + @IsOptional() // Make executorProperty optional + public executorProperty?: ExecutorProperty; +} + +export class AddSceneTapToRunDto { + @ApiProperty({ + description: 'Unit UUID', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + + @ApiProperty({ + description: 'Scene name', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneName: string; + + @ApiProperty({ + description: 'Decision expression', + required: true, + }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Actions', + required: true, + type: [Action], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + public actions: Action[]; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/scene/dtos/index.ts b/src/scene/dtos/index.ts new file mode 100644 index 0000000..fbd1865 --- /dev/null +++ b/src/scene/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.scene.dto'; diff --git a/src/scene/interface/scene.interface.ts b/src/scene/interface/scene.interface.ts new file mode 100644 index 0000000..00ebcae --- /dev/null +++ b/src/scene/interface/scene.interface.ts @@ -0,0 +1,23 @@ +export interface AddTapToRunSceneInterface { + success: boolean; + msg?: string; + result: { + id: string; + }; +} +export interface GetTapToRunSceneByUnitInterface { + success: boolean; + msg?: string; + result: { + list: Array<{ + id: string; + name: string; + status: string; + }>; + }; +} +export interface DeleteTapToRunSceneInterface { + success: boolean; + msg?: string; + result: boolean; +} diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts new file mode 100644 index 0000000..248f2e6 --- /dev/null +++ b/src/scene/scene.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { SceneService } from './services/scene.service'; +import { SceneController } from './controllers/scene.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: [SceneController], + providers: [ + SceneService, + SpaceRepository, + DeviceService, + DeviceRepository, + ProductRepository, + ], + exports: [SceneService], +}) +export class SceneModule {} diff --git a/src/scene/services/index.ts b/src/scene/services/index.ts new file mode 100644 index 0000000..3a7442e --- /dev/null +++ b/src/scene/services/index.ts @@ -0,0 +1 @@ +export * from './scene.service'; diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts new file mode 100644 index 0000000..5623956 --- /dev/null +++ b/src/scene/services/scene.service.ts @@ -0,0 +1,239 @@ +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddSceneTapToRunDto } 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 { + AddTapToRunSceneInterface, + DeleteTapToRunSceneInterface, + GetTapToRunSceneByUnitInterface, +} from '../interface/scene.interface'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class SceneService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly spaceRepository: SpaceRepository, + private readonly deviceService: DeviceService, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + this.tuya = new TuyaContext({ + baseUrl: 'https://openapi.tuyaeu.com', + accessKey, + secretKey, + }); + } + + async addTapToRunScene(addSceneTapToRunDto: AddSceneTapToRunDto) { + try { + const unit = await this.getUnitByUuid(addSceneTapToRunDto.unitUuid); + if (!unit.spaceTuyaUuid) { + throw new BadRequestException('Invalid unit UUID'); + } + const actions = addSceneTapToRunDto.actions.map((action) => { + return { + ...action, + }; + }); + + const convertedData = convertKeysToSnakeCase(actions); + for (const action of convertedData) { + if (action.action_executor === 'device_issue') { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, + false, + ); + if (device) { + action.entity_id = device.deviceTuyaUuid; + } + } + } + const path = `/v2.0/cloud/scene/rule`; + const response: AddTapToRunSceneInterface = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: unit.spaceTuyaUuid, + name: addSceneTapToRunDto.sceneName, + type: 'scene', + decision_expr: addSceneTapToRunDto.decisionExpr, + actions: convertedData, + }, + }); + 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 || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async getUnitByUuid(unitUuid: string): Promise { + 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 building 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 getTapToRunSceneByUnit(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=scene`; + const response: GetTapToRunSceneByUnitInterface = 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: 'tap_to_run', + }; + }); + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async deleteTapToRunScene(unitUuid: string, sceneId: string) { + try { + const unit = await this.getUnitByUuid(unitUuid); + if (!unit.spaceTuyaUuid) { + throw new BadRequestException('Invalid unit UUID'); + } + + const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unit.spaceTuyaUuid}`; + const response: DeleteTapToRunSceneInterface = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } + + return response; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async triggerTapToRunScene(sceneId: string) { + try { + const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`; + const response: DeleteTapToRunSceneInterface = await this.tuya.request({ + method: 'POST', + path, + }); + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + return response; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async triggerTapToRunSceneDetails(sceneId: string) { + 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 responseData = convertKeysToCamelCase(response.result); + return { + id: responseData.id, + name: responseData.name, + status: responseData.status, + type: 'tap_to_run', + actions: responseData.actions, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } +} From d6f846086cc4bfb27bb2aec8b949db4171e9d07b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:36:24 +0300 Subject: [PATCH 235/259] Add TUYA_EU_URL to Tuya configuration --- libs/common/src/config/tuya.config.ts | 1 + src/device/services/device.service.ts | 3 ++- src/group/services/group.service.ts | 3 ++- src/guards/device.guard.ts | 3 ++- src/scene/services/scene.service.ts | 3 ++- src/unit/services/unit.service.ts | 4 +++- src/users/services/user.service.ts | 4 ++-- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/libs/common/src/config/tuya.config.ts b/libs/common/src/config/tuya.config.ts index ca4dd73..ba3344e 100644 --- a/libs/common/src/config/tuya.config.ts +++ b/libs/common/src/config/tuya.config.ts @@ -5,6 +5,7 @@ export default registerAs( (): Record => ({ TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID, TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY, + TUYA_EU_URL: process.env.TUYA_EU_URL, TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET === 'true' ? true : false, }), diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index cacf266..c3da047 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -35,8 +35,9 @@ export class DeviceService { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 94cf8e0..4f1a1f7 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -18,8 +18,9 @@ export class GroupService { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/guards/device.guard.ts b/src/guards/device.guard.ts index 5d08598..179ef11 100644 --- a/src/guards/device.guard.ts +++ b/src/guards/device.guard.ts @@ -21,8 +21,9 @@ export class CheckDeviceGuard implements CanActivate { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 5623956..0c60e27 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -28,8 +28,9 @@ export class SceneService { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 3279a05..1106043 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -37,8 +37,10 @@ export class UnitService { ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/users/services/user.service.ts b/src/users/services/user.service.ts index d6b3610..774293b 100644 --- a/src/users/services/user.service.ts +++ b/src/users/services/user.service.ts @@ -9,9 +9,9 @@ export class UserService { constructor(private readonly configService: ConfigService) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); From feaae7341a2b6d81236aadc47924576cc5181e17 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:03:01 +0300 Subject: [PATCH 236/259] Refactor Scene Module: Add Update Scene Tap To Run Endpoint --- src/scene/controllers/scene.controller.ts | 33 +++++++- src/scene/dtos/index.ts | 2 +- .../dtos/{add.scene.dto.ts => scene.dto.ts} | 31 ++++++++ src/scene/services/scene.service.ts | 76 ++++++++++++++++--- 4 files changed, 126 insertions(+), 16 deletions(-) rename src/scene/dtos/{add.scene.dto.ts => scene.dto.ts} (77%) diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts index 14712b9..40af2a8 100644 --- a/src/scene/controllers/scene.controller.ts +++ b/src/scene/controllers/scene.controller.ts @@ -8,10 +8,11 @@ import { HttpStatus, Param, Post, + Put, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddSceneTapToRunDto } from '../dtos/add.scene.dto'; +import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos/scene.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @ApiTags('Scene Module') @@ -99,11 +100,37 @@ export class SceneController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('tap-to-run/details/:sceneId') - async triggerTapToRunSceneDetails(@Param('sceneId') sceneId: string) { + async getTapToRunSceneDetails(@Param('sceneId') sceneId: string) { try { const tapToRunScenes = - await this.sceneService.triggerTapToRunSceneDetails(sceneId); + await this.sceneService.getTapToRunSceneDetails(sceneId); return tapToRunScenes; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + ``; + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('tap-to-run/:sceneId') + async updateTapToRunScene( + @Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto, + @Param('sceneId') sceneId: string, + ) { + try { + const tapToRunScene = await this.sceneService.updateTapToRunScene( + updateSceneTapToRunDto, + sceneId, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene updated successfully', + data: tapToRunScene, + }; } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/scene/dtos/index.ts b/src/scene/dtos/index.ts index fbd1865..b1ca9c8 100644 --- a/src/scene/dtos/index.ts +++ b/src/scene/dtos/index.ts @@ -1 +1 @@ -export * from './add.scene.dto'; +export * from './scene.dto'; diff --git a/src/scene/dtos/add.scene.dto.ts b/src/scene/dtos/scene.dto.ts similarity index 77% rename from src/scene/dtos/add.scene.dto.ts rename to src/scene/dtos/scene.dto.ts index d6b531d..ca39212 100644 --- a/src/scene/dtos/add.scene.dto.ts +++ b/src/scene/dtos/scene.dto.ts @@ -101,3 +101,34 @@ export class AddSceneTapToRunDto { Object.assign(this, dto); } } +export class UpdateSceneTapToRunDto { + @ApiProperty({ + description: 'Scene name', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneName: string; + + @ApiProperty({ + description: 'Decision expression', + required: true, + }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Actions', + required: true, + type: [Action], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + public actions: Action[]; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 0c60e27..4ef83b3 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -5,7 +5,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddSceneTapToRunDto } from '../dtos'; +import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos'; import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; @@ -36,12 +36,24 @@ export class SceneService { }); } - async addTapToRunScene(addSceneTapToRunDto: AddSceneTapToRunDto) { + async addTapToRunScene( + addSceneTapToRunDto: AddSceneTapToRunDto, + spaceTuyaId = null, + ) { try { - const unit = await this.getUnitByUuid(addSceneTapToRunDto.unitUuid); - if (!unit.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); + let unitSpaceTuyaId; + if (!spaceTuyaId) { + const unitDetails = await this.getUnitByUuid( + addSceneTapToRunDto.unitUuid, + ); + unitSpaceTuyaId = unitDetails.spaceTuyaUuid; + if (!unitDetails) { + throw new BadRequestException('Invalid unit UUID'); + } + } else { + unitSpaceTuyaId = spaceTuyaId; } + const actions = addSceneTapToRunDto.actions.map((action) => { return { ...action, @@ -65,7 +77,7 @@ export class SceneService { method: 'POST', path, body: { - space_id: unit.spaceTuyaUuid, + space_id: unitSpaceTuyaId, name: addSceneTapToRunDto.sceneName, type: 'scene', decision_expr: addSceneTapToRunDto.decisionExpr, @@ -157,14 +169,24 @@ export class SceneService { } } } - async deleteTapToRunScene(unitUuid: string, sceneId: string) { + async deleteTapToRunScene( + unitUuid: string, + sceneId: string, + spaceTuyaId = null, + ) { try { - const unit = await this.getUnitByUuid(unitUuid); - if (!unit.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); + 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=${sceneId}&space_id=${unit.spaceTuyaUuid}`; + const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`; const response: DeleteTapToRunSceneInterface = await this.tuya.request({ method: 'DELETE', path, @@ -208,7 +230,7 @@ export class SceneService { } } } - async triggerTapToRunSceneDetails(sceneId: string) { + async getTapToRunSceneDetails(sceneId: string, withSpaceId = false) { try { const path = `/v2.0/cloud/scene/rule/${sceneId}`; const response = await this.tuya.request({ @@ -225,7 +247,37 @@ export class SceneService { status: responseData.status, type: 'tap_to_run', actions: responseData.actions, + ...(withSpaceId && { spaceId: responseData.spaceId }), }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } + } + } + async updateTapToRunScene( + updateSceneTapToRunDto: UpdateSceneTapToRunDto, + sceneId: string, + ) { + try { + const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true); + if (!spaceTuyaId.spaceId) { + throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND); + } + const addSceneTapToRunDto: AddSceneTapToRunDto = { + ...updateSceneTapToRunDto, + unitUuid: null, + }; + const newTapToRunScene = await this.addTapToRunScene( + addSceneTapToRunDto, + spaceTuyaId.spaceId, + ); + if (newTapToRunScene.id) { + await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); + return newTapToRunScene; + } } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException From c5e24daaa58700b586a21917419fd66b0536478d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:43:54 +0300 Subject: [PATCH 237/259] refactor error message --- src/scene/services/scene.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 4ef83b3..4083067 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -113,7 +113,7 @@ export class SceneService { relations: ['spaceType'], }); if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { - throw new BadRequestException('Invalid building UUID'); + throw new BadRequestException('Invalid unit UUID'); } return { uuid: unit.uuid, From f4205c39b3d1cbdad509e2b1baa4cdf4f088c44f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 25 Jun 2024 00:38:46 +0300 Subject: [PATCH 238/259] Add method to get device by Tuya UUID and update getDevicesInGetawayTuya method --- src/device/services/device.service.ts | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index c3da047..ef45960 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -53,6 +53,14 @@ export class DeviceService { ...(withProductDevice && { relations: ['productDevice'] }), }); } + async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { + return await this.deviceRepository.findOne({ + where: { + deviceTuyaUuid, + }, + relations: ['productDevice'], + }); + } async addDeviceUser(addDeviceDto: AddDeviceDto) { try { const device = await this.getDeviceDetailsByDeviceIdTuya( @@ -434,19 +442,32 @@ export class DeviceService { deviceDetails.deviceTuyaUuid, ); + const devices = await Promise.all( + response.map(async (device: any) => { + const deviceDetails = await this.getDeviceByDeviceTuyaUuid(device.id); + if (deviceDetails.deviceTuyaUuid) { + return { + ...device, + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + }; + } + return null; + }), + ); + return { uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, - device: response, + devices: devices.filter((device) => device !== null), }; } catch (error) { - throw new HttpException( - error.message || 'Device Not Found', - HttpStatus.NOT_FOUND, - ); + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } + async getDevicesInGetawayTuya( deviceId: string, ): Promise { From b0dba387fb233c20730655c575bcc368628862e6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:02:55 +0300 Subject: [PATCH 239/259] Refactor device controller and service --- src/device/controllers/device.controller.ts | 6 +++--- src/device/services/device.service.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index c9fc59c..acf1632 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -200,10 +200,10 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('getaway/:gatewayUuid/devices') - async getDevicesInGetaway(@Param('gatewayUuid') gatewayUuid: string) { + @Get('gateway/:gatewayUuid/devices') + async getDevicesInGateway(@Param('gatewayUuid') gatewayUuid: string) { try { - return await this.deviceService.getDevicesInGetaway(gatewayUuid); + return await this.deviceService.getDevicesInGateway(gatewayUuid); } catch (error) { throw new HttpException( error.message || 'Internal server error', diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ef45960..bc9ed2c 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -428,7 +428,7 @@ export class DeviceService { }); return device.permission[0].permissionType.type; } - async getDevicesInGetaway(gatewayUuid: string) { + async getDevicesInGateway(gatewayUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(gatewayUuid); @@ -438,7 +438,7 @@ export class DeviceService { throw new BadRequestException('This is not a gateway device'); } - const response = await this.getDevicesInGetawayTuya( + const response = await this.getDevicesInGatewayTuya( deviceDetails.deviceTuyaUuid, ); @@ -468,7 +468,7 @@ export class DeviceService { } } - async getDevicesInGetawayTuya( + async getDevicesInGatewayTuya( deviceId: string, ): Promise { try { @@ -479,7 +479,7 @@ export class DeviceService { }); const camelCaseResponse = response.result.map((device: any) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { product_id, category, id, ...rest } = device; + const { product_id, category, ...rest } = device; const camelCaseDevice = convertKeysToCamelCase({ ...rest }); return camelCaseDevice as GetDeviceDetailsInterface[]; }); From d86b2ddeb70309e75c9425c228c7b60212411206 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:15:21 +0300 Subject: [PATCH 240/259] Refactor device service to include Tuya UUID in device response --- src/device/services/device.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index bc9ed2c..868874d 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -446,8 +446,11 @@ export class DeviceService { response.map(async (device: any) => { const deviceDetails = await this.getDeviceByDeviceTuyaUuid(device.id); if (deviceDetails.deviceTuyaUuid) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...rest } = device; return { - ...device, + ...rest, + tuyaUuid: deviceDetails.deviceTuyaUuid, uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, From 09074f83e2e0465083addb1d7ed59c67f5963c89 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:08 +0300 Subject: [PATCH 241/259] Add Door Lock Controller with temporary password functionality --- .../controllers/door.lock.controller.ts | 182 ++++++++++++++++++ src/door-lock/controllers/index.ts | 1 + 2 files changed, 183 insertions(+) create mode 100644 src/door-lock/controllers/door.lock.controller.ts create mode 100644 src/door-lock/controllers/index.ts diff --git a/src/door-lock/controllers/door.lock.controller.ts b/src/door-lock/controllers/door.lock.controller.ts new file mode 100644 index 0000000..f3605ef --- /dev/null +++ b/src/door-lock/controllers/door.lock.controller.ts @@ -0,0 +1,182 @@ +import { DoorLockService } from '../services/door.lock.service'; +import { + Body, + Controller, + Post, + Param, + HttpException, + HttpStatus, + Get, + Delete, + UseGuards, +} 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 { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Door Lock Module') +@Controller({ + version: '1', + path: 'door-lock', +}) +export class DoorLockController { + constructor(private readonly doorLockService: DoorLockService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('temporary-password/online/:doorLockUuid') + async addOnlineTemporaryPassword( + @Body() addDoorLockDto: AddDoorLockOnlineDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOnlineTemporaryPassword( + addDoorLockDto, + doorLockUuid, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'online temporary password added successfully', + data: { + id: temporaryPassword.id, + }, + }; + } 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/:doorLockUuid') + async addOfflineOneTimeTemporaryPassword( + @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'offline temporary password added successfully', + 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/:doorLockUuid') + async addOfflineMultipleTimeTemporaryPassword( + @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + const temporaryPassword = + await this.doorLockService.addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'offline temporary password added successfully', + data: temporaryPassword, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('temporary-password/online/:doorLockUuid') + async getOnlineTemporaryPasswords( + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + return await this.doorLockService.getOnlineTemporaryPasswords( + doorLockUuid, + ); + } 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, + ) { + try { + return await this.doorLockService.getOfflineOneTimeTemporaryPasswords( + doorLockUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('temporary-password/offline/multiple-time/:doorLockUuid') + async getOfflineMultipleTimeTemporaryPasswords( + @Param('doorLockUuid') doorLockUuid: string, + ) { + try { + return await this.doorLockService.getOfflineMultipleTimeTemporaryPasswords( + doorLockUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('temporary-password/: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, + ); + } + } +} diff --git a/src/door-lock/controllers/index.ts b/src/door-lock/controllers/index.ts new file mode 100644 index 0000000..d7d3c84 --- /dev/null +++ b/src/door-lock/controllers/index.ts @@ -0,0 +1 @@ +export * from './door.lock.controller'; From 062eb7660b91af06b5ded59c05a6dd421050268e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:20 +0300 Subject: [PATCH 242/259] Add door lock DTOs for adding offline and online temporary passwords --- src/door-lock/dtos/add.offline-temp.dto.ts | 36 ++++++++++ src/door-lock/dtos/add.online-temp.dto.ts | 84 ++++++++++++++++++++++ src/door-lock/dtos/index.ts | 1 + 3 files changed, 121 insertions(+) create mode 100644 src/door-lock/dtos/add.offline-temp.dto.ts create mode 100644 src/door-lock/dtos/add.online-temp.dto.ts create mode 100644 src/door-lock/dtos/index.ts diff --git a/src/door-lock/dtos/add.offline-temp.dto.ts b/src/door-lock/dtos/add.offline-temp.dto.ts new file mode 100644 index 0000000..f30cd10 --- /dev/null +++ b/src/door-lock/dtos/add.offline-temp.dto.ts @@ -0,0 +1,36 @@ +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; + + @ApiProperty({ + description: 'effectiveTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public effectiveTime: string; + + @ApiProperty({ + description: 'invalidTime', + required: true, + }) + @IsString() + @IsNotEmpty() + public invalidTime: string; +} diff --git a/src/door-lock/dtos/add.online-temp.dto.ts b/src/door-lock/dtos/add.online-temp.dto.ts new file mode 100644 index 0000000..bb84068 --- /dev/null +++ b/src/door-lock/dtos/add.online-temp.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsEnum, + Length, +} 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 AddDoorLockOnlineDto { + @ApiProperty({ + description: 'name', + required: true, + }) + @IsString() + @IsNotEmpty() + public name: 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 }) + @Type(() => ScheduleDto) + public scheduleList: ScheduleDto[]; +} diff --git a/src/door-lock/dtos/index.ts b/src/door-lock/dtos/index.ts new file mode 100644 index 0000000..3b78a05 --- /dev/null +++ b/src/door-lock/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.online-temp.dto'; From 46debaaafc9cf758a8d7dfbea6b1f1f3bd840c38 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:31 +0300 Subject: [PATCH 243/259] Add door lock interfaces --- .../interfaces/door.lock.interface.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/door-lock/interfaces/door.lock.interface.ts diff --git a/src/door-lock/interfaces/door.lock.interface.ts b/src/door-lock/interfaces/door.lock.interface.ts new file mode 100644 index 0000000..5b89061 --- /dev/null +++ b/src/door-lock/interfaces/door.lock.interface.ts @@ -0,0 +1,57 @@ +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; + }; + msg?: string; +} +export interface addDeviceObjectInterface { + name: string; + encryptedPassword: string; + effectiveTime: string; + invalidTime: string; + ticketId: string; + scheduleList?: any[]; +} + +export interface ScheduleDto { + effectiveTime: string; + invalidTime: string; + workingDay: WorkingDays[]; +} + +export interface AddDoorLockOnlineInterface { + name: string; + password: string; + effectiveTime: string; + invalidTime: string; + scheduleList?: ScheduleDto[]; +} +export interface getPasswordInterface { + success: boolean; + result: [ + { + effective_time: number; + id: number; + invalid_time: number; + name: 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; +} From b9a15134a21b8a4501e2f8ef8766cf03338ec8b6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:41 +0300 Subject: [PATCH 244/259] feat: Add DoorLockService and PasswordEncryptionService --- src/door-lock/services/door.lock.service.ts | 575 ++++++++++++++++++ src/door-lock/services/encryption.services.ts | 57 ++ src/door-lock/services/index.ts | 1 + 3 files changed, 633 insertions(+) create mode 100644 src/door-lock/services/door.lock.service.ts create mode 100644 src/door-lock/services/encryption.services.ts create mode 100644 src/door-lock/services/index.ts diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts new file mode 100644 index 0000000..d60c4bd --- /dev/null +++ b/src/door-lock/services/door.lock.service.ts @@ -0,0 +1,575 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { + AddDoorLockOnlineInterface, + addDeviceObjectInterface, + createTickInterface, + deleteTemporaryPasswordInterface, + getPasswordInterface, +} 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 { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class DoorLockService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly deviceRepository: DeviceRepository, + private readonly passwordEncryptionService: PasswordEncryptionService, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + async deleteDoorLockPassword(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 deletePass = await this.deleteDoorLockPasswordTuya( + deviceDetails.deviceTuyaUuid, + passwordId, + ); + + if (!deletePass.success) { + throw new HttpException('PasswordId not found', HttpStatus.NOT_FOUND); + } + return deletePass; + } catch (error) { + throw new HttpException( + error.message || 'Error deleting temporary password', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async deleteDoorLockPasswordTuya( + doorLockUuid: string, + passwordId: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords/${passwordId}`; + + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as deleteTemporaryPasswordInterface; + } catch (error) { + throw new HttpException( + 'Error deleting temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOfflineMultipleTimeTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + 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); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || + 'Error getting offline multiple time temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOfflineOneTimeTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result.filter( + (item) => + (!item.schedule_list || item.schedule_list.length === 0) && + item.type === 1, + ); + + return convertKeysToCamelCase(passwordFiltered); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || 'Error getting offline one time temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getOnlineTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya( + deviceDetails.deviceTuyaUuid, + ); + + if (passwords.result.length > 0) { + const passwordFiltered = passwords.result + .filter((item) => item.schedule_list && item.schedule_list.length > 0) + .map((password: any) => { + 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 convertKeysToCamelCase(passwordFiltered); + } + + return passwords; + } catch (error) { + throw new HttpException( + error.message || 'Error getting online temporary passwords', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getTemporaryPasswordsTuya( + doorLockUuid: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords?valid=true`; + + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as getPasswordInterface; + } catch (error) { + throw new HttpException( + 'Error getting temporary passwords from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineMultipleTimeTemporaryPassword( + addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + doorLockUuid: string, + ) { + try { + const createOnlinePass = await this.addOnlineTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + 'multiple', + false, + ); + if (!createOnlinePass) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( + addDoorLockOfflineTempDto, + createOnlinePass.id, + createOnlinePass.deviceTuyaUuid, + 'multiple', + ); + if (!createOnceOfflinePass.success) { + throw new HttpException( + createOnceOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + return { + result: createOnceOfflinePass.result, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error adding offline temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineOneTimeTemporaryPassword( + addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + doorLockUuid: string, + ) { + try { + const createOnlinePass = await this.addOnlineTemporaryPassword( + addDoorLockOfflineTempDto, + doorLockUuid, + 'once', + false, + ); + if (!createOnlinePass) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( + addDoorLockOfflineTempDto, + createOnlinePass.id, + createOnlinePass.deviceTuyaUuid, + 'once', + ); + if (!createOnceOfflinePass.success) { + throw new HttpException( + createOnceOfflinePass.msg, + HttpStatus.BAD_REQUEST, + ); + } + return { + result: createOnceOfflinePass.result, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error adding offline temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOfflineTemporaryPasswordTuya( + addDoorLockDto: AddDoorLockOnlineInterface, + onlinePassId: number, + doorLockUuid: string, + type: string, + ): Promise { + try { + const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`; + + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: addDoorLockDto.name, + ...(type === 'multiple' && { + effective_time: addDoorLockDto.effectiveTime, + invalid_time: addDoorLockDto.invalidTime, + }), + + type, + password_id: onlinePassId, + }, + }); + + return response as createTickInterface; + } catch (error) { + throw new HttpException( + 'Error adding offline temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addOnlineTemporaryPassword( + addDoorLockDto: AddDoorLockOnlineInterface, + doorLockUuid: string, + type: string = 'once', + isOnline: boolean = true, + ) { + try { + const passwordData = await this.getTicketAndEncryptedPassword( + doorLockUuid, + addDoorLockDto.password, + ); + if ( + !passwordData.ticketKey || + !passwordData.encryptedPassword || + !passwordData.deviceTuyaUuid + ) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + const addDeviceObj: addDeviceObjectInterface = { + ...addDoorLockDto, + ...passwordData, + }; + const createPass = await this.addOnlineTemporaryPasswordTuya( + addDeviceObj, + passwordData.deviceTuyaUuid, + type, + isOnline, + ); + + if (!createPass.success) { + throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST); + } + return { + id: createPass.result.id, + deviceTuyaUuid: passwordData.deviceTuyaUuid, + }; + } catch (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 { + 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 addOnlineTemporaryPasswordTuya( + addDeviceObj: addDeviceObjectInterface, + doorLockUuid: string, + type: string, + isOnline: boolean = true, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; + let scheduleList; + if (isOnline) { + 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.name, + password: addDeviceObj.encryptedPassword, + effective_time: addDeviceObj.effectiveTime, + invalid_time: addDeviceObj.invalidTime, + password_type: 'ticket', + ticket_id: addDeviceObj.ticketId, + ...(isOnline && { + schedule_list: scheduleList, + }), + type: type === 'multiple' ? '0' : '1', + }, + }); + + 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); + } + } +} diff --git a/src/door-lock/services/encryption.services.ts b/src/door-lock/services/encryption.services.ts new file mode 100644 index 0000000..7e56afe --- /dev/null +++ b/src/door-lock/services/encryption.services.ts @@ -0,0 +1,57 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import * as CryptoJS from 'crypto-js'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class PasswordEncryptionService { + constructor(private readonly configService: ConfigService) {} + + encrypt(plainText: string, secretKey: string): string { + const keyBytes = CryptoJS.enc.Utf8.parse(secretKey); + + const encrypted = CryptoJS.AES.encrypt(plainText, keyBytes, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + + return encrypted.ciphertext.toString(CryptoJS.enc.Hex); + } + + decrypt(encryptedText: string, secretKey: string): string { + const keyBytes = CryptoJS.enc.Utf8.parse(secretKey); + + const decrypted = CryptoJS.AES.decrypt( + { + ciphertext: CryptoJS.enc.Hex.parse(encryptedText), + } as any, + keyBytes, + { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }, + ); + + return decrypted.toString(CryptoJS.enc.Utf8); + } + + generateEncryptedPassword( + plainTextPassword: string, + ticketKey: string, + ): string { + try { + const accessSecret = this.configService.get( + 'auth-config.SECRET_KEY', + ); + // The accessSecret must be 32 bytes, ensure it is properly padded or truncated + const paddedAccessSecret = accessSecret.padEnd(32, '0').slice(0, 32); + const plainTextTicketKey = this.decrypt(ticketKey, paddedAccessSecret); + + return this.encrypt(plainTextPassword, plainTextTicketKey); + } catch (error) { + throw new HttpException( + `Error encrypting password: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/door-lock/services/index.ts b/src/door-lock/services/index.ts new file mode 100644 index 0000000..e847a10 --- /dev/null +++ b/src/door-lock/services/index.ts @@ -0,0 +1 @@ +export * from './door.lock.service'; From a0a5b992db54f9ec04988a66f065156c9959e0ae Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:50 +0300 Subject: [PATCH 245/259] Add DoorLockModule with controller, service, and dependencies --- src/door-lock/door.lock.module.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/door-lock/door.lock.module.ts diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts new file mode 100644 index 0000000..3b1920e --- /dev/null +++ b/src/door-lock/door.lock.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DoorLockService } from './services/door.lock.service'; +import { DoorLockController } from './controllers/door.lock.controller'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { PasswordEncryptionService } from './services/encryption.services'; +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [DoorLockController], + providers: [DoorLockService, PasswordEncryptionService, DeviceRepository], + exports: [DoorLockService], +}) +export class DoorLockModule {} From e2359e28a90fa11455e07ee83bb6ae1781b5e92c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:47:55 +0300 Subject: [PATCH 246/259] Add enum for working days in common library --- libs/common/src/constants/working-days.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 libs/common/src/constants/working-days.ts diff --git a/libs/common/src/constants/working-days.ts b/libs/common/src/constants/working-days.ts new file mode 100644 index 0000000..d870d3c --- /dev/null +++ b/libs/common/src/constants/working-days.ts @@ -0,0 +1,9 @@ +export enum WorkingDays { + Sun = 'Sun', + Mon = 'Mon', + Tue = 'Tue', + Wed = 'Wed', + Thu = 'Thu', + Fri = 'Fri', + Sat = 'Sat', +} From 88685f40b773de1e248685f0e94dbad6918affc2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:48:00 +0300 Subject: [PATCH 247/259] Add DoorLockModule to app module imports --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 59b4275..c95d1bf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { SeederModule } from '@app/common/seed/seeder.module'; import { UserNotificationModule } from './user-notification/user-notification.module'; import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; import { SceneModule } from './scene/scene.module'; +import { DoorLockModule } from './door-lock/door.lock.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -38,6 +39,7 @@ import { SceneModule } from './scene/scene.module'; UserNotificationModule, SeederModule, SceneModule, + DoorLockModule, ], controllers: [AuthenticationController], }) From 05e51c79ebc3a665a127abce9f5e851b2d3fb471 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:54:01 +0300 Subject: [PATCH 248/259] Remove unnecessary comment and improve code readability in DoorLockService. --- src/door-lock/services/door.lock.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index d60c4bd..fbd520c 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -481,7 +481,6 @@ export class DoorLockService { } }); - // Convert the binary string to an integer const workingDayValue = parseInt(binaryString, 2); return workingDayValue; From 53e2be63db457c38e67c2bd7652995f168a4e63c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:54:35 +0300 Subject: [PATCH 249/259] Convert binary string to integer in DoorLockService --- src/door-lock/services/door.lock.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index fbd520c..d60c4bd 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -481,6 +481,7 @@ export class DoorLockService { } }); + // Convert the binary string to an integer const workingDayValue = parseInt(binaryString, 2); return workingDayValue; From 8bc09618692afb773d49f946e67d354384683d46 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:20:45 +0300 Subject: [PATCH 250/259] Add optional scheduleList property to AddDoorLockOnlineDto class --- src/door-lock/dtos/add.online-temp.dto.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/door-lock/dtos/add.online-temp.dto.ts b/src/door-lock/dtos/add.online-temp.dto.ts index bb84068..269aabc 100644 --- a/src/door-lock/dtos/add.online-temp.dto.ts +++ b/src/door-lock/dtos/add.online-temp.dto.ts @@ -6,6 +6,7 @@ import { ValidateNested, IsEnum, Length, + IsOptional, } from 'class-validator'; import { Type } from 'class-transformer'; import { WorkingDays } from '@app/common/constants/working-days'; @@ -79,6 +80,7 @@ export class AddDoorLockOnlineDto { }) @IsArray() @ValidateNested({ each: true }) + @IsOptional() @Type(() => ScheduleDto) public scheduleList: ScheduleDto[]; } From 0702d3892f3f3c3b7603b22167dc33ac175ac480 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:20:55 +0300 Subject: [PATCH 251/259] Refactor password filtering and mapping logic --- src/door-lock/services/door.lock.service.ts | 28 +++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index d60c4bd..09cd80f 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -168,18 +168,24 @@ export class DoorLockService { if (passwords.result.length > 0) { const passwordFiltered = passwords.result - .filter((item) => item.schedule_list && item.schedule_list.length > 0) + .filter((item) => item.type === 1) .map((password: any) => { - password.schedule_list = password.schedule_list.map((schedule) => { - schedule.working_day = this.getDaysFromWorkingDayValue( - schedule.working_day, + 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; + }, ); - schedule.effective_time = this.minutesToTime( - schedule.effective_time, - ); - schedule.invalid_time = this.minutesToTime(schedule.invalid_time); - return schedule; - }); + } return password; }); @@ -343,7 +349,7 @@ export class DoorLockService { addDeviceObj, passwordData.deviceTuyaUuid, type, - isOnline, + addDeviceObj.scheduleList ? isOnline : false, ); if (!createPass.success) { From 63a8c5664e54740376557e0566bfcd5599ab46e7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:17:35 +0300 Subject: [PATCH 252/259] Fix password filtering in DoorLockService --- src/door-lock/services/door.lock.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index 09cd80f..e88fee4 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -136,7 +136,7 @@ export class DoorLockService { const passwordFiltered = passwords.result.filter( (item) => (!item.schedule_list || item.schedule_list.length === 0) && - item.type === 1, + item.type === 0, //temp solution ); return convertKeysToCamelCase(passwordFiltered); @@ -168,7 +168,7 @@ export class DoorLockService { if (passwords.result.length > 0) { const passwordFiltered = passwords.result - .filter((item) => item.type === 1) + .filter((item) => item.type === 0) //temp solution .map((password: any) => { if (password.schedule_list?.length > 0) { password.schedule_list = password.schedule_list.map( @@ -456,7 +456,8 @@ export class DoorLockService { ...(isOnline && { schedule_list: scheduleList, }), - type: type === 'multiple' ? '0' : '1', + + type: '0', //temporary solution, }, }); From ddfc55d9dddc2d17f3a99a374e50120e5fd35083 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:29:54 +0300 Subject: [PATCH 253/259] Add endpoint to get devices by unit ID --- src/device/controllers/device.controller.ts | 14 +++++++- src/device/services/device.service.ts | 40 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index acf1632..a54b5a9 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -83,7 +83,19 @@ export class DeviceController { ); } } - + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('unit/:unitUuid') + async getDevicesByUnitId(@Param('unitUuid') unitUuid: string) { + try { + return await this.deviceService.getDevicesByUnitId(unitUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) @Put('room') diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 868874d..8cbe772 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -24,6 +24,7 @@ import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { In } from 'typeorm'; import { ProductType } from '@app/common/constants/product-type.enum'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; @Injectable() export class DeviceService { @@ -32,6 +33,7 @@ export class DeviceService { private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, private readonly productRepository: ProductRepository, + private readonly spaceRepository: SpaceRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -538,4 +540,42 @@ export class DeviceService { ); } } + async getDevicesByUnitId(unitUuid: string) { + try { + const spaces = await this.spaceRepository.find({ + where: { + parent: { + uuid: unitUuid, + }, + }, + relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + }); + + const devices = spaces.flatMap((space) => { + return space.devicesSpaceEntity.map((device) => device); + }); + + const devicesData = await Promise.all( + devices.map(async (device) => { + return { + haveRoom: true, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: PermissionType.CONTROLLABLE, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + } as GetDeviceDetailsInterface; + }), + ); + + return devicesData; + } catch (error) { + throw new HttpException( + 'This unit does not have any devices', + HttpStatus.NOT_FOUND, + ); + } + } } From 5a5d97894509f38abba67d5f4d774d1e009a1290 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:51:33 +0300 Subject: [PATCH 254/259] Refactor SceneService to update device uuid in actions --- src/scene/services/scene.service.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 4083067..b45caf8 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -241,12 +241,30 @@ export class SceneService { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } const responseData = convertKeysToCamelCase(response.result); + const actions = responseData.actions.map((action) => { + return { + ...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; + } + } + } + return { id: responseData.id, name: responseData.name, status: responseData.status, type: 'tap_to_run', - actions: responseData.actions, + actions: actions, ...(withSpaceId && { spaceId: responseData.spaceId }), }; } catch (err) { From b7812271726c450ad2abfdd7376d56ee42f18db5 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:34:41 +0300 Subject: [PATCH 255/259] Add logging interceptor --- src/app.module.ts | 8 ++++ src/interceptors/logging.interceptor.ts | 63 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/interceptors/logging.interceptor.ts diff --git a/src/app.module.ts b/src/app.module.ts index c95d1bf..0b5fd4a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,8 @@ import { UserNotificationModule } from './user-notification/user-notification.mo import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; 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'; @Module({ imports: [ ConfigModule.forRoot({ @@ -42,5 +44,11 @@ import { DoorLockModule } from './door-lock/door.lock.module'; DoorLockModule, ], controllers: [AuthenticationController], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + ], }) export class AuthModule {} diff --git a/src/interceptors/logging.interceptor.ts b/src/interceptors/logging.interceptor.ts new file mode 100644 index 0000000..e74f4c2 --- /dev/null +++ b/src/interceptors/logging.interceptor.ts @@ -0,0 +1,63 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, throwError } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body } = request; + + return next.handle().pipe( + map((response) => { + // Filter out sensitive fields from the request body + const filteredRequestBody = this.filterSensitiveFields(body); + console.log( + '-------------------------------------------------------------------', + ); + console.log(`Request Method: ${method}`); + console.log(`Request URL: ${url}`); + if ( + filteredRequestBody && + Object.keys(filteredRequestBody).length > 0 + ) { + console.log(`Request Body: ${JSON.stringify(filteredRequestBody)}`); + } + // Filter out sensitive fields from the response + const filteredResponse = this.filterSensitiveFields(response); + console.log(`Response: ${JSON.stringify(filteredResponse)}`); + return filteredResponse; + }), + catchError((error) => { + // Do not log anything if there is an error + return throwError(error); + }), + ); + } + + private filterSensitiveFields(data: any): any { + const blacklist = ['password', 'refreshToken', 'accessToken', 'otp']; + + if (data && typeof data === 'object' && !Array.isArray(data)) { + return Object.keys(data).reduce((acc, key) => { + if (blacklist.includes(key)) { + acc[key] = '[FILTERED]'; + } else if (typeof data[key] === 'object' && data[key] !== null) { + acc[key] = this.filterSensitiveFields(data[key]); + } else { + acc[key] = data[key]; + } + return acc; + }, {}); + } else if (Array.isArray(data)) { + return data.map((item) => this.filterSensitiveFields(item)); + } + + return data; + } +} From 2ff03120875887c0bee51d51e264810d3a3bfe34 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:39:31 +0300 Subject: [PATCH 256/259] Filter out sensitive fields from request and response bodies for logging --- src/interceptors/logging.interceptor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interceptors/logging.interceptor.ts b/src/interceptors/logging.interceptor.ts index e74f4c2..9e06186 100644 --- a/src/interceptors/logging.interceptor.ts +++ b/src/interceptors/logging.interceptor.ts @@ -15,7 +15,7 @@ export class LoggingInterceptor implements NestInterceptor { return next.handle().pipe( map((response) => { - // Filter out sensitive fields from the request body + // Filter out sensitive fields from the request body for logging const filteredRequestBody = this.filterSensitiveFields(body); console.log( '-------------------------------------------------------------------', @@ -28,10 +28,10 @@ export class LoggingInterceptor implements NestInterceptor { ) { console.log(`Request Body: ${JSON.stringify(filteredRequestBody)}`); } - // Filter out sensitive fields from the response + // Filter out sensitive fields from the response for logging const filteredResponse = this.filterSensitiveFields(response); console.log(`Response: ${JSON.stringify(filteredResponse)}`); - return filteredResponse; + return response; // Return the actual response unmodified }), catchError((error) => { // Do not log anything if there is an error From 2b8ba755b184999d1d921c3e1c42c8975839a9dd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:28:04 +0300 Subject: [PATCH 257/259] Add Jest configuration and dummy test --- jest.config.js | 24 ++++++++++++++++++++++++ libs/common/dummy.spec.ts | 5 +++++ package.json | 30 +++--------------------------- src/dummy.spec.ts | 5 +++++ 4 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 jest.config.js create mode 100644 libs/common/dummy.spec.ts create mode 100644 src/dummy.spec.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..14b1042 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + moduleFileExtensions: [ + "js", + "json", + "ts" + ], + rootDir: ".", + testRegex: ".*\\.spec\\.ts$", + transform: { + "^.+\\.(t|j)s$": "ts-jest" + }, + collectCoverageFrom: [ + "**/*.(t|j)s" + ], + coverageDirectory: "./coverage", + testEnvironment: "node", + roots: [ + "/src/", + "/libs/" + ], + moduleNameMapper: { + "^@app/common(|/.*)$": "/libs/common/src/$1" + } +}; diff --git a/libs/common/dummy.spec.ts b/libs/common/dummy.spec.ts new file mode 100644 index 0000000..70a64c3 --- /dev/null +++ b/libs/common/dummy.spec.ts @@ -0,0 +1,5 @@ +describe('Dummy Test', () => { + it('should pass', () => { + expect(true).toBe(true); + }); +}); diff --git a/package.json b/package.json index 0136b92..52c5cf7 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "start:debug": "npx nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", + "test": "jest --config jest.config.js", + "test:watch": "jest --watch --config jest.config.js", + "test:cov": "jest --coverage --config jest.config.js", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" }, @@ -72,29 +72,5 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": ".", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "./coverage", - "testEnvironment": "node", - "roots": [ - "/apps/", - "/libs/" - ], - "moduleNameMapper": { - "^@app/common(|/.*)$": "/libs/common/src/$1" - } } } diff --git a/src/dummy.spec.ts b/src/dummy.spec.ts new file mode 100644 index 0000000..70a64c3 --- /dev/null +++ b/src/dummy.spec.ts @@ -0,0 +1,5 @@ +describe('Dummy Test', () => { + it('should pass', () => { + expect(true).toBe(true); + }); +}); From bcdd5f1aec081f730beca160de98177f3760b0d3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:25:06 +0300 Subject: [PATCH 258/259] test --- src/app.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.module.ts b/src/app.module.ts index 0b5fd4a..19cabd3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,6 +42,7 @@ import { LoggingInterceptor } from './interceptors/logging.interceptor'; SeederModule, SceneModule, DoorLockModule, + // ], controllers: [AuthenticationController], providers: [ From 2c8f31ec7c80dcc0398fe6beec9afe08a8fcf208 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:26:41 +0300 Subject: [PATCH 259/259] test --- src/users/controllers/user.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/users/controllers/user.controller.ts b/src/users/controllers/user.controller.ts index 09518e8..9d84c83 100644 --- a/src/users/controllers/user.controller.ts +++ b/src/users/controllers/user.controller.ts @@ -14,7 +14,7 @@ export class UserController { @ApiBearerAuth() @UseGuards(AdminRoleGuard) - @Get('list') + @Get('list1') async userList(@Query() userListDto: UserListDto) { try { return await this.userService.userDetails(userListDto);