Merge pull request #59 from SyncrowIOT/dev

Merge Dev to Main Branches for the Staging environment
This commit is contained in:
faris Aljohari
2024-07-15 12:11:59 +03:00
committed by GitHub
338 changed files with 13781 additions and 479 deletions

2
.deployment Normal file
View File

@ -0,0 +1,2 @@
[config]
SCM_DO_BUILD_DURING_DEPLOYMENT=true

55
.github/workflows/dev_syncrow(dev).yml vendored Normal file
View 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
View File

@ -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
View 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
View 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"]

View File

@ -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
![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 | | |
| +------------------+----------------+ |
+-----------------------------------------------------------------+

View File

@ -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!');
});
});
});

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -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();

View File

@ -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!');
});
});

View File

@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -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!');
});
});
});

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -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();

View File

@ -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!');
});
});

View File

@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/backend"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

BIN
bun.lockb Executable file

Binary file not shown.

24
jest.config.js Normal file
View 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"
}
};

View File

@ -0,0 +1,5 @@
describe('Dummy Test', () => {
it('should pass', () => {
expect(true).toBe(true);
});
});

View 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 {}

View File

@ -0,0 +1,8 @@
export class AuthInterface {
email: string;
userId: number;
uuid: string;
sessionId: string;
id: number;
roles?: string[];
}

View 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);
}
}

View 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');
}
}
}

View 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');
}
}
}

View 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 {}

View 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();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class CommonService {}

View 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,
}),
);

View 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];

View 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,
}),
);

View 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,
}),
);

View 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];
}

View 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;

View 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 '';
}
}

View 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,
}),
);

View File

@ -0,0 +1,4 @@
export enum OtpType {
VERIFICATION = 'VERIFICATION',
PASSWORD = 'PASSWORD',
}

View File

@ -0,0 +1,4 @@
export enum PermissionType {
READ = 'READ',
CONTROLLABLE = 'CONTROLLABLE',
}

View File

@ -0,0 +1,8 @@
export enum ProductType {
AC = 'AC',
GW = 'GW',
CPS = 'CPS',
DL = 'DL',
WPS = 'WPS',
TH_G = '3G',
}

View File

@ -0,0 +1,4 @@
export enum RoleType {
SUPER_ADMIN = 'SUPER_ADMIN',
ADMIN = 'ADMIN',
}

View File

@ -0,0 +1,7 @@
export enum SpaceType {
COMMUNITY = 'community',
BUILDING = 'building',
FLOOR = 'floor',
UNIT = 'unit',
ROOM = 'room',
}

View File

@ -0,0 +1,9 @@
export enum WorkingDays {
Sun = 'Sun',
Mon = 'Mon',
Tue = 'Tue',
Wed = 'Wed',
Thu = 'Thu',
Fri = 'Fri',
Sat = 'Sat',
}

View 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 {}

View File

@ -0,0 +1 @@
export * from './snack-naming.strategy';

View 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}`);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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 {}

View 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;
}

View 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);
}
}

View 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);
}
}

View File

@ -0,0 +1,2 @@
export * from './helper.hash.service';
export * from './space.permission.service';

View 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');
}
}
}

View 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');
}
}
}

View 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);
});
}
}

View 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
View File

@ -0,0 +1,2 @@
export * from './common.module';
export * from './common.service';

View 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;
}
}
}

View File

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

View 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);
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

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

View File

@ -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);
}
}

View File

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

View File

@ -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());
}
}

View File

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

View File

@ -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 {}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './device.user.permission.dto';

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export * from './device.user.permission.entity';

View File

@ -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());
}
}

View File

@ -0,0 +1 @@
export * from './device.user.permission.repository';

View 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 {}

View 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;
}

View File

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

View 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);
}
}

View File

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

View File

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

View File

@ -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());
}
}

View File

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

View File

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

View 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;
}

View File

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

View File

@ -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);
}
}

View File

@ -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 {}

View File

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

View File

@ -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());
}
}

View File

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

View 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;
}

View File

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

View 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);
}
}

View 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 {}

View File

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

Some files were not shown because too many files have changed in this diff Show More