mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-11-26 09:54:55 +00:00
Merge pull request #59 from SyncrowIOT/dev
Merge Dev to Main Branches for the Staging environment
This commit is contained in:
2
.deployment
Normal file
2
.deployment
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[config]
|
||||||
|
SCM_DO_BUILD_DURING_DEPLOYMENT=true
|
||||||
55
.github/workflows/dev_syncrow(dev).yml
vendored
Normal file
55
.github/workflows/dev_syncrow(dev).yml
vendored
Normal file
@ -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 }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -54,3 +54,5 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
config.dev
|
||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -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{,/**}"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -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"]
|
||||||
71
README.md
71
README.md
@ -1,8 +1,27 @@
|
|||||||
# Backend
|
# 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.
|
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
|
## Installation
|
||||||
First, ensure that you have Node.js `v20.11` or newer (LTS ONLY) installed on your system.
|
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
|
$ npm run test:cov
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## ERD Diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 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 | | |
|
||||||
|
| +------------------+----------------+ |
|
||||||
|
+-----------------------------------------------------------------+
|
||||||
|
|||||||
@ -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>(AuthController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(authController.getHello()).toBe('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
@ -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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
@ -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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": false,
|
|
||||||
"outDir": "../../dist/apps/backend"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
24
jest.config.js
Normal file
24
jest.config.js
Normal file
@ -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: [
|
||||||
|
"<rootDir>/src/",
|
||||||
|
"<rootDir>/libs/"
|
||||||
|
],
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@app/common(|/.*)$": "<rootDir>/libs/common/src/$1"
|
||||||
|
}
|
||||||
|
};
|
||||||
5
libs/common/dummy.spec.ts
Normal file
5
libs/common/dummy.spec.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
describe('Dummy Test', () => {
|
||||||
|
it('should pass', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
libs/common/src/auth/auth.module.ts
Normal file
28
libs/common/src/auth/auth.module.ts
Normal file
@ -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 {}
|
||||||
8
libs/common/src/auth/interfaces/auth.interface.ts
Normal file
8
libs/common/src/auth/interfaces/auth.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class AuthInterface {
|
||||||
|
email: string;
|
||||||
|
userId: number;
|
||||||
|
uuid: string;
|
||||||
|
sessionId: string;
|
||||||
|
id: number;
|
||||||
|
roles?: string[];
|
||||||
|
}
|
||||||
94
libs/common/src/auth/services/auth.service.ts
Normal file
94
libs/common/src/auth/services/auth.service.ts
Normal file
@ -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<any> {
|
||||||
|
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<UserSessionEntity> {
|
||||||
|
return await this.sessionRepository.save(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokens(payload) {
|
||||||
|
const [accessToken, refreshToken] = await Promise.all([
|
||||||
|
this.jwtService.signAsync(payload, {
|
||||||
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
|
expiresIn: '24h',
|
||||||
|
}),
|
||||||
|
this.jwtService.signAsync(payload, {
|
||||||
|
secret: this.configService.get<string>('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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
libs/common/src/auth/strategies/jwt.strategy.ts
Normal file
40
libs/common/src/auth/strategies/jwt.strategy.ts
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
libs/common/src/auth/strategies/refresh-token.strategy.ts
Normal file
43
libs/common/src/auth/strategies/refresh-token.strategy.ts
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
libs/common/src/common.module.ts
Normal file
23
libs/common/src/common.module.ts
Normal file
@ -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 {}
|
||||||
18
libs/common/src/common.service.spec.ts
Normal file
18
libs/common/src/common.service.spec.ts
Normal file
@ -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>(CommonService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
4
libs/common/src/common.service.ts
Normal file
4
libs/common/src/common.service.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommonService {}
|
||||||
12
libs/common/src/config/email.config.ts
Normal file
12
libs/common/src/config/email.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'email-config',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
6
libs/common/src/config/index.ts
Normal file
6
libs/common/src/config/index.ts
Normal file
@ -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];
|
||||||
9
libs/common/src/config/onesignal.config.ts
Normal file
9
libs/common/src/config/onesignal.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'onesignal-config',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID,
|
||||||
|
ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
9
libs/common/src/config/super.admin.config.ts
Normal file
9
libs/common/src/config/super.admin.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'super-admin',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
SUPER_ADMIN_EMAIL: process.env.SUPER_ADMIN_EMAIL,
|
||||||
|
SUPER_ADMIN_PASSWORD: process.env.SUPER_ADMIN_PASSWORD,
|
||||||
|
}),
|
||||||
|
);
|
||||||
30
libs/common/src/config/tuya-web-socket-config/config.ts
Normal file
30
libs/common/src/config/tuya-web-socket-config/config.ts
Normal file
@ -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<K extends keyof IEnvConfig>(
|
||||||
|
env: TUYA_PASULAR_ENV,
|
||||||
|
): IEnvConfig[K] {
|
||||||
|
return TuyaEnvConfig[env];
|
||||||
|
}
|
||||||
212
libs/common/src/config/tuya-web-socket-config/index.ts
Normal file
212
libs/common/src/config/tuya-web-socket-config/index.ts
Normal file
@ -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;
|
||||||
51
libs/common/src/config/tuya-web-socket-config/utils.ts
Normal file
51
libs/common/src/config/tuya-web-socket-config/utils.ts
Normal file
@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
12
libs/common/src/config/tuya.config.ts
Normal file
12
libs/common/src/config/tuya.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'tuya-config',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
4
libs/common/src/constants/otp-type.enum.ts
Normal file
4
libs/common/src/constants/otp-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum OtpType {
|
||||||
|
VERIFICATION = 'VERIFICATION',
|
||||||
|
PASSWORD = 'PASSWORD',
|
||||||
|
}
|
||||||
4
libs/common/src/constants/permission-type.enum.ts
Normal file
4
libs/common/src/constants/permission-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum PermissionType {
|
||||||
|
READ = 'READ',
|
||||||
|
CONTROLLABLE = 'CONTROLLABLE',
|
||||||
|
}
|
||||||
8
libs/common/src/constants/product-type.enum.ts
Normal file
8
libs/common/src/constants/product-type.enum.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum ProductType {
|
||||||
|
AC = 'AC',
|
||||||
|
GW = 'GW',
|
||||||
|
CPS = 'CPS',
|
||||||
|
DL = 'DL',
|
||||||
|
WPS = 'WPS',
|
||||||
|
TH_G = '3G',
|
||||||
|
}
|
||||||
4
libs/common/src/constants/role.type.enum.ts
Normal file
4
libs/common/src/constants/role.type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum RoleType {
|
||||||
|
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
}
|
||||||
7
libs/common/src/constants/space-type.enum.ts
Normal file
7
libs/common/src/constants/space-type.enum.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum SpaceType {
|
||||||
|
COMMUNITY = 'community',
|
||||||
|
BUILDING = 'building',
|
||||||
|
FLOOR = 'floor',
|
||||||
|
UNIT = 'unit',
|
||||||
|
ROOM = 'room',
|
||||||
|
}
|
||||||
9
libs/common/src/constants/working-days.ts
Normal file
9
libs/common/src/constants/working-days.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export enum WorkingDays {
|
||||||
|
Sun = 'Sun',
|
||||||
|
Mon = 'Mon',
|
||||||
|
Tue = 'Tue',
|
||||||
|
Wed = 'Wed',
|
||||||
|
Thu = 'Thu',
|
||||||
|
Fri = 'Fri',
|
||||||
|
Sat = 'Sat',
|
||||||
|
}
|
||||||
66
libs/common/src/database/database.module.ts
Normal file
66
libs/common/src/database/database.module.ts
Normal file
@ -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 {}
|
||||||
1
libs/common/src/database/strategies/index.ts
Normal file
1
libs/common/src/database/strategies/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './snack-naming.strategy';
|
||||||
61
libs/common/src/database/strategies/snack-naming.strategy.ts
Normal file
61
libs/common/src/database/strategies/snack-naming.strategy.ts
Normal file
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/common/src/guards/jwt-refresh.auth.guard.ts
Normal file
11
libs/common/src/guards/jwt-refresh.auth.guard.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/common/src/guards/jwt.auth.guard.ts
Normal file
11
libs/common/src/guards/jwt.auth.guard.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
libs/common/src/helper/camelCaseConverter.ts
Normal file
20
libs/common/src/helper/camelCaseConverter.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
28
libs/common/src/helper/helper.module.ts
Normal file
28
libs/common/src/helper/helper.module.ts
Normal file
@ -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 {}
|
||||||
10
libs/common/src/helper/randomString.ts
Normal file
10
libs/common/src/helper/randomString.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
53
libs/common/src/helper/services/device.messages.service.ts
Normal file
53
libs/common/src/helper/services/device.messages.service.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
libs/common/src/helper/services/helper.hash.service.ts
Normal file
63
libs/common/src/helper/services/helper.hash.service.ts
Normal file
@ -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<string, any> | Record<string, any>[],
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
libs/common/src/helper/services/index.ts
Normal file
2
libs/common/src/helper/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './helper.hash.service';
|
||||||
|
export * from './space.permission.service';
|
||||||
41
libs/common/src/helper/services/onesignal.service.ts
Normal file
41
libs/common/src/helper/services/onesignal.service.ts
Normal file
@ -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<string>('onesignal-config.ONESIGNAL_APP_ID'),
|
||||||
|
this.configService.get<string>('onesignal-config.ONESIGNAL_API_KEY'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendNotification(
|
||||||
|
content: string,
|
||||||
|
title: string,
|
||||||
|
subscriptionIds: string[],
|
||||||
|
): Promise<any> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
libs/common/src/helper/services/space.permission.service.ts
Normal file
39
libs/common/src/helper/services/space.permission.service.ts
Normal file
@ -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<void> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
libs/common/src/helper/services/tuya.web.socket.service.ts
Normal file
72
libs/common/src/helper/services/tuya.web.socket.service.ts
Normal file
@ -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<string>('tuya-config.TUYA_ACCESS_ID'),
|
||||||
|
accessKey: this.configService.get<string>('tuya-config.TUYA_ACCESS_KEY'),
|
||||||
|
url: TuyaWebsocket.URL.EU,
|
||||||
|
env: TuyaWebsocket.env.TEST,
|
||||||
|
maxRetryTimes: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.configService.get<string>('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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
16
libs/common/src/helper/snakeCaseConverter.ts
Normal file
16
libs/common/src/helper/snakeCaseConverter.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
2
libs/common/src/index.ts
Normal file
2
libs/common/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './common.module';
|
||||||
|
export * from './common.service';
|
||||||
13
libs/common/src/modules/abstract/dtos/abstract.dto.ts
Normal file
13
libs/common/src/modules/abstract/dtos/abstract.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/modules/abstract/dtos/index.ts
Normal file
1
libs/common/src/modules/abstract/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './abstract.dto';
|
||||||
40
libs/common/src/modules/abstract/entities/abstract.entity.ts
Normal file
40
libs/common/src/modules/abstract/entities/abstract.entity.ts
Normal file
@ -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<T, [AbstractEntity, O?]>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.notification.dto';
|
||||||
@ -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<DeviceNotificationDto> {
|
||||||
|
@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<DeviceNotificationEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.notification.entity';
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DeviceNotificationEntity } from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeviceNotificationRepository extends Repository<DeviceNotificationEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(DeviceNotificationEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.notification.repository';
|
||||||
@ -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 {}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.user.permission.dto';
|
||||||
@ -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<DeviceUserPermissionDto> {
|
||||||
|
@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<DeviceUserPermissionEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.user.permission.entity';
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DeviceUserPermissionEntity } from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeviceUserPermissionRepository extends Repository<DeviceUserPermissionEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(DeviceUserPermissionEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './device.user.permission.repository';
|
||||||
11
libs/common/src/modules/device/device.repository.module.ts
Normal file
11
libs/common/src/modules/device/device.repository.module.ts
Normal file
@ -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 {}
|
||||||
23
libs/common/src/modules/device/dtos/device.dto.ts
Normal file
23
libs/common/src/modules/device/dtos/device.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
1
libs/common/src/modules/device/dtos/index.ts
Normal file
1
libs/common/src/modules/device/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './device.dto';
|
||||||
57
libs/common/src/modules/device/entities/device.entity.ts
Normal file
57
libs/common/src/modules/device/entities/device.entity.ts
Normal file
@ -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<DeviceDto> {
|
||||||
|
@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<DeviceEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/modules/device/entities/index.ts
Normal file
1
libs/common/src/modules/device/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './device.entity';
|
||||||
1
libs/common/src/modules/device/index.ts
Normal file
1
libs/common/src/modules/device/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './device.repository.module';
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DeviceEntity } from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeviceRepository extends Repository<DeviceEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(DeviceEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/modules/device/repositories/index.ts
Normal file
1
libs/common/src/modules/device/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './device.repository';
|
||||||
1
libs/common/src/modules/permission/dtos/index.ts
Normal file
1
libs/common/src/modules/permission/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './permission.dto';
|
||||||
11
libs/common/src/modules/permission/dtos/permission.dto.ts
Normal file
11
libs/common/src/modules/permission/dtos/permission.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
1
libs/common/src/modules/permission/entities/index.ts
Normal file
1
libs/common/src/modules/permission/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './permission.entity';
|
||||||
@ -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<PermissionTypeDto> {
|
||||||
|
@Column({
|
||||||
|
nullable: false,
|
||||||
|
enum: Object.values(PermissionType),
|
||||||
|
})
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => DeviceUserPermissionEntity,
|
||||||
|
(permission) => permission.permissionType,
|
||||||
|
{
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
permission: DeviceUserPermissionEntity[];
|
||||||
|
|
||||||
|
constructor(partial: Partial<PermissionTypeEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {}
|
||||||
1
libs/common/src/modules/permission/repositories/index.ts
Normal file
1
libs/common/src/modules/permission/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './permission.repository';
|
||||||
@ -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<PermissionTypeEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(PermissionTypeEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/modules/product/dtos/index.ts
Normal file
1
libs/common/src/modules/product/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './product.dto';
|
||||||
19
libs/common/src/modules/product/dtos/product.dto.ts
Normal file
19
libs/common/src/modules/product/dtos/product.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
1
libs/common/src/modules/product/entities/index.ts
Normal file
1
libs/common/src/modules/product/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './product.entity';
|
||||||
33
libs/common/src/modules/product/entities/product.entity.ts
Normal file
33
libs/common/src/modules/product/entities/product.entity.ts
Normal file
@ -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<ProductDto> {
|
||||||
|
@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<ProductEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/common/src/modules/product/product.repository.module.ts
Normal file
11
libs/common/src/modules/product/product.repository.module.ts
Normal file
@ -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 {}
|
||||||
1
libs/common/src/modules/product/repositories/index.ts
Normal file
1
libs/common/src/modules/product/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './product.repository';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user