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/.github/workflows/dev_syncrow(dev).yml b/.github/workflows/dev_syncrow(dev).yml new file mode 100644 index 0000000..d38510c --- /dev/null +++ b/.github/workflows/dev_syncrow(dev).yml @@ -0,0 +1,55 @@ +name: Backend deployment to Azure App Service + +on: + push: + branches: + - 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: + runs-on: ubuntu-latest + + 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: Log in to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Log in to Azure Container Registry + run: az acr login --name ${{ env.ACR_REGISTRY }} + + - name: List build output + run: ls -R dist/ + + - 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 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/.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 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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4be2071 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install +RUN npm install -g @nestjs/cli + +COPY . . + +RUN npm run build + +EXPOSE 4000 + +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md index dee6f14..67eb623 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. @@ -38,3 +57,53 @@ $ 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) + + +## 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 | | | + | +------------------+----------------+ | + +-----------------------------------------------------------------+ 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 03810e7..0000000 --- a/apps/auth/src/auth.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; - -@Module({ - imports: [], - controllers: [AuthController], - 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/main.ts b/apps/auth/src/main.ts deleted file mode 100644 index d1e14e8..0000000 --- a/apps/auth/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { AuthModule } from './auth.module'; - -async function bootstrap() { - const app = await NestFactory.create(AuthModule); - await app.listen(7001); -} -bootstrap(); 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/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.controller.ts b/apps/backend/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/apps/backend/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} 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.service.ts b/apps/backend/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/apps/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts deleted file mode 100644 index 7af4000..0000000 --- a/apps/backend/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(7000); -} -bootstrap(); 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/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/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d841218 Binary files /dev/null and b/bun.lockb differ 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/libs/common/src/auth/auth.module.ts b/libs/common/src/auth/auth.module.ts new file mode 100644 index 0000000..b6c6d45 --- /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 } 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'; +import { RefreshTokenStrategy } from './strategies/refresh-token.strategy'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + PassportModule, + JwtModule.register({}), + HelperModule, + ], + providers: [ + JwtStrategy, + RefreshTokenStrategy, + 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..9b73050 --- /dev/null +++ b/libs/common/src/auth/interfaces/auth.interface.ts @@ -0,0 +1,8 @@ +export class AuthInterface { + email: string; + userId: number; + uuid: string; + sessionId: string; + id: number; + roles?: string[]; +} 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..f1c1c0d --- /dev/null +++ b/libs/common/src/auth/services/auth.service.ts @@ -0,0 +1,94 @@ +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 { + constructor( + private jwtService: JwtService, + private readonly userRepository: UserRepository, + private readonly sessionRepository: UserSessionRepository, + private readonly helperHashService: HelperHashService, + private readonly configService: ConfigService, + ) {} + + async validateUser(email: string, pass: string): Promise { + const user = await this.userRepository.findOne({ + where: { + email, + }, + relations: ['roles.roleType'], + }); + + 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 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, + userId: user.userId, + 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; + } + + 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 new file mode 100644 index 0000000..d548dd8 --- /dev/null +++ b/libs/common/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,40 @@ +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 JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + 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, + 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 new file mode 100644 index 0000000..b6d6c2a --- /dev/null +++ b/libs/common/src/auth/strategies/refresh-token.strategy.ts @@ -0,0 +1,43 @@ +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, + userUuid: payload.uuid, + uuid: payload.uuid, + sessionId: payload.sessionId, + roles: payload?.roles, + }; + } 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..8c3e78c --- /dev/null +++ b/libs/common/src/common.module.ts @@ -0,0 +1,23 @@ +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, + isGlobal: true, + }), + 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..bd249e7 --- /dev/null +++ b/libs/common/src/config/email.config.ts @@ -0,0 +1,12 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'email-config', + (): Record => ({ + 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/config/index.ts b/libs/common/src/config/index.ts new file mode 100644 index 0000000..b642e54 --- /dev/null +++ b/libs/common/src/config/index.ts @@ -0,0 +1,6 @@ +import emailConfig from './email.config'; +import superAdminConfig from './super.admin.config'; +import tuyaConfig from './tuya.config'; +import oneSignalConfig from './onesignal.config'; + +export default [emailConfig, superAdminConfig, tuyaConfig, oneSignalConfig]; 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/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/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..0b41b23 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/index.ts @@ -0,0 +1,212 @@ +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 obj = this.handleMessage(data); + this.event.emit(TuyaMessageSubscribeWebsocket.data, this.server, obj); + } 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..ba3344e --- /dev/null +++ b/libs/common/src/config/tuya.config.ts @@ -0,0 +1,12 @@ +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, + TUYA_EU_URL: process.env.TUYA_EU_URL, + TRUN_ON_TUYA_SOCKET: + process.env.TRUN_ON_TUYA_SOCKET === 'true' ? true : false, + }), +); 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/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/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts new file mode 100644 index 0000000..f0d24ad --- /dev/null +++ b/libs/common/src/constants/product-type.enum.ts @@ -0,0 +1,8 @@ +export enum ProductType { + AC = 'AC', + GW = 'GW', + CPS = 'CPS', + DL = 'DL', + WPS = 'WPS', + TH_G = '3G', +} 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..3051b04 --- /dev/null +++ b/libs/common/src/constants/role.type.enum.ts @@ -0,0 +1,4 @@ +export enum RoleType { + SUPER_ADMIN = 'SUPER_ADMIN', + ADMIN = 'ADMIN', +} 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/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', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts new file mode 100644 index 0000000..ef5a0a1 --- /dev/null +++ b/libs/common/src/database/database.module.ts @@ -0,0 +1,66 @@ +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'; +import { ProductEntity } from '../modules/product/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 { 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: [ + 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, + ProductEntity, + DeviceUserPermissionEntity, + DeviceEntity, + PermissionTypeEntity, + SpaceEntity, + SpaceTypeEntity, + UserSpaceEntity, + DeviceUserPermissionEntity, + UserRoleEntity, + RoleTypeEntity, + UserNotificationEntity, + DeviceNotificationEntity, + ], + namingStrategy: new SnakeNamingStrategy(), + synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), + logging: false, + 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, + ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), + }), + }), + ], +}) +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..4953cc2 --- /dev/null +++ b/libs/common/src/database/strategies/snack-naming.strategy.ts @@ -0,0 +1,61 @@ +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, + ): 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-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/guards/jwt.auth.guard.ts b/libs/common/src/guards/jwt.auth.guard.ts new file mode 100644 index 0000000..a0ccdea --- /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) { + if (err || !user) { + throw err || new UnauthorizedException(); + } + return user; + } +} 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; +} diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts new file mode 100644 index 0000000..3378051 --- /dev/null +++ b/libs/common/src/helper/helper.module.ts @@ -0,0 +1,28 @@ +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'; +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, + TuyaWebSocketService, + OneSignalService, + DeviceMessagesService, + DeviceNotificationRepository, + ], + exports: [HelperHashService, SpacePermissionService], + controllers: [], + imports: [SpaceRepositoryModule, DeviceNotificationRepositoryModule], +}) +export class HelperModule {} 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/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); + } +} 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..46139ab --- /dev/null +++ b/libs/common/src/helper/services/index.ts @@ -0,0 +1,2 @@ +export * from './helper.hash.service'; +export * from './space.permission.service'; 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'); + } + } +} 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..0f11cbc --- /dev/null +++ b/libs/common/src/helper/services/space.permission.service.ts @@ -0,0 +1,39 @@ +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 { + try { + 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}`, + ); + } + } catch (err) { + throw new BadRequestException(err.message || 'Invalid UUID'); + } + } +} 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..cca7fc6 --- /dev/null +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -0,0 +1,72 @@ +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, + }); + + if (this.configService.get('tuya-config.TRUN_ON_TUYA_SOCKET')) { + // 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); + }); + } +} 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; +} 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..8876774 --- /dev/null +++ b/libs/common/src/modules/abstract/dtos/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..7c54a31 --- /dev/null +++ b/libs/common/src/modules/abstract/entities/abstract.entity.ts @@ -0,0 +1,40 @@ +import { Exclude } from 'class-transformer'; +import { CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm'; + +import { AbstractDto } from '../dtos'; +import { Constructor } from '../../../../../common/src/util/types'; + +export abstract class AbstractEntity< + T extends AbstractDto = AbstractDto, + O = never, +> { + @PrimaryColumn({ + type: 'uuid', + generated: 'uuid', + }) + @Exclude() + 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/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-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..d136df7 --- /dev/null +++ b/libs/common/src/modules/device-user-permission/entities/device.user.permission.entity.ts @@ -0,0 +1,43 @@ +import { Column, Entity, ManyToOne, Unique } 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' }) +@Unique(['userUuid', 'deviceUuid']) +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'; 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..438e268 --- /dev/null +++ b/libs/common/src/modules/device/device.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeviceEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([DeviceEntity])], +}) +export class DeviceRepositoryModule {} 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..7a6ba0c --- /dev/null +++ b/libs/common/src/modules/device/dtos/device.dto.ts @@ -0,0 +1,23 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + spaceUuid: string; + + @IsString() + @IsNotEmpty() + userUuid: string; + + @IsString() + @IsNotEmpty() + deviceTuyaUuid: string; + + @IsString() + @IsNotEmpty() + productUuid: string; +} 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..343f2bd --- /dev/null +++ b/libs/common/src/modules/device/dtos/index.ts @@ -0,0 +1 @@ +export * from './device.dto'; 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..3a46e07 --- /dev/null +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -0,0 +1,57 @@ +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceDto } from '../dtos/device.dto'; +import { SpaceEntity } from '../../space/entities'; +import { ProductEntity } from '../../product/entities'; +import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'device' }) +@Unique(['deviceTuyaUuid']) +export class DeviceEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + deviceTuyaUuid: string; + + @Column({ + nullable: true, + default: true, + }) + isActive: true; + + @ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) + user: UserEntity; + + @OneToMany( + () => DeviceUserPermissionEntity, + (permission) => permission.device, + { + nullable: true, + }, + ) + permission: DeviceUserPermissionEntity[]; + @OneToMany( + () => DeviceNotificationEntity, + (deviceUserNotification) => deviceUserNotification.device, + { + nullable: true, + }, + ) + deviceUserNotification: DeviceNotificationEntity[]; + + @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { + nullable: true, + }) + 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/device/entities/index.ts b/libs/common/src/modules/device/entities/index.ts new file mode 100644 index 0000000..64911c7 --- /dev/null +++ b/libs/common/src/modules/device/entities/index.ts @@ -0,0 +1 @@ +export * from './device.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.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..bf59e16 --- /dev/null +++ b/libs/common/src/modules/device/repositories/index.ts @@ -0,0 +1 @@ +export * from './device.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..48e985e --- /dev/null +++ b/libs/common/src/modules/permission/dtos/index.ts @@ -0,0 +1 @@ +export * from './permission.dto'; 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..90a8fd8 --- /dev/null +++ b/libs/common/src/modules/permission/entities/index.ts @@ -0,0 +1 @@ +export * from './permission.entity'; 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..d15d936 --- /dev/null +++ b/libs/common/src/modules/permission/entities/permission.entity.ts @@ -0,0 +1,28 @@ +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-user-permission/entities'; + +@Entity({ name: 'permission-type' }) +export class PermissionTypeEntity extends AbstractEntity { + @Column({ + nullable: false, + enum: Object.values(PermissionType), + }) + type: string; + + @OneToMany( + () => DeviceUserPermissionEntity, + (permission) => permission.permissionType, + { + nullable: true, + }, + ) + permission: DeviceUserPermissionEntity[]; + + 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..528b955 --- /dev/null +++ b/libs/common/src/modules/permission/repositories/index.ts @@ -0,0 +1 @@ +export * from './permission.repository'; 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/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..6553dc1 --- /dev/null +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -0,0 +1,33 @@ +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 { + @Column({ + nullable: false, + }) + catName: string; + + @Column({ + nullable: false, + unique: true, + }) + public prodId: string; + + @Column({ + nullable: false, + }) + 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/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()); + } +} 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..fd332fb --- /dev/null +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -0,0 +1,23 @@ +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, + enum: Object.values(RoleType), + }) + type: string; + @OneToMany(() => UserRoleEntity, (role) => role.roleType, { + nullable: true, + }) + roles: UserRoleEntity[]; + 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 {} 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..8e7c052 --- /dev/null +++ b/libs/common/src/modules/session/dtos/session.dto.ts @@ -0,0 +1,25 @@ +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..626a6a7 --- /dev/null +++ b/libs/common/src/modules/session/entities/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..d16ad2b --- /dev/null +++ b/libs/common/src/modules/session/entities/session.entity.ts @@ -0,0 +1,33 @@ +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({ + type: 'uuid', + default: () => 'gen_random_uuid()', + 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/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 {} 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..98706d0 --- /dev/null +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -0,0 +1,23 @@ +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; + + @IsString() + @IsNotEmpty() + public invitationCode: 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..030db57 --- /dev/null +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -0,0 +1,54 @@ +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'; +import { UserSpaceEntity } from '../../user-space/entities'; +import { DeviceEntity } from '../../device/entities'; + +@Entity({ name: 'space' }) +@Unique(['invitationCode']) +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: true, + }) + public spaceTuyaUuid: string; + + @Column({ + nullable: false, + }) + public spaceName: string; + + @Column({ + nullable: true, + }) + public invitationCode: string; + @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) + parent: SpaceEntity; + + @OneToMany(() => SpaceEntity, (space) => space.parent) + children: SpaceEntity[]; + @ManyToOne(() => SpaceTypeEntity, (spaceType) => spaceType.spaces, { + nullable: false, + }) + spaceType: SpaceTypeEntity; + + @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/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 {} 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-otp/dtos/index.ts b/libs/common/src/modules/user-otp/dtos/index.ts new file mode 100644 index 0000000..114762e --- /dev/null +++ b/libs/common/src/modules/user-otp/dtos/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..febdacb --- /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..d09957f --- /dev/null +++ b/libs/common/src/modules/user-otp/entities/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..454a3b1 --- /dev/null +++ b/libs/common/src/modules/user-otp/entities/user-otp.entity.ts @@ -0,0 +1,34 @@ +import { Column, Entity } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserOtpDto } from '../dtos'; +import { OtpType } from '../../../../src/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; + + @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-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..c733594 --- /dev/null +++ b/libs/common/src/modules/user-role/entities/user.role.entity.ts @@ -0,0 +1,24 @@ +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.roles, { + nullable: false, + }) + user: UserEntity; + + @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.roles, { + 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-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..a6caaa5 --- /dev/null +++ b/libs/common/src/modules/user-space/entities/user.space.entity.ts @@ -0,0 +1,29 @@ +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', + 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 {} 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..e4aa507 --- /dev/null +++ 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 new file mode 100644 index 0000000..c432751 --- /dev/null +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -0,0 +1,85 @@ +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'; +import { UserSpaceEntity } from '../../user-space/entities'; +import { UserRoleEntity } from '../../user-role/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; +import { UserNotificationEntity } from '../../user-notification/entities'; +import { DeviceEntity } from '../../device/entities'; + +@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; + + @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, + }) + public refreshToken: string; + + @Column({ + nullable: true, + default: false, + }) + public isUserVerified: boolean; + + @Column({ + nullable: false, + default: true, + }) + public isActive: boolean; + + @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) + userSpaces: UserSpaceEntity[]; + + @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) + userDevice: DeviceEntity[]; + + @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, + }) + roles: UserRoleEntity[]; + 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..aab6ec9 --- /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); diff --git a/libs/common/src/response/response.interceptor.ts b/libs/common/src/response/response.interceptor.ts new file mode 100644 index 0000000..6a456f2 --- /dev/null +++ b/libs/common/src/response/response.interceptor.ts @@ -0,0 +1,5 @@ +export interface Response { + statusCode: number; + message: string; + data?: T; +} 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..5f7a4b2 --- /dev/null +++ b/libs/common/src/seed/services/role.type.seeder.ts @@ -0,0 +1,44 @@ +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 (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/seed/services/supper.admin.seeder.ts b/libs/common/src/seed/services/supper.admin.seeder.ts new file mode 100644 index 0000000..6cb5f60 --- /dev/null +++ b/libs/common/src/seed/services/supper.admin.seeder.ts @@ -0,0 +1,72 @@ +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 SuperAdminSeeder { + 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/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts new file mode 100644 index 0000000..8636dea --- /dev/null +++ b/libs/common/src/util/email.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; + +@Injectable() +export class EmailService { + private smtpConfig: any; + + constructor(private readonly configService: ConfigService) { + 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( + email: string, + subject: string, + message: string, + ): Promise { + const transporter = nodemailer.createTransport(this.smtpConfig); + + const mailOptions = { + from: this.smtpConfig.auth.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..49a9efe --- /dev/null +++ b/libs/common/src/util/types.ts @@ -0,0 +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}` + : 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]; 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..f6f053a --- /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('APIs Documentation') + .addBearerAuth({ + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + in: 'header', + }) + .build(); + + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup('api', app, document, { + swaggerOptions: { + persistAuthorization: true, + }, + }); +} diff --git a/apps/auth/tsconfig.app.json b/libs/common/tsconfig.lib.json similarity index 70% rename from apps/auth/tsconfig.app.json rename to libs/common/tsconfig.lib.json index 01d9c9a..8fdbf52 100644 --- a/apps/auth/tsconfig.app.json +++ b/libs/common/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "declaration": false, - "outDir": "../../dist/apps/auth" + "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..cf7551a 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,32 +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" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 5e884ee..7267182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,36 @@ "license": "UNLICENSED", "dependencies": { "@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", + "@nestjs/websockets": "^10.3.8", + "@tuya/tuya-connector-nodejs": "^2.1.2", + "argon2": "^0.40.1", + "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", + "onesignal-node": "^3.4.0", + "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", + "ws": "^8.17.0" }, "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", @@ -25,6 +48,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", @@ -327,9 +351,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", @@ -337,11 +361,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", @@ -471,9 +495,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" @@ -531,14 +555,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" @@ -630,9 +654,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" @@ -818,24 +842,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, "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", @@ -844,8 +880,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" }, @@ -863,9 +899,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", @@ -896,7 +932,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" }, @@ -908,7 +944,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" @@ -1006,9 +1042,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" @@ -1069,11 +1105,15 @@ "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", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1090,7 +1130,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" }, @@ -1102,7 +1141,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" }, @@ -1113,14 +1151,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", @@ -1137,7 +1173,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" }, @@ -1152,7 +1187,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", @@ -1594,9 +1628,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", @@ -1608,10 +1642,10 @@ } }, "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==", - "dev": true, + "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" } @@ -1639,12 +1673,12 @@ "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", - "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", @@ -1671,6 +1705,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", @@ -1719,6 +1758,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", @@ -1747,6 +1855,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", @@ -1784,6 +1907,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", @@ -1826,6 +1989,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", @@ -1853,6 +2048,43 @@ } } }, + "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/@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", @@ -1905,11 +2137,18 @@ "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", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1951,29 +2190,51 @@ "@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", + "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", @@ -2042,9 +2303,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": "*", @@ -2146,6 +2407,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", @@ -2159,18 +2428,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==", - "dev": true, + "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": { @@ -2180,9 +2448,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": { @@ -2213,9 +2481,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", @@ -2233,6 +2501,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", @@ -2618,7 +2891,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" }, @@ -2648,7 +2921,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" } @@ -2726,7 +2999,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" } @@ -2745,6 +3017,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", @@ -2770,6 +3047,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", @@ -2779,13 +3064,26 @@ "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/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", - "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", @@ -2813,11 +3111,49 @@ "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==", - "dev": true + "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", + "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", @@ -2938,14 +3274,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", @@ -2961,6 +3295,35 @@ } ] }, + "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/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", + "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", @@ -2995,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", @@ -3031,11 +3399,24 @@ "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", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3053,9 +3434,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": [ { @@ -3072,8 +3453,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" }, @@ -3129,11 +3510,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", @@ -3154,14 +3548,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" @@ -3189,9 +3584,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": [ { @@ -3208,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", @@ -3292,6 +3692,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", @@ -3304,6 +3719,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", @@ -3344,7 +3830,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", @@ -3358,7 +3843,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", @@ -3380,6 +3864,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 +3908,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" }, @@ -3478,6 +3969,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", @@ -3595,13 +4128,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", @@ -3611,11 +4143,47 @@ "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/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", + "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", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "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" }, @@ -3670,14 +4238,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" @@ -3690,11 +4257,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", @@ -3735,7 +4309,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" } @@ -3773,11 +4347,46 @@ "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", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "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", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } }, "node_modules/ee-first": { "version": "1.1.1", @@ -3785,9 +4394,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": { @@ -3805,8 +4414,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", @@ -3817,9 +4425,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", @@ -3838,6 +4446,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", @@ -3856,7 +4475,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" } @@ -3879,16 +4497,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", @@ -4243,6 +4861,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", @@ -4284,6 +4916,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", @@ -4298,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", @@ -4312,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", @@ -4343,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", @@ -4541,16 +5198,34 @@ } }, "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": { + "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", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -4562,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", @@ -4616,7 +5299,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", @@ -4683,20 +5365,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", @@ -4718,7 +5386,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.*" } @@ -4762,11 +5429,18 @@ "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", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -4860,6 +5534,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", @@ -4878,20 +5593,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" }, @@ -4921,6 +5636,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", @@ -4930,6 +5653,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", @@ -4951,6 +5682,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", @@ -4975,7 +5720,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", @@ -5094,6 +5838,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", @@ -5145,7 +5912,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" } @@ -5210,6 +5976,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", @@ -5230,8 +6001,12 @@ "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/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", @@ -5243,14 +6018,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" }, @@ -5296,9 +6071,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", @@ -5320,7 +6095,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" }, @@ -6014,7 +6788,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" }, @@ -6022,6 +6795,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", @@ -6046,6 +6824,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", @@ -6058,6 +6841,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", @@ -6088,6 +6876,60 @@ "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/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", + "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", @@ -6128,6 +6970,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "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", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6161,8 +7008,47 @@ "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.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", @@ -6176,6 +7062,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", @@ -6232,7 +7123,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", @@ -6359,7 +7250,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" }, @@ -6382,7 +7272,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" } @@ -6398,11 +7287,49 @@ "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", - "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", @@ -6427,6 +7354,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", @@ -6453,6 +7390,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", @@ -6481,6 +7426,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", @@ -6493,6 +7448,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", @@ -6514,6 +7477,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", @@ -6522,6 +7493,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", @@ -6541,6 +7520,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", @@ -6550,6 +7537,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", @@ -6653,6 +7652,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", @@ -6683,6 +7687,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", @@ -6691,6 +7713,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", @@ -6713,7 +7770,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" } @@ -6728,7 +7784,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" @@ -6744,7 +7799,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" } @@ -6763,6 +7817,100 @@ "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/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", + "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", @@ -6863,6 +8011,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", @@ -6955,11 +8138,20 @@ "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/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" } @@ -6981,9 +8173,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" }, @@ -7106,11 +8298,36 @@ "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", "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", @@ -7120,11 +8337,103 @@ "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", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7400,7 +8709,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" }, @@ -7415,7 +8723,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" }, @@ -7426,8 +8733,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", @@ -7514,11 +8820,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" }, @@ -7530,11 +8847,19 @@ "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" } }, + "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", @@ -7615,7 +8940,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" }, @@ -7666,12 +8990,50 @@ "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", + "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", "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", @@ -7693,6 +9055,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", @@ -7701,6 +9068,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", @@ -7739,7 +9114,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", @@ -7754,7 +9128,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", @@ -7768,7 +9141,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" }, @@ -7781,7 +9153,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" }, @@ -7888,6 +9259,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", @@ -7923,9 +9299,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", @@ -8071,6 +9447,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", @@ -8124,6 +9519,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", @@ -8217,7 +9624,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", @@ -8298,6 +9705,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", @@ -8348,11 +9771,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" @@ -8375,8 +9940,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", @@ -8429,7 +9993,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" } @@ -8447,11 +10010,23 @@ "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", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -8467,6 +10042,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", @@ -8475,6 +10058,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", @@ -8512,10 +10113,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", @@ -8581,6 +10183,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" @@ -8594,6 +10197,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -8611,7 +10215,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" }, @@ -8641,7 +10244,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", @@ -8679,6 +10281,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", @@ -8691,7 +10313,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" } @@ -8706,7 +10327,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", @@ -8724,7 +10344,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" } @@ -8733,7 +10352,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 497acf1..52c5cf7 100644 --- a/package.json +++ b/package.json @@ -6,28 +6,51 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "npx nest build", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/apps/backend/main", + "start": "node dist/main", + "start:dev": "npx nest start --watch", + "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" }, "dependencies": { "@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", + "@nestjs/websockets": "^10.3.8", + "@tuya/tuya-connector-nodejs": "^2.1.2", + "argon2": "^0.40.1", + "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", + "onesignal-node": "^3.4.0", + "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", + "ws": "^8.17.0" }, "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", @@ -36,6 +59,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", @@ -48,25 +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/" - ] } -} \ No newline at end of file +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..19cabd3 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,55 @@ +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'; +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'; +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'; +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'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { LoggingInterceptor } from './interceptors/logging.interceptor'; +@Module({ + imports: [ + ConfigModule.forRoot({ + load: config, + }), + AuthenticationModule, + UserModule, + RoleModule, + CommunityModule, + BuildingModule, + FloorModule, + UnitModule, + RoomModule, + RoomModule, + GroupModule, + DeviceModule, + DeviceMessagesSubscriptionModule, + UserDevicePermissionModule, + UserNotificationModule, + SeederModule, + SceneModule, + DoorLockModule, + // + ], + controllers: [AuthenticationController], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + ], +}) +export class AuthModule {} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..10515e7 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,29 @@ +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 '../../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 '../../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], + controllers: [AuthenticationController, UserAuthController], + providers: [ + AuthenticationService, + UserAuthService, + UserRepository, + UserSessionRepository, + UserOtpRepository, + UserRoleRepository, + RoleTypeRepository, + ], + exports: [AuthenticationService, UserAuthService], +}) +export class AuthenticationModule {} diff --git a/src/auth/constants/login.response.constant.ts b/src/auth/constants/login.response.constant.ts new file mode 100644 index 0000000..0be9517 --- /dev/null +++ b/src/auth/constants/login.response.constant.ts @@ -0,0 +1,3 @@ +export interface ILoginResponse { + access_token: string; +} diff --git a/src/auth/controllers/authentication.controller.ts b/src/auth/controllers/authentication.controller.ts new file mode 100644 index 0000000..070620b --- /dev/null +++ b/src/auth/controllers/authentication.controller.ts @@ -0,0 +1,16 @@ +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') + async Authentication() { + return await this.authenticationService.main(); + } +} diff --git a/src/auth/controllers/index.ts b/src/auth/controllers/index.ts new file mode 100644 index 0000000..2ce466d --- /dev/null +++ b/src/auth/controllers/index.ts @@ -0,0 +1,2 @@ +export * from './authentication.controller'; +export * from './user-auth.controller'; diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts new file mode 100644 index 0000000..86f9ce6 --- /dev/null +++ b/src/auth/controllers/user-auth.controller.ts @@ -0,0 +1,128 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + Post, + Req, + 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 '../../../libs/common/src/response/response.decorator'; +import { UserLoginDto } from '../dtos/user-login.dto'; +import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; +import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; + +@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, + default: () => 'gen_random_uuid()', // this is a default value for the uuid column + }, + 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 Logged in Successfully', + }; + } + + @ApiBearerAuth() + @UseGuards(SuperAdminRoleGuard) + @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', + }; + } + + @ApiBearerAuth() + @UseGuards(SuperAdminRoleGuard) + @Get('user/list') + async userList() { + 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/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/src/auth/dtos/user-auth.dto.ts b/src/auth/dtos/user-auth.dto.ts new file mode 100644 index 0000000..729b735 --- /dev/null +++ b/src/auth/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/src/auth/dtos/user-login.dto.ts b/src/auth/dtos/user-login.dto.ts new file mode 100644 index 0000000..6a14047 --- /dev/null +++ b/src/auth/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/src/auth/dtos/user-otp.dto.ts b/src/auth/dtos/user-otp.dto.ts new file mode 100644 index 0000000..bab47c8 --- /dev/null +++ b/src/auth/dtos/user-otp.dto.ts @@ -0,0 +1,22 @@ +import { OtpType } from '../../../libs/common/src/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/src/auth/dtos/user-password.dto.ts b/src/auth/dtos/user-password.dto.ts new file mode 100644 index 0000000..fe2118c --- /dev/null +++ b/src/auth/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/src/auth/services/authentication.service.ts b/src/auth/services/authentication.service.ts new file mode 100644 index 0000000..1d5d580 --- /dev/null +++ b/src/auth/services/authentication.service.ts @@ -0,0 +1,120 @@ +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, + 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/src/auth/services/index.ts b/src/auth/services/index.ts new file mode 100644 index 0000000..ac532d6 --- /dev/null +++ b/src/auth/services/index.ts @@ -0,0 +1,2 @@ +export * from './authentication.service'; +export * from './user-auth.service'; diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts new file mode 100644 index 0000000..a514c1c --- /dev/null +++ b/src/auth/services/user-auth.service.ts @@ -0,0 +1,207 @@ +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, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { UserSignUpDto } from '../dtos/user-auth.dto'; +import { HelperHashService } from '../../../libs/common/src/helper/services'; +import { UserLoginDto } from '../dtos/user-login.dto'; +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 '../../../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'; + +@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, + private readonly userRoleRepository: UserRoleRepository, + private readonly roleTypeRepository: RoleTypeRepository, + ) {} + + 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); // Hash the password using bcrypt + const hashedPassword = await this.helperHashService.bcrypt( + userSignUpDto.password, + salt, + ); + + try { + const user = await this.userRepository.save({ + ...userSignUpDto, + password: hashedPassword, + }); + + return user; + } catch (error) { + throw new BadRequestException('Failed to register user'); + } + } + + 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) { + 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.uuid, + uuid: user.uuid, + roles: user?.roles?.map((role) => { + return { uuid: role.uuid, type: role.roleType.type }; + }), + 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.update({ uuid }, { isActive: false }); + } + + 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.uuid); + throw new BadRequestException('OTP expired'); + } + + if (data.type == OtpType.VERIFICATION) { + await this.userRepository.update( + { email: data.email }, + { isUserVerified: true }, + ); + } + + 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/building/building.module.ts b/src/building/building.module.ts new file mode 100644 index 0000000..80391fe --- /dev/null +++ b/src/building/building.module.ts @@ -0,0 +1,32 @@ +import { Module } 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 { 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: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + UserRepositoryModule, + ], + controllers: [BuildingController], + providers: [ + BuildingService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + UserRepository, + ], + exports: [BuildingService], +}) +export class BuildingModule {} diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts new file mode 100644 index 0000000..a76d620 --- /dev/null +++ b/src/building/controllers/building.controller.ts @@ -0,0 +1,155 @@ +import { BuildingService } from '../services/building.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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 { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { BuildingPermissionGuard } from 'src/guards/building.permission.guard'; + +@ApiTags('Building Module') +@Controller({ + version: '1', + path: 'building', +}) +export class BuildingController { + constructor(private readonly buildingService: BuildingService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) + @Post() + async addBuilding(@Body() addBuildingDto: AddBuildingDto) { + try { + const building = await this.buildingService.addBuilding(addBuildingDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Building added successfully', + data: building, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) + @Get(':buildingUuid') + async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { + try { + const building = + await this.buildingService.getBuildingByUuid(buildingUuid); + return building; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) + @Get('parent/:buildingUuid') + async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { + try { + const building = + await this.buildingService.getBuildingParentByUuid(buildingUuid); + return building; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(AdminRoleGuard, CheckUserBuildingGuard) + @Post('user') + async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) { + try { + await this.buildingService.addUserBuilding(addUserBuildingDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + 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') + async getBuildingsByUserId(@Param('userUuid') userUuid: string) { + try { + return await this.buildingService.getBuildingsByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, BuildingPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || 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'; diff --git a/src/building/dtos/add.building.dto.ts b/src/building/dtos/add.building.dto.ts new file mode 100644 index 0000000..5d79231 --- /dev/null +++ b/src/building/dtos/add.building.dto.ts @@ -0,0 +1,42 @@ +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); + } +} +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/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'; 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 new file mode 100644 index 0000000..1127456 --- /dev/null +++ b/src/building/interface/building.interface.ts @@ -0,0 +1,31 @@ +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; +} +export interface RenameBuildingByUuidInterface { + uuid: string; + 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 new file mode 100644 index 0000000..dfc2089 --- /dev/null +++ b/src/building/services/building.service.ts @@ -0,0 +1,311 @@ +import { GetBuildingChildDto } from '../dtos/get.building.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddBuildingDto, AddUserBuildingDto } 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) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'building', + }, + }); + + if (!spaceType) { + throw new BadRequestException('Invalid building UUID'); + } + 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 + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + } + } + + async getBuildingByUuid( + buildingUuid: string, + ): Promise { + try { + const building = await this.spaceRepository.findOne({ + where: { + uuid: buildingUuid, + spaceType: { + type: 'building', + }, + }, + relations: ['spaceType'], + }); + if ( + !building || + !building.spaceType || + building.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); + } + return { + uuid: building.uuid, + createdAt: building.createdAt, + updatedAt: building.updatedAt, + name: building.spaceName, + type: building.spaceType.type, + }; + } catch (err) { + 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 { + try { + const { includeSubSpaces, page, pageSize } = getBuildingChildDto; + + 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'); + } + + 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); + } + } + } + + 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 || + !building.spaceType || + building.spaceType.type !== 'building' + ) { + throw new BadRequestException('Invalid building UUID'); + } + 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) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Building not found', HttpStatus.NOT_FOUND); + } + } + } + + 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 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, + ): 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); + } + } + } +} 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/community/community.module.ts b/src/community/community.module.ts new file mode 100644 index 0000000..e27627e --- /dev/null +++ b/src/community/community.module.ts @@ -0,0 +1,34 @@ +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'; +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'; +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: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + UserRepositoryModule, + ], + controllers: [CommunityController], + providers: [ + CommunityService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + UserRepository, + SpacePermissionService, + ], + exports: [CommunityService, SpacePermissionService], +}) +export class CommunityModule {} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts new file mode 100644 index 0000000..c88acf1 --- /dev/null +++ b/src/community/controllers/community.controller.ts @@ -0,0 +1,143 @@ +import { CommunityService } from '../services/community.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { CommunityPermissionGuard } from 'src/guards/community.permission.guard'; + +@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 { + const community = + await this.communityService.addCommunity(addCommunityDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Community added successfully', + data: community, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) + @Get(':communityUuid') + async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { + try { + const community = + await this.communityService.getCommunityByUuid(communityUuid); + return community; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('user/:userUuid') + async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { + try { + return await this.communityService.getCommunitiesByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(AdminRoleGuard, CheckUserCommunityGuard) + @Post('user') + async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { + try { + await this.communityService.addUserCommunity(addUserCommunityDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user community added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CommunityPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || 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'; diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts new file mode 100644 index 0000000..06aec7c --- /dev/null +++ b/src/community/dtos/add.community.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddCommunityDto { + @ApiProperty({ + description: 'communityName', + required: true, + }) + @IsString() + @IsNotEmpty() + public communityName: string; + + constructor(dto: Partial) { + 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/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts new file mode 100644 index 0000000..be614e5 --- /dev/null +++ b/src/community/dtos/get.community.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 GetCommunityDto { + @ApiProperty({ + description: 'communityUuid', + required: true, + }) + @IsString() + @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; +} 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'; 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 new file mode 100644 index 0000000..31c0579 --- /dev/null +++ b/src/community/interface/community.interface.ts @@ -0,0 +1,26 @@ +export interface GetCommunityByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface CommunityChildInterface { + uuid: string; + name: string; + type: string; + totalCount?: number; + children?: CommunityChildInterface[]; +} +export interface RenameCommunityByUuidInterface { + uuid: string; + 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 new file mode 100644 index 0000000..66c7037 --- /dev/null +++ b/src/community/services/community.service.ts @@ -0,0 +1,254 @@ +import { GetCommunityChildDto } from './../dtos/get.community.dto'; +import { SpaceTypeRepository } from './../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddCommunityDto, AddUserCommunityDto } 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) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'community', + }, + }); + + 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); + } + } + + async getCommunityByUuid( + communityUuid: string, + ): Promise { + try { + const community = await this.spaceRepository.findOne({ + where: { + uuid: communityUuid, + spaceType: { + type: 'community', + }, + }, + relations: ['spaceType'], + }); + if ( + !community || + !community.spaceType || + community.spaceType.type !== 'community' + ) { + throw new BadRequestException('Invalid community UUID'); + } + return { + uuid: community.uuid, + createdAt: community.createdAt, + updatedAt: community.updatedAt, + name: community.spaceName, + type: community.spaceType.type, + }; + } catch (err) { + 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 { + try { + const { includeSubSpaces, page, pageSize } = getCommunityChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: communityUuid }, + relations: ['children', 'spaceType'], + }); + + 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); + } + } + } + + 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 !== 'community') // Filter remaining community type + .map((child) => ({ + uuid: child.uuid, + name: child.spaceName, + type: child.spaceType.type, + })); + } + + const childHierarchies = await Promise.all( + 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; + } + + 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 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, + ): 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); + } + } + } +} 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'; diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..0ea13da --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,9 @@ +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/src/config/auth.config.ts b/src/config/auth.config.ts new file mode 100644 index 0000000..bb2157b --- /dev/null +++ b/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/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..d7d0014 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,4 @@ +import AuthConfig from './auth.config'; +import AppConfig from './app.config'; +import JwtConfig from './jwt.config'; +export default [AuthConfig, AppConfig, JwtConfig]; diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts new file mode 100644 index 0000000..36c2f96 --- /dev/null +++ b/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/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/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts new file mode 100644 index 0000000..a54b5a9 --- /dev/null +++ b/src/device/controllers/device.controller.ts @@ -0,0 +1,226 @@ +import { DeviceService } from '../services/device.service'; +import { + Body, + Controller, + Get, + Post, + Query, + Param, + HttpException, + HttpStatus, + UseGuards, + Req, + Put, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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 { 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({ + version: '1', + path: 'device', +}) +export class DeviceController { + constructor(private readonly deviceService: DeviceService) {} + @ApiBearerAuth() + @UseGuards(SuperAdminRoleGuard, CheckDeviceGuard) + @Post() + async addDeviceUser(@Body() addDeviceDto: AddDeviceDto) { + try { + const device = await this.deviceService.addDeviceUser(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) + @Get('user/: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( + @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, + @Req() req: any, + ) { + try { + const userUuid = req.user.uuid; + return await this.deviceService.getDevicesByRoomId( + getDeviceByRoomUuidDto, + userUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @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') + async updateDeviceInRoom( + @Body() updateDeviceInRoomDto: UpdateDeviceInRoomDto, + ) { + try { + const device = await this.deviceService.updateDeviceInRoom( + updateDeviceInRoomDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device updated in room successfully', + data: device, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @Get(':deviceUuid') + async getDeviceDetailsByDeviceId( + @Param('deviceUuid') deviceUuid: string, + @Req() req: any, + ) { + try { + const userUuid = req.user.uuid; + return await this.deviceService.getDeviceDetailsByDeviceId( + deviceUuid, + userUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @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, CheckUserHavePermission) + @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, CheckUserHaveControllablePermission) + @Post(':deviceUuid/control') + async controlDevice( + @Body() controlDeviceDto: ControlDeviceDto, + @Param('deviceUuid') deviceUuid: string, + ) { + try { + return await this.deviceService.controlDevice( + controlDeviceDto, + deviceUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @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('gateway/:gatewayUuid/devices') + async getDevicesInGateway(@Param('gatewayUuid') gatewayUuid: string) { + try { + return await this.deviceService.getDevicesInGateway(gatewayUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} 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..07d1ad0 --- /dev/null +++ b/src/device/device.module.ts @@ -0,0 +1,27 @@ +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'; +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 { DeviceUserPermissionRepository } from '@app/common/modules/device-user-permission/repositories'; +import { UserRepository } from '@app/common/modules/user/repositories'; +@Module({ + imports: [ConfigModule, ProductRepositoryModule, DeviceRepositoryModule], + controllers: [DeviceController], + providers: [ + DeviceService, + ProductRepository, + DeviceUserPermissionRepository, + PermissionTypeRepository, + SpaceRepository, + DeviceRepository, + UserRepository, + ], + 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..f732317 --- /dev/null +++ b/src/device/dtos/add.device.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddDeviceDto { + @ApiProperty({ + description: 'deviceTuyaUuid', + required: true, + }) + @IsString() + @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, + }) + @IsString() + @IsNotEmpty() + public roomUuid: 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..ab2125d --- /dev/null +++ b/src/device/dtos/control.device.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ControlDeviceDto { + @ApiProperty({ + description: 'code', + required: true, + }) + @IsString() + @IsNotEmpty() + 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 new file mode 100644 index 0000000..26002bb --- /dev/null +++ b/src/device/dtos/get.device.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetDeviceByRoomUuidDto { + @ApiProperty({ + description: 'roomUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public roomUuid: string; +} diff --git a/src/device/dtos/index.ts b/src/device/dtos/index.ts new file mode 100644 index 0000000..1a0b6b3 --- /dev/null +++ b/src/device/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './add.device.dto'; +export * from './control.device.dto'; +export * from './get.device.dto'; diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts new file mode 100644 index 0000000..526c199 --- /dev/null +++ b/src/device/interfaces/get.device.interface.ts @@ -0,0 +1,66 @@ +export interface GetDeviceDetailsInterface { + 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 { + success: boolean; + msg: string; + result: boolean; +} + +export interface controlDeviceInterface { + success: boolean; + result: boolean; + msg: string; +} +export interface GetDeviceDetailsFunctionsInterface { + result: { + category: string; + functions: []; + }; + success: boolean; + msg: string; +} +export interface GetDeviceDetailsFunctionsStatusInterface { + result: [{ id: string; status: [] }]; + success: boolean; + msg: string; +} + +export interface DeviceInstructionResponse { + productUuid: string; + productType: string; + functions: { + code: string; + values: any[]; + 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 new file mode 100644 index 0000000..8cbe772 --- /dev/null +++ b/src/device/services/device.service.ts @@ -0,0 +1,581 @@ +import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository'; +import { + Injectable, + HttpException, + HttpStatus, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto'; +import { + DeviceInstructionResponse, + GetDeviceDetailsFunctionsInterface, + GetDeviceDetailsFunctionsStatusInterface, + GetDeviceDetailsInterface, + controlDeviceInterface, + updateDeviceFirmwareInterface, +} from '../interfaces/get.device.interface'; +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 { 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 { + private tuya: TuyaContext; + constructor( + 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'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { + return await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + ...(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( + 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 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 { + haveRoom: device.spaceDevice ? true : false, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + } 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, + ): Promise { + try { + const devices = await this.deviceRepository.find({ + where: { + spaceDevice: { uuid: getDeviceByRoomUuidDto.roomUuid }, + 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 { + haveRoom: device.spaceDevice ? true : false, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + permissionType: device.permission[0].permissionType.type, + ...(await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + )), + uuid: device.uuid, + } as GetDeviceDetailsInterface; + }), + ); + + return devicesData; + } catch (error) { + // Handle the error here + throw new HttpException( + 'Error fetching devices by room', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + 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 controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); + + 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( + error.message || 'Device Not Found', + error.status || HttpStatus.NOT_FOUND, + ); + } + } + async controlDeviceTuya( + deviceUuid: string, + controlDeviceDto: ControlDeviceDto, + ): Promise { + try { + 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 }, + ], + }, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error control device from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDeviceDetailsByDeviceId(deviceUuid: string, userUuid: string) { + try { + const userDevicePermission = await this.getUserDevicePermission( + userUuid, + deviceUuid, + ); + + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + + const response = await this.getDeviceDetailsByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); + + return { + ...response, + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + permissionType: userDevicePermission, + }; + } catch (error) { + throw new HttpException( + error.message || 'Device Not Found', + 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, id, ...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 getDeviceInstructionByDeviceId( + deviceUuid: string, + ): Promise { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + + const response = await this.getDeviceInstructionByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); + + return { + 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('Device Not Found', HttpStatus.NOT_FOUND); + } + } + + async getDeviceInstructionByDeviceIdTuya( + 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 from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDevicesInstructionStatus(deviceUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + const deviceStatus = await this.getDevicesInstructionStatusTuya( + deviceDetails.deviceTuyaUuid, + ); + + return { + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + status: deviceStatus.result[0].status, + }; + } catch (error) { + throw new HttpException( + 'Error fetching device functions status', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDevicesInstructionStatusTuya( + deviceUuid: string, + ): Promise { + try { + const path = `/v1.0/iot-03/devices/status`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + device_ids: deviceUuid, + }, + }); + return response as GetDeviceDetailsFunctionsStatusInterface; + } catch (error) { + throw new HttpException( + 'Error fetching device functions status from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + 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 getDevicesInGateway(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 BadRequestException('This is not a gateway device'); + } + + const response = await this.getDevicesInGatewayTuya( + deviceDetails.deviceTuyaUuid, + ); + + const devices = await Promise.all( + 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 { + ...rest, + tuyaUuid: deviceDetails.deviceTuyaUuid, + 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, + devices: devices.filter((device) => device !== null), + }; + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + } + + async getDevicesInGatewayTuya( + 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, ...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, + ); + } + } + 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, + ); + } + } + 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, + ); + } + } +} 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'; 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'; 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 {} 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..269aabc --- /dev/null +++ b/src/door-lock/dtos/add.online-temp.dto.ts @@ -0,0 +1,86 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsEnum, + Length, + IsOptional, +} 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 }) + @IsOptional() + @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'; 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; +} 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..e88fee4 --- /dev/null +++ b/src/door-lock/services/door.lock.service.ts @@ -0,0 +1,582 @@ +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 === 0, //temp solution + ); + + 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.type === 0) //temp solution + .map((password: any) => { + if (password.schedule_list?.length > 0) { + password.schedule_list = password.schedule_list.map( + (schedule) => { + schedule.working_day = this.getDaysFromWorkingDayValue( + schedule.working_day, + ); + schedule.effective_time = this.minutesToTime( + schedule.effective_time, + ); + schedule.invalid_time = this.minutesToTime( + schedule.invalid_time, + ); + return schedule; + }, + ); + } + return password; + }); + + return 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, + addDeviceObj.scheduleList ? isOnline : false, + ); + + 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: '0', //temporary solution, + }, + }); + + 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'; 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); + }); +}); diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts new file mode 100644 index 0000000..b4940fe --- /dev/null +++ b/src/floor/controllers/floor.controller.ts @@ -0,0 +1,155 @@ +import { FloorService } from '../services/floor.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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 { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +import { FloorPermissionGuard } from 'src/guards/floor.permission.guard'; + +@ApiTags('Floor Module') +@Controller({ + version: '1', + path: 'floor', +}) +export class FloorController { + constructor(private readonly floorService: FloorService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) + @Post() + async addFloor(@Body() addFloorDto: AddFloorDto) { + try { + const floor = await this.floorService.addFloor(addFloorDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Floor added successfully', + data: floor, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, FloorPermissionGuard) + @Get(':floorUuid') + async getFloorByUuid(@Param('floorUuid') floorUuid: string) { + try { + const floor = await this.floorService.getFloorByUuid(floorUuid); + return floor; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, FloorPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, FloorPermissionGuard) + @Get('parent/:floorUuid') + async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { + try { + const floor = await this.floorService.getFloorParentByUuid(floorUuid); + return floor; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(AdminRoleGuard, CheckUserFloorGuard) + @Post('user') + async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) { + try { + await this.floorService.addUserFloor(addUserFloorDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + 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') + async getFloorsByUserId(@Param('userUuid') userUuid: string) { + try { + return await this.floorService.getFloorsByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, FloorPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || 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..3d1655a --- /dev/null +++ b/src/floor/dtos/add.floor.dto.ts @@ -0,0 +1,42 @@ +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); + } +} +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/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/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/floor.module.ts b/src/floor/floor.module.ts new file mode 100644 index 0000000..71a6c67 --- /dev/null +++ b/src/floor/floor.module.ts @@ -0,0 +1,32 @@ +import { Module } 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 { 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: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + UserRepositoryModule, + ], + controllers: [FloorController], + providers: [ + FloorService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + UserRepository, + ], + exports: [FloorService], +}) +export class FloorModule {} diff --git a/src/floor/interface/floor.interface.ts b/src/floor/interface/floor.interface.ts new file mode 100644 index 0000000..37f35c4 --- /dev/null +++ b/src/floor/interface/floor.interface.ts @@ -0,0 +1,32 @@ +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; +} +export interface RenameFloorByUuidInterface { + uuid: string; + 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 new file mode 100644 index 0000000..af4b26f --- /dev/null +++ b/src/floor/services/floor.service.ts @@ -0,0 +1,292 @@ +import { GetFloorChildDto } from '../dtos/get.floor.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddFloorDto, AddUserFloorDto } 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) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'floor', + }, + }); + + 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); + } + } + + async getFloorByUuid(floorUuid: string): Promise { + try { + const floor = await this.spaceRepository.findOne({ + where: { + uuid: floorUuid, + spaceType: { + type: 'floor', + }, + }, + relations: ['spaceType'], + }); + if (!floor || !floor.spaceType || floor.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); + } + + return { + uuid: floor.uuid, + createdAt: floor.createdAt, + updatedAt: floor.updatedAt, + name: floor.spaceName, + type: floor.spaceType.type, + }; + } catch (err) { + 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 { + try { + const { includeSubSpaces, page, pageSize } = getFloorChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: floorUuid }, + relations: ['children', 'spaceType'], + }); + + 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); + } + } + } + + 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' && + child.spaceType.type !== 'community', + ) // Filter remaining floor and 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 !== 'floor' && + 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, + 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 || !floor.spaceType || floor.spaceType.type !== 'floor') { + throw new BadRequestException('Invalid floor UUID'); + } + + 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) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); + } + } + } + + 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 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, + ): 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); + } + } + } +} 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/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts new file mode 100644 index 0000000..ce6fae8 --- /dev/null +++ b/src/group/controllers/group.controller.ts @@ -0,0 +1,59 @@ +import { GroupService } from '../services/group.service'; +import { + Controller, + Get, + UseGuards, + Param, + HttpException, + HttpStatus, + Req, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; + +@ApiTags('Group Module') +@Controller({ + version: '1', + path: 'group', +}) +export class GroupController { + constructor(private readonly groupService: GroupService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid') + async getGroupsBySpaceUuid(@Param('unitUuid') unitUuid: string) { + try { + return await this.groupService.getGroupsByUnitUuid(unitUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid/devices/:groupName') + async getUnitDevicesByGroupName( + @Param('unitUuid') unitUuid: string, + @Param('groupName') groupName: string, + @Req() req: any, + ) { + try { + const userUuid = req.user.uuid; + + return await this.groupService.getUnitDevicesByGroupName( + unitUuid, + groupName, + userUuid, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} 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/group.module.ts b/src/group/group.module.ts new file mode 100644 index 0000000..b5ade2d --- /dev/null +++ b/src/group/group.module.ts @@ -0,0 +1,21 @@ +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 { DeviceRepositoryModule } from '@app/common/modules/device'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [GroupController], + providers: [ + GroupService, + DeviceRepository, + SpaceRepository, + DeviceRepository, + ProductRepository, + ], + exports: [GroupService], +}) +export class GroupModule {} diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts new file mode 100644 index 0000000..4f1a1f7 --- /dev/null +++ b/src/group/services/group.service.ts @@ -0,0 +1,151 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +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 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 tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async getGroupsByUnitUuid(unitUuid: string) { + try { + const spaces = await this.spaceRepository.find({ + where: { + parent: { + uuid: unitUuid, + }, + }, + relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + }); + + const groupNames = spaces.flatMap((space) => { + return space.devicesSpaceEntity.map( + (device) => device.productDevice.prodType, + ); + }); + + const uniqueGroupNames = [...new Set(groupNames)]; + + return uniqueGroupNames.map((groupName) => ({ groupName })); + } catch (error) { + throw new HttpException( + 'This unit does not have any groups', + HttpStatus.NOT_FOUND, + ); + } + } + + async getUnitDevicesByGroupName( + unitUuid: string, + groupName: string, + userUuid: string, + ) { + try { + 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, + }; + }), + ); + }), + ); + 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( + '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, + ); + } + } +} 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'; diff --git a/src/guards/admin.role.guard.ts b/src/guards/admin.role.guard.ts new file mode 100644 index 0000000..f7d64a0 --- /dev/null +++ b/src/guards/admin.role.guard.ts @@ -0,0 +1,20 @@ +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) { + 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'); + } + } + return user; + } +} 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/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.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/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/device.guard.ts b/src/guards/device.guard.ts new file mode 100644 index 0000000..179ef11 --- /dev/null +++ b/src/guards/device.guard.ts @@ -0,0 +1,90 @@ +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'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + 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/device.product.guard.ts b/src/guards/device.product.guard.ts new file mode 100644 index 0000000..108307a --- /dev/null +++ b/src/guards/device.product.guard.ts @@ -0,0 +1,72 @@ +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; + + 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] }, + relations: ['productDevice'], + }); + + if (!firstDevice) { + throw new BadRequestException('First device not found'); + } + + 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.prodType !== firstProductType) { + throw new BadRequestException(`Devices have different product types`); + } + } + } + + 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', + }); + } + } +} 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/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/room.guard.ts b/src/guards/room.guard.ts new file mode 100644 index 0000000..c5ed514 --- /dev/null +++ b/src/guards/room.guard.ts @@ -0,0 +1,83 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + HttpStatus, +} from '@nestjs/common'; + +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; + +@Injectable() +export class CheckRoomGuard implements CanActivate { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly deviceRepository: DeviceRepository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + if (req.query && req.query.roomUuid) { + const { roomUuid } = req.query; + await this.checkRoomIsFound(roomUuid); + } else if (req.body && req.body.roomUuid && req.body.deviceUuid) { + const { roomUuid, deviceUuid } = req.body; + await this.checkRoomIsFound(roomUuid); + await this.checkDeviceIsFound(deviceUuid); + } 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 checkDeviceIsFound(deviceUuid: string) { + const response = await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + }); + + if (!response.uuid) { + 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', + }); + } + } +} 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/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/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; + } + } +} 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/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.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, + }); + } + } +} 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', + }); + } + } +} diff --git a/src/interceptors/logging.interceptor.ts b/src/interceptors/logging.interceptor.ts new file mode 100644 index 0000000..9e06186 --- /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 for logging + 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 for logging + const filteredResponse = this.filterSensitiveFields(response); + console.log(`Response: ${JSON.stringify(filteredResponse)}`); + return response; // Return the actual response unmodified + }), + 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; + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0253ad0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,48 @@ +import { NestFactory } from '@nestjs/core'; +import { AuthModule } from './app.module'; +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 { SeederService } from '@app/common/seed/services/seeder.service'; + +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({ + transform: true, // Auto-transform payloads to their DTO instances. + transformOptions: { + enableImplicitConversion: true, // Convert incoming payloads to their DTO instances if possible. + }, + }), + ); + + 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); +bootstrap(); 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..cb2038d --- /dev/null +++ b/src/role/controllers/role.controller.ts @@ -0,0 +1,54 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { RoleService } from '../services/role.service'; +import { AddUserRoleDto } from '../dtos'; +import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; + +@ApiTags('Role Module') +@Controller({ + version: '1', + path: 'role', +}) +export class RoleController { + constructor(private readonly roleService: RoleService) {} + @ApiBearerAuth() + @UseGuards(SuperAdminRoleGuard) + @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(SuperAdminRoleGuard) + @Post() + async addUserRoleType(@Body() addUserRoleDto: AddUserRoleDto) { + try { + await this.roleService.addUserRoleType(addUserRoleDto); + return { + statusCode: HttpStatus.OK, + message: 'User Role Added 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..a9b9771 --- /dev/null +++ b/src/role/dtos/index.ts @@ -0,0 +1 @@ +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/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..ece3780 --- /dev/null +++ b/src/role/services/role.service.ts @@ -0,0 +1,54 @@ +import { RoleTypeRepository } from './../../../libs/common/src/modules/role-type/repositories/role.type.repository'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { AddUserRoleDto } from '../dtos/role.add.dto'; +import { UserRoleRepository } from '@app/common/modules/user-role/repositories'; +import { QueryFailedError } from 'typeorm'; + +@Injectable() +export class RoleService { + constructor( + private readonly roleTypeRepository: RoleTypeRepository, + private readonly userRoleRepository: UserRoleRepository, + ) {} + + async addUserRoleType(addUserRoleDto: AddUserRoleDto) { + try { + 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, + ); + } + } + + async fetchRoleTypes() { + const roleTypes = await this.roleTypeRepository.find(); + + return roleTypes; + } + private async fetchRoleByType(roleType: string) { + return await this.roleTypeRepository.findOne({ + where: { + type: roleType, + }, + }); + } +} 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..0a92e57 --- /dev/null +++ b/src/room/controllers/room.controller.ts @@ -0,0 +1,131 @@ +import { RoomService } from '../services/room.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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 { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { RoomPermissionGuard } from 'src/guards/room.permission.guard'; + +@ApiTags('Room Module') +@Controller({ + version: '1', + path: 'room', +}) +export class RoomController { + constructor(private readonly roomService: RoomService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) + @Post() + async addRoom(@Body() addRoomDto: AddRoomDto) { + try { + const room = await this.roomService.addRoom(addRoomDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Room added successfully', + data: room, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoomPermissionGuard) + @Get(':roomUuid') + async getRoomByUuid(@Param('roomUuid') roomUuid: string) { + try { + const room = await this.roomService.getRoomByUuid(roomUuid); + return room; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoomPermissionGuard) + @Get('parent/:roomUuid') + async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { + try { + const room = await this.roomService.getRoomParentByUuid(roomUuid); + return room; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(AdminRoleGuard, CheckUserRoomGuard) + @Post('user') + async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) { + try { + await this.roomService.addUserRoom(addUserRoomDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + 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') + async getRoomsByUserId(@Param('userUuid') userUuid: string) { + try { + return await this.roomService.getRoomsByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RoomPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/room/dtos/add.room.dto.ts b/src/room/dtos/add.room.dto.ts new file mode 100644 index 0000000..2718a29 --- /dev/null +++ b/src/room/dtos/add.room.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddRoomDto { + @ApiProperty({ + description: 'roomName', + required: true, + }) + @IsString() + @IsNotEmpty() + public roomName: string; + + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + constructor(dto: Partial) { + 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/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/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 new file mode 100644 index 0000000..49473a3 --- /dev/null +++ b/src/room/interface/room.interface.ts @@ -0,0 +1,24 @@ +export interface GetRoomByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; +} + +export interface RoomParentInterface { + uuid: string; + name: string; + type: string; + parent?: RoomParentInterface; +} +export interface RenameRoomByUuidInterface { + uuid: string; + 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 new file mode 100644 index 0000000..2d6d98c --- /dev/null +++ b/src/room/room.module.ts @@ -0,0 +1,32 @@ +import { Module } 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 { 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: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + UserRepositoryModule, + ], + controllers: [RoomController], + providers: [ + RoomService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + UserRepository, + ], + 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..b9ea30e --- /dev/null +++ b/src/room/services/room.service.ts @@ -0,0 +1,198 @@ +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddRoomDto, AddUserRoomDto } from '../dtos'; +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) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + type: 'room', + }, + }); + + 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); + } + } + + async getRoomByUuid(roomUuid: string): Promise { + try { + const room = await this.spaceRepository.findOne({ + where: { + uuid: roomUuid, + spaceType: { + type: 'room', + }, + }, + relations: ['spaceType'], + }); + if (!room || !room.spaceType || room.spaceType.type !== 'room') { + throw new BadRequestException('Invalid room UUID'); + } + + return { + uuid: room.uuid, + createdAt: room.createdAt, + updatedAt: room.updatedAt, + name: room.spaceName, + type: room.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } + } + } + + async getRoomParentByUuid(roomUuid: string): Promise { + try { + const room = await this.spaceRepository.findOne({ + where: { + uuid: roomUuid, + spaceType: { + type: 'room', + }, + }, + relations: ['spaceType', 'parent', 'parent.spaceType'], + }); + if (!room || !room.spaceType || room.spaceType.type !== 'room') { + throw new BadRequestException('Invalid room UUID'); + } + + return { + 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 (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Room not found', HttpStatus.NOT_FOUND); + } + } + } + + 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 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, + ): 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); + } + } + } +} 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..40af2a8 --- /dev/null +++ b/src/scene/controllers/scene.controller.ts @@ -0,0 +1,141 @@ +import { SceneService } from '../services/scene.service'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos/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 getTapToRunSceneDetails(@Param('sceneId') sceneId: string) { + try { + const tapToRunScenes = + 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', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/scene/dtos/index.ts b/src/scene/dtos/index.ts new file mode 100644 index 0000000..b1ca9c8 --- /dev/null +++ b/src/scene/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene.dto'; diff --git a/src/scene/dtos/scene.dto.ts b/src/scene/dtos/scene.dto.ts new file mode 100644 index 0000000..ca39212 --- /dev/null +++ b/src/scene/dtos/scene.dto.ts @@ -0,0 +1,134 @@ +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); + } +} +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/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..b45caf8 --- /dev/null +++ b/src/scene/services/scene.service.ts @@ -0,0 +1,310 @@ +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +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'; +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'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async addTapToRunScene( + addSceneTapToRunDto: AddSceneTapToRunDto, + spaceTuyaId = null, + ) { + try { + 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, + }; + }); + + 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: unitSpaceTuyaId, + 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 unit UUID'); + } + return { + uuid: unit.uuid, + createdAt: unit.createdAt, + updatedAt: unit.updatedAt, + name: unit.spaceName, + type: unit.spaceType.type, + spaceTuyaUuid: unit.spaceTuyaUuid, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async 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, + spaceTuyaId = null, + ) { + try { + let unitSpaceTuyaId; + if (!spaceTuyaId) { + const unitDetails = await this.getUnitByUuid(unitUuid); + unitSpaceTuyaId = unitDetails.spaceTuyaUuid; + if (!unitSpaceTuyaId) { + throw new BadRequestException('Invalid unit UUID'); + } + } else { + unitSpaceTuyaId = spaceTuyaId; + } + + const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`; + 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 getTapToRunSceneDetails(sceneId: string, withSpaceId = false) { + 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); + 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: 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 + } else { + throw new HttpException( + err.message || 'Scene not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } +} 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..1d5cbd3 --- /dev/null +++ b/src/unit/controllers/unit.controller.ts @@ -0,0 +1,188 @@ +import { UnitService } from '../services/unit.service'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +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'; +import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; + +@ApiTags('Unit Module') +@Controller({ + version: '1', + path: 'unit', +}) +export class UnitController { + constructor(private readonly unitService: UnitService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) + @Post() + async addUnit(@Body() addUnitDto: AddUnitDto) { + try { + const unit = await this.unitService.addUnit(addUnitDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Unit added successfully', + data: unit, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid') + async getUnitByUuid(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitByUuid(unitUuid); + return unit; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get('parent/:unitUuid') + async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitParentByUuid(unitUuid); + return unit; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckUserUnitGuard) + @Post('user') + async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { + try { + await this.unitService.addUserUnit(addUserUnitDto); + 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, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('user/:userUuid') + async getUnitsByUserId(@Param('userUuid') userUuid: string) { + try { + return await this.unitService.getUnitsByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @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) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @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 new file mode 100644 index 0000000..9896c37 --- /dev/null +++ b/src/unit/dtos/add.unit.dto.ts @@ -0,0 +1,61 @@ +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); + } +} +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); + } +} +export class AddUserUnitUsingCodeDto { + @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/dtos/get.unit.dto.ts b/src/unit/dtos/get.unit.dto.ts new file mode 100644 index 0000000..2fae52a --- /dev/null +++ b/src/unit/dtos/get.unit.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, 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; +} +export class GetUnitByUserIdDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; +} 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/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 new file mode 100644 index 0000000..635f38e --- /dev/null +++ b/src/unit/interface/unit.interface.ts @@ -0,0 +1,37 @@ +export interface GetUnitByUuidInterface { + uuid: string; + createdAt: Date; + updatedAt: Date; + name: string; + type: string; + spaceTuyaUuid: 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; +} +export interface RenameUnitByUuidInterface { + uuid: string; + name: string; + type: string; +} +export interface GetUnitByUserUuidInterface { + uuid: string; + name: string; + type: string; +} +export interface addTuyaSpaceInterface { + success: boolean; + result: string; + msg: string; +} 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..1106043 --- /dev/null +++ b/src/unit/services/unit.service.ts @@ -0,0 +1,447 @@ +import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; +import { + UnitChildInterface, + UnitParentInterface, + GetUnitByUuidInterface, + RenameUnitByUuidInterface, + GetUnitByUserUuidInterface, + addTuyaSpaceInterface, +} 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'; +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'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async addUnit(addUnitDto: AddUnitDto) { + try { + const spaceType = await this.spaceTypeRepository.findOne({ + where: { + 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 { + const unit = await this.spaceRepository.findOne({ + where: { + uuid: unitUuid, + spaceType: { + type: 'unit', + }, + }, + relations: ['spaceType'], + }); + if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); + } + return { + uuid: unit.uuid, + createdAt: unit.createdAt, + updatedAt: unit.updatedAt, + name: unit.spaceName, + type: unit.spaceType.type, + spaceTuyaUuid: unit.spaceTuyaUuid, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async getUnitChildByUuid( + unitUuid: string, + getUnitChildDto: GetUnitChildDto, + ): Promise { + try { + const { page, pageSize } = getUnitChildDto; + + const space = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['children', 'spaceType'], + }); + + 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, false, 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); + } + } + } + + 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' && + child.spaceType.type !== 'unit', + ) // Filter remaining unit and floor and community and unit 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' && + child.spaceType.type !== 'unit', + ) // Filter remaining unit and floor and community and unit 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 || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); + } + 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) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + 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 addUserUnit(addUserUnitDto: AddUserUnitDto) { + try { + return 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, + ): 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); + } + } + } + async getUnitInvitationCode(unitUuid: string): Promise { + try { + // Generate a 6-character random invitation code + const invitationCode = generateRandomString(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.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', + HttpStatus.BAD_REQUEST, + ); + } + } + + 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 new file mode 100644 index 0000000..7924e4a --- /dev/null +++ b/src/unit/unit.module.ts @@ -0,0 +1,38 @@ +import { Module } 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 { 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'; +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: [ + ConfigModule, + SpaceRepositoryModule, + SpaceTypeRepositoryModule, + UserSpaceRepositoryModule, + UserRepositoryModule, + ], + controllers: [UnitController], + providers: [ + UnitService, + SpaceRepository, + SpaceTypeRepository, + UserSpaceRepository, + UserRepository, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, + ], + exports: [UnitService], +}) +export class UnitModule {} diff --git a/src/user-device-permission/controllers/index.ts b/src/user-device-permission/controllers/index.ts new file mode 100644 index 0000000..9ea28cf --- /dev/null +++ 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 new file mode 100644 index 0000000..2f68708 --- /dev/null +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -0,0 +1,114 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + 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 { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; +import { AdminRoleGuard } from 'src/guards/admin.role.guard'; + +@ApiTags('Device Permission Module') +@Controller({ + version: '1', + path: 'device-permission', +}) +export class UserDevicePermissionController { + constructor( + private readonly userDevicePermissionService: UserDevicePermissionService, + ) {} + + @ApiBearerAuth() + @UseGuards(AdminRoleGuard) + @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 (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(AdminRoleGuard) + @Put('edit/:devicePermissionUuid') + async editDevicePermission( + @Param('devicePermissionUuid') devicePermissionUuid: string, + @Body() userDevicePermissionEditDto: UserDevicePermissionEditDto, + ) { + try { + await this.userDevicePermissionService.editUserPermission( + devicePermissionUuid, + userDevicePermissionEditDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User Permission for Devices Updated Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(AdminRoleGuard) + @Get(':deviceUuid/list') + async fetchDevicePermission(@Param('deviceUuid') deviceUuid: string) { + try { + const deviceDetails = + await this.userDevicePermissionService.fetchUserPermission(deviceUuid); + return { + statusCode: HttpStatus.OK, + message: 'Device Details fetched Successfully', + data: deviceDetails, + }; + } catch (err) { + throw new Error(err); + } + } + @ApiBearerAuth() + @UseGuards(AdminRoleGuard) + @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 new file mode 100644 index 0000000..7792d07 --- /dev/null +++ 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 new file mode 100644 index 0000000..12ee133 --- /dev/null +++ b/src/user-device-permission/dtos/user-device-permission.add.dto.ts @@ -0,0 +1,29 @@ +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class UserDevicePermissionAddDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'permission type', + enum: PermissionType, + required: true, + }) + @IsEnum(PermissionType) + permissionType: PermissionType; + + @ApiProperty({ + description: 'device uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + 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 new file mode 100644 index 0000000..ce537e1 --- /dev/null +++ b/src/user-device-permission/dtos/user-device-permission.edit.dto.ts @@ -0,0 +1,13 @@ +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +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 new file mode 100644 index 0000000..1dc9e53 --- /dev/null +++ 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 new file mode 100644 index 0000000..c1f3d07 --- /dev/null +++ b/src/user-device-permission/services/user-device-permission.service.ts @@ -0,0 +1,107 @@ +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) { + 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 User already belongs to this device', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async editUserPermission( + devicePermissionUuid: string, + userDevicePermissionEditDto: UserDevicePermissionEditDto, + ) { + 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 User already belongs to this device', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + 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 new file mode 100644 index 0000000..e2a8b46 --- /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 } 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'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; + +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [UserDevicePermissionController], + providers: [ + DeviceUserPermissionRepository, + PermissionTypeRepository, + DeviceRepository, + UserDevicePermissionService, + ], + exports: [UserDevicePermissionService], +}) +export class UserDevicePermissionModule {} 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 {} 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/src/users/controllers/user.controller.ts b/src/users/controllers/user.controller.ts new file mode 100644 index 0000000..9d84c83 --- /dev/null +++ b/src/users/controllers/user.controller.ts @@ -0,0 +1,25 @@ +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 { AdminRoleGuard } from 'src/guards/admin.role.guard'; + +@ApiTags('User Module') +@Controller({ + version: '1', + path: 'user', +}) +export class UserController { + constructor(private readonly userService: UserService) {} + + @ApiBearerAuth() + @UseGuards(AdminRoleGuard) + @Get('list1') + async userList(@Query() userListDto: UserListDto) { + try { + return await this.userService.userDetails(userListDto); + } catch (err) { + throw new Error(err); + } + } +} 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/src/users/dtos/user.list.dto.ts b/src/users/dtos/user.list.dto.ts new file mode 100644 index 0000000..327a2ba --- /dev/null +++ b/src/users/dtos/user.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/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/src/users/services/user.service.ts b/src/users/services/user.service.ts new file mode 100644 index 0000000..774293b --- /dev/null +++ b/src/users/services/user.service.ts @@ -0,0 +1,28 @@ +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 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 tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + async userDetails(userListDto: UserListDto) { + const path = `/v2.0/apps/${userListDto.schema}/users`; + const data = await this.tuya.request({ + method: 'GET', + path, + query: userListDto, + }); + return data; + } +} diff --git a/src/users/user.module.ts b/src/users/user.module.ts new file mode 100644 index 0000000..be9e33d --- /dev/null +++ b/src/users/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/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