diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index e9d3f01..8c3e78c 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -13,6 +13,7 @@ import { EmailService } from './util/email.service'; imports: [ ConfigModule.forRoot({ load: config, + isGlobal: true, }), DatabaseModule, HelperModule, diff --git a/libs/common/src/config/index.ts b/libs/common/src/config/index.ts index d4cbbdb..b642e54 100644 --- a/libs/common/src/config/index.ts +++ b/libs/common/src/config/index.ts @@ -1,3 +1,6 @@ import emailConfig from './email.config'; import superAdminConfig from './super.admin.config'; -export default [emailConfig, superAdminConfig]; +import tuyaConfig from './tuya.config'; +import oneSignalConfig from './onesignal.config'; + +export default [emailConfig, superAdminConfig, tuyaConfig, oneSignalConfig]; diff --git a/libs/common/src/config/onesignal.config.ts b/libs/common/src/config/onesignal.config.ts new file mode 100644 index 0000000..5567fd1 --- /dev/null +++ b/libs/common/src/config/onesignal.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'onesignal-config', + (): Record => ({ + ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID, + ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY, + }), +); diff --git a/libs/common/src/config/tuya-web-socket-config/config.ts b/libs/common/src/config/tuya-web-socket-config/config.ts new file mode 100644 index 0000000..5103805 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/config.ts @@ -0,0 +1,30 @@ +export enum TuyaRegionConfigEnum { + CN = 'wss://mqe.tuyacn.com:8285/', + US = 'wss://mqe.tuyaus.com:8285/', + EU = 'wss://mqe.tuyaeu.com:8285/', + IN = 'wss://mqe.tuyain.com:8285/', +} + +export enum TUYA_PASULAR_ENV { + PROD = 'prod', + TEST = 'test', +} + +export const TuyaEnvConfig = Object.freeze({ + [TUYA_PASULAR_ENV.PROD]: { + name: TUYA_PASULAR_ENV.PROD, + value: 'event', + desc: 'online environment', + }, + [TUYA_PASULAR_ENV.TEST]: { + name: TUYA_PASULAR_ENV.TEST, + value: 'event-test', + desc: 'test environment', + }, +}); +type IEnvConfig = typeof TuyaEnvConfig; +export function getTuyaEnvConfig( + env: TUYA_PASULAR_ENV, +): IEnvConfig[K] { + return TuyaEnvConfig[env]; +} diff --git a/libs/common/src/config/tuya-web-socket-config/index.ts b/libs/common/src/config/tuya-web-socket-config/index.ts new file mode 100644 index 0000000..0b41b23 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/index.ts @@ -0,0 +1,212 @@ +import { EventEmitter } from 'events'; +import { WebSocket } from 'ws'; + +import { + TUYA_PASULAR_ENV, + getTuyaEnvConfig, + TuyaRegionConfigEnum, +} from './config'; +import { getTopicUrl, buildQuery, buildPassword, decrypt } from './utils'; + +type LoggerLevel = 'INFO' | 'ERROR'; + +interface IConfig { + accessId: string; + accessKey: string; + env: TUYA_PASULAR_ENV; + url: TuyaRegionConfigEnum; + + timeout?: number; + maxRetryTimes?: number; + retryTimeout?: number; + logger?: (level: LoggerLevel, ...args: any) => void; +} + +class TuyaMessageSubscribeWebsocket { + static URL = TuyaRegionConfigEnum; + static env = TUYA_PASULAR_ENV; + + static data = 'TUTA_DATA'; + static error = 'TUYA_ERROR'; + static open = 'TUYA_OPEN'; + static close = 'TUYA_CLOSE'; + static reconnect = 'TUYA_RECONNECT'; + static ping = 'TUYA_PING'; + static pong = 'TUYA_PONG'; + + private config: IConfig; + private server?: WebSocket; + private timer: any; + private retryTimes: number; + private event: EventEmitter; + + constructor(config: IConfig) { + this.config = Object.assign( + { + ackTimeoutMillis: 3000, + subscriptionType: 'Failover', + retryTimeout: 1000, + maxRetryTimes: 100, + timeout: 30000, + logger: console.log, + }, + config, + ); + this.event = new EventEmitter(); + this.retryTimes = 0; + } + + public start() { + this.server = this._connect(); + } + + public open(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.open, cb); + } + + public message(cb: (ws: WebSocket, message: any) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.data, cb); + } + + public ping(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.ping, cb); + } + + public pong(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.pong, cb); + } + + public reconnect(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.reconnect, cb); + } + + public ackMessage(messageId: string) { + this.server && this.server.send(JSON.stringify({ messageId })); + } + + public error(cb: (ws: WebSocket, error: any) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.error, cb); + } + + public close(cb: (ws: WebSocket) => void) { + this.event.on(TuyaMessageSubscribeWebsocket.close, cb); + } + + private _reconnect() { + if ( + this.config.maxRetryTimes && + this.retryTimes < this.config.maxRetryTimes + ) { + const timer = setTimeout(() => { + clearTimeout(timer); + this.retryTimes++; + this._connect(false); + }, this.config.retryTimeout); + } + } + + private _connect(isInit = true) { + const { accessId, accessKey, env, url } = this.config; + const topicUrl = getTopicUrl( + url, + accessId, + getTuyaEnvConfig(env).value, + `?${buildQuery({ subscriptionType: 'Failover', ackTimeoutMillis: 30000 })}`, + ); + const password = buildPassword(accessId, accessKey); + this.server = new WebSocket(topicUrl, { + rejectUnauthorized: false, + headers: { username: accessId, password }, + }); + this.subOpen(this.server, isInit); + this.subMessage(this.server); + this.subPing(this.server); + this.subPong(this.server); + this.subError(this.server); + this.subClose(this.server); + return this.server; + } + + private subOpen(server: WebSocket, isInit = true) { + server.on('open', () => { + if (server.readyState === server.OPEN) { + this.retryTimes = 0; + } + this.keepAlive(server); + this.event.emit( + isInit + ? TuyaMessageSubscribeWebsocket.open + : TuyaMessageSubscribeWebsocket.reconnect, + this.server, + ); + }); + } + + private subPing(server: WebSocket) { + server.on('ping', () => { + this.event.emit(TuyaMessageSubscribeWebsocket.ping, this.server); + this.keepAlive(server); + server.pong(this.config.accessId); + }); + } + + private subPong(server: WebSocket) { + server.on('pong', () => { + this.keepAlive(server); + this.event.emit(TuyaMessageSubscribeWebsocket.pong, this.server); + }); + } + + private subMessage(server: WebSocket) { + server.on('message', (data: any) => { + try { + this.keepAlive(server); + const obj = this.handleMessage(data); + this.event.emit(TuyaMessageSubscribeWebsocket.data, this.server, obj); + } catch (e) { + this.logger('ERROR', e); + this.event.emit(TuyaMessageSubscribeWebsocket.error, e); + } + }); + } + + private subClose(server: WebSocket) { + server.on('close', (...data) => { + this._reconnect(); + this.clearKeepAlive(); + this.event.emit(TuyaMessageSubscribeWebsocket.close, ...data); + }); + } + + private subError(server: WebSocket) { + server.on('error', (e) => { + this.event.emit(TuyaMessageSubscribeWebsocket.error, this.server, e); + }); + } + + private clearKeepAlive() { + clearTimeout(this.timer); + } + + private keepAlive(server: WebSocket) { + this.clearKeepAlive(); + this.timer = setTimeout(() => { + server.ping(this.config.accessId); + }, this.config.timeout); + } + + private handleMessage(data: string) { + const { payload, ...others } = JSON.parse(data); + const pStr = Buffer.from(payload, 'base64').toString('utf-8'); + const pJson = JSON.parse(pStr); + pJson.data = decrypt(pJson.data, this.config.accessKey); + return { payload: pJson, ...others }; + } + + private logger(level: LoggerLevel, ...info: any) { + const realInfo = `${Date.now()} `; + this.config.logger && this.config.logger(level, realInfo, ...info); + } +} + +export default TuyaMessageSubscribeWebsocket; diff --git a/libs/common/src/config/tuya-web-socket-config/utils.ts b/libs/common/src/config/tuya-web-socket-config/utils.ts new file mode 100644 index 0000000..5c22043 --- /dev/null +++ b/libs/common/src/config/tuya-web-socket-config/utils.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MD5, AES, enc, mode, pad } from 'crypto-js'; + +export function getTopicUrl( + websocketUrl: string, + accessId: string, + env: string, + query: string, +) { + return `${websocketUrl}ws/v2/consumer/persistent/${accessId}/out/${env}/${accessId}-sub${query}`; +} + +export function buildQuery(query: { [key: string]: number | string }) { + return Object.keys(query) + .map((key) => `${key}=${encodeURIComponent(query[key])}`) + .join('&'); +} + +export function buildPassword(accessId: string, accessKey: string) { + const key = MD5(accessKey).toString(); + return MD5(`${accessId}${key}`).toString().substr(8, 16); +} + +export function decrypt(data: string, accessKey: string) { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const json = AES.decrypt(data, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }); + const dataStr = enc.Utf8.stringify(json).toString(); + return JSON.parse(dataStr); + } catch (e) { + return ''; + } +} + +export function encrypt(data: any, accessKey: string) { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const realData = JSON.stringify(data); + const retData = AES.encrypt(realData, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }).toString(); + return retData; + } catch (e) { + return ''; + } +} diff --git a/libs/common/src/config/tuya.config.ts b/libs/common/src/config/tuya.config.ts new file mode 100644 index 0000000..ca4dd73 --- /dev/null +++ b/libs/common/src/config/tuya.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'tuya-config', + (): Record => ({ + TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID, + TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY, + TRUN_ON_TUYA_SOCKET: + process.env.TRUN_ON_TUYA_SOCKET === 'true' ? true : false, + }), +); diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ce51bae..bfda450 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -16,6 +16,8 @@ import { GroupDeviceEntity } from '../modules/group-device/entities'; import { DeviceUserPermissionEntity } from '../modules/device-user-permission/entities'; import { UserRoleEntity } from '../modules/user-role/entities'; import { RoleTypeEntity } from '../modules/role-type/entities'; +import { UserNotificationEntity } from '../modules/user-notification/entities'; +import { DeviceNotificationEntity } from '../modules/device-notification/entities'; @Module({ imports: [ @@ -46,6 +48,8 @@ import { RoleTypeEntity } from '../modules/role-type/entities'; DeviceUserPermissionEntity, UserRoleEntity, RoleTypeEntity, + UserNotificationEntity, + DeviceNotificationEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index baa8f07..3378051 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -3,12 +3,26 @@ import { HelperHashService } from './services'; import { SpacePermissionService } from './services/space.permission.service'; import { SpaceRepository } from '../modules/space/repositories'; import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; +import { TuyaWebSocketService } from './services/tuya.web.socket.service'; + +import { OneSignalService } from './services/onesignal.service'; +import { DeviceMessagesService } from './services/device.messages.service'; +import { DeviceNotificationRepositoryModule } from '../modules/device-notification/device.notification.module'; +import { DeviceNotificationRepository } from '../modules/device-notification/repositories'; @Global() @Module({ - providers: [HelperHashService, SpacePermissionService, SpaceRepository], + providers: [ + HelperHashService, + SpacePermissionService, + SpaceRepository, + TuyaWebSocketService, + OneSignalService, + DeviceMessagesService, + DeviceNotificationRepository, + ], exports: [HelperHashService, SpacePermissionService], controllers: [], - imports: [SpaceRepositoryModule], + imports: [SpaceRepositoryModule, DeviceNotificationRepositoryModule], }) export class HelperModule {} diff --git a/libs/common/src/helper/services/device.messages.service.ts b/libs/common/src/helper/services/device.messages.service.ts new file mode 100644 index 0000000..c51f6f4 --- /dev/null +++ b/libs/common/src/helper/services/device.messages.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; +import { OneSignalService } from './onesignal.service'; + +@Injectable() +export class DeviceMessagesService { + constructor( + private readonly deviceNotificationRepository: DeviceNotificationRepository, + private readonly oneSignalService: OneSignalService, + ) {} + + async getDevicesUserNotifications(deviceTuyaUuid: string, bizData: any) { + try { + // Retrieve notifications for the specified device + const notifications = await this.deviceNotificationRepository.find({ + where: { + device: { + deviceTuyaUuid, + }, + }, + relations: ['user', 'user.userNotification'], + }); + + // If notifications are found, send them + if (notifications) { + await this.sendNotifications(notifications, bizData); + } + } catch (error) { + console.error('Error fetching device notifications:', error); + } + } + + private async sendNotifications(notifications: any[], bizData: any) { + const notificationPromises = []; + + // Iterate over each notification and its associated user notifications + notifications.forEach((notification) => { + notification.user.userNotification.forEach((userNotification) => { + // Queue notification sending without awaiting + notificationPromises.push( + this.oneSignalService.sendNotification( + JSON.stringify(bizData), + 'device-status', + [userNotification.subscriptionUuid], + ), + ); + }); + }); + + // Wait for all notification sending operations to complete + await Promise.all(notificationPromises); + } +} diff --git a/libs/common/src/helper/services/onesignal.service.ts b/libs/common/src/helper/services/onesignal.service.ts new file mode 100644 index 0000000..1aa2c38 --- /dev/null +++ b/libs/common/src/helper/services/onesignal.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as OneSignal from 'onesignal-node'; + +@Injectable() +export class OneSignalService { + private client: any; + + constructor(private readonly configService: ConfigService) { + // Initialize OneSignal client here + this.client = new OneSignal.Client( + this.configService.get('onesignal-config.ONESIGNAL_APP_ID'), + this.configService.get('onesignal-config.ONESIGNAL_API_KEY'), + ); + } + + async sendNotification( + content: string, + title: string, + subscriptionIds: string[], + ): Promise { + const notification = { + contents: { + en: content, + }, + headings: { + en: title, + }, + include_subscription_ids: subscriptionIds, + }; + + try { + const response = await this.client.createNotification(notification); + + return response; + } catch (err) { + console.error('Error:', err); + throw new Error('Error sending notification'); + } + } +} diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts new file mode 100644 index 0000000..cca7fc6 --- /dev/null +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import TuyaWebsocket from '../../config/tuya-web-socket-config'; +import { ConfigService } from '@nestjs/config'; +import { OneSignalService } from './onesignal.service'; +import { DeviceMessagesService } from './device.messages.service'; + +@Injectable() +export class TuyaWebSocketService { + private client: any; // Adjust type according to your TuyaWebsocket client + + constructor( + private readonly configService: ConfigService, + private readonly oneSignalService: OneSignalService, + private readonly deviceMessagesService: DeviceMessagesService, + ) { + // Initialize the TuyaWebsocket client + this.client = new TuyaWebsocket({ + accessId: this.configService.get('tuya-config.TUYA_ACCESS_ID'), + accessKey: this.configService.get('tuya-config.TUYA_ACCESS_KEY'), + url: TuyaWebsocket.URL.EU, + env: TuyaWebsocket.env.TEST, + maxRetryTimes: 100, + }); + + if (this.configService.get('tuya-config.TRUN_ON_TUYA_SOCKET')) { + // Set up event handlers + this.setupEventHandlers(); + + // Start receiving messages + this.client.start(); + } + } + + private setupEventHandlers() { + // Event handlers + this.client.open(() => { + console.log('open'); + }); + + this.client.message(async (ws: WebSocket, message: any) => { + try { + await this.deviceMessagesService.getDevicesUserNotifications( + message.payload.data.bizData.devId, + message.payload.data.bizData, + ); + this.client.ackMessage(message.messageId); + } catch (error) { + console.error('Error processing message:', error); + } + }); + + this.client.reconnect(() => { + console.log('reconnect'); + }); + + this.client.ping(() => { + console.log('ping'); + }); + + this.client.pong(() => { + console.log('pong'); + }); + + this.client.close((ws: WebSocket, ...args: any[]) => { + console.log('close', ...args); + }); + + this.client.error((ws: WebSocket, error: any) => { + console.error('WebSocket error:', error); + }); + } +} diff --git a/libs/common/src/modules/device-notification/device.notification.module.ts b/libs/common/src/modules/device-notification/device.notification.module.ts new file mode 100644 index 0000000..73acce3 --- /dev/null +++ b/libs/common/src/modules/device-notification/device.notification.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeviceNotificationEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([DeviceNotificationEntity])], +}) +export class DeviceNotificationRepositoryModule {} diff --git a/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts b/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts new file mode 100644 index 0000000..0746c14 --- /dev/null +++ b/libs/common/src/modules/device-notification/dtos/device.notification.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceNotificationDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; +} diff --git a/libs/common/src/modules/device-notification/dtos/index.ts b/libs/common/src/modules/device-notification/dtos/index.ts new file mode 100644 index 0000000..d205031 --- /dev/null +++ b/libs/common/src/modules/device-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './device.notification.dto'; diff --git a/libs/common/src/modules/device-notification/entities/device.notification.entity.ts b/libs/common/src/modules/device-notification/entities/device.notification.entity.ts new file mode 100644 index 0000000..84e592a --- /dev/null +++ b/libs/common/src/modules/device-notification/entities/device.notification.entity.ts @@ -0,0 +1,33 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceNotificationDto } from '../dtos'; +import { DeviceEntity } from '../../device/entities'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'device-notification' }) +@Unique(['userUuid', 'deviceUuid']) +export class DeviceNotificationEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public userUuid: string; + + @Column({ + nullable: false, + }) + deviceUuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.permission, { + nullable: false, + }) + device: DeviceEntity; + + @ManyToOne(() => UserEntity, (user) => user.userPermission, { + nullable: false, + }) + user: UserEntity; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/device-notification/entities/index.ts b/libs/common/src/modules/device-notification/entities/index.ts new file mode 100644 index 0000000..fedfc80 --- /dev/null +++ b/libs/common/src/modules/device-notification/entities/index.ts @@ -0,0 +1 @@ +export * from './device.notification.entity'; diff --git a/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts b/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts new file mode 100644 index 0000000..b226791 --- /dev/null +++ b/libs/common/src/modules/device-notification/repositories/device.notification.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceNotificationEntity } from '../entities'; + +@Injectable() +export class DeviceNotificationRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceNotificationEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device-notification/repositories/index.ts b/libs/common/src/modules/device-notification/repositories/index.ts new file mode 100644 index 0000000..1f3da6f --- /dev/null +++ b/libs/common/src/modules/device-notification/repositories/index.ts @@ -0,0 +1 @@ +export * from './device.notification.repository'; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index c6b1859..3389fec 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -5,6 +5,7 @@ import { GroupDeviceEntity } from '../../group-device/entities'; import { SpaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { DeviceUserPermissionEntity } from '../../device-user-permission/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; import { UserEntity } from '../../user/entities'; @Entity({ name: 'device' }) @@ -32,7 +33,14 @@ export class DeviceEntity extends AbstractEntity { }, ) permission: DeviceUserPermissionEntity[]; - + @OneToMany( + () => DeviceNotificationEntity, + (deviceUserNotification) => deviceUserNotification.device, + { + nullable: true, + }, + ) + deviceUserNotification: DeviceNotificationEntity[]; @OneToMany( () => GroupDeviceEntity, (userGroupDevices) => userGroupDevices.device, diff --git a/libs/common/src/modules/user-notification/dtos/index.ts b/libs/common/src/modules/user-notification/dtos/index.ts new file mode 100644 index 0000000..307e6f3 --- /dev/null +++ b/libs/common/src/modules/user-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './user.notification.dto'; diff --git a/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts b/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts new file mode 100644 index 0000000..4e9f72f --- /dev/null +++ b/libs/common/src/modules/user-notification/dtos/user.notification.dto.ts @@ -0,0 +1,19 @@ +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class UserNotificationDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public userUuid: string; + + @IsString() + @IsNotEmpty() + public subscriptionUuid: string; + + @IsBoolean() + @IsNotEmpty() + public active: boolean; +} diff --git a/libs/common/src/modules/user-notification/entities/index.ts b/libs/common/src/modules/user-notification/entities/index.ts new file mode 100644 index 0000000..1acf5c0 --- /dev/null +++ b/libs/common/src/modules/user-notification/entities/index.ts @@ -0,0 +1 @@ +export * from './user.notification.entity'; diff --git a/libs/common/src/modules/user-notification/entities/user.notification.entity.ts b/libs/common/src/modules/user-notification/entities/user.notification.entity.ts new file mode 100644 index 0000000..aa6ec6b --- /dev/null +++ b/libs/common/src/modules/user-notification/entities/user.notification.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { UserNotificationDto } from '../dtos'; +import { UserEntity } from '../../user/entities'; + +@Entity({ name: 'user-notification' }) +@Unique(['user', 'subscriptionUuid']) +export class UserNotificationEntity extends AbstractEntity { + @ManyToOne(() => UserEntity, (user) => user.roles, { + nullable: false, + }) + user: UserEntity; + @Column({ + nullable: false, + }) + subscriptionUuid: string; + + @Column({ + nullable: false, + default: true, + }) + active: boolean; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/user-notification/repositories/index.ts b/libs/common/src/modules/user-notification/repositories/index.ts new file mode 100644 index 0000000..76d3ac2 --- /dev/null +++ b/libs/common/src/modules/user-notification/repositories/index.ts @@ -0,0 +1 @@ +export * from './user.notification.repository'; diff --git a/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts b/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts new file mode 100644 index 0000000..6862e88 --- /dev/null +++ b/libs/common/src/modules/user-notification/repositories/user.notification.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { UserNotificationEntity } from '../entities'; + +@Injectable() +export class UserNotificationRepository extends Repository { + constructor(private dataSource: DataSource) { + super(UserNotificationEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/user-notification/user.notification.repository.module.ts b/libs/common/src/modules/user-notification/user.notification.repository.module.ts new file mode 100644 index 0000000..73997be --- /dev/null +++ b/libs/common/src/modules/user-notification/user.notification.repository.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserNotificationEntity } from './entities'; +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([UserNotificationEntity])], +}) +export class UserNotificationRepositoryModule {} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 9d93671..c432751 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -4,6 +4,8 @@ import { UserDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user-space/entities'; import { UserRoleEntity } from '../../user-role/entities'; +import { DeviceNotificationEntity } from '../../device-notification/entities'; +import { UserNotificationEntity } from '../../user-notification/entities'; import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'user' }) @@ -57,12 +59,21 @@ export class UserEntity extends AbstractEntity { @OneToMany(() => DeviceEntity, (userDevice) => userDevice.user) userDevice: DeviceEntity[]; + @OneToMany( + () => UserNotificationEntity, + (userNotification) => userNotification.user, + ) + userNotification: UserNotificationEntity[]; @OneToMany( () => DeviceUserPermissionEntity, (userPermission) => userPermission.user, ) userPermission: DeviceUserPermissionEntity[]; - + @OneToMany( + () => DeviceNotificationEntity, + (deviceUserNotification) => deviceUserNotification.user, + ) + deviceUserNotification: DeviceNotificationEntity[]; @OneToMany(() => UserRoleEntity, (role) => role.user, { nullable: true, }) diff --git a/package-lock.json b/package-lock.json index 47268ca..7267182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.8", "@tuya/tuya-connector-nodejs": "^2.1.2", "argon2": "^0.40.1", "axios": "^1.6.7", @@ -29,11 +30,13 @@ "ioredis": "^5.3.2", "morgan": "^1.10.0", "nodemailer": "^6.9.10", + "onesignal-node": "^3.4.0", "passport-jwt": "^4.0.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "ws": "^8.17.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", @@ -2060,6 +2063,28 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "10.3.8", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.8.tgz", + "integrity": "sha512-DTSCK+FYtSTljT6XjVUUZhf1cPxKEJf1AG1y2n+ERnd0vzMpnYpMFgGkDlXqa3uC+LAMcOcx1EyTCcHsSHrOVg==", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.6.2" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3086,11 +3111,40 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" + }, "node_modules/axios": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", @@ -3257,6 +3311,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -3296,6 +3358,11 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -3536,6 +3603,11 @@ } ] }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4076,6 +4148,17 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4288,6 +4371,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -4852,6 +4944,11 @@ "node": ">= 0.8" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4866,11 +4963,18 @@ "node": ">=4" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -4897,8 +5001,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -5134,6 +5237,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", @@ -5318,6 +5429,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -5415,6 +5534,47 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5522,6 +5682,20 @@ "node": ">= 0.8" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5802,6 +5976,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5824,6 +6003,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -6611,6 +6795,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6635,6 +6824,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -6647,6 +6841,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6698,6 +6897,20 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -7151,23 +7364,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", - "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7281,6 +7477,14 @@ "node": ">=8" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7289,6 +7493,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -7325,6 +7537,18 @@ "wrappy": "1" } }, + "node_modules/onesignal-node": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/onesignal-node/-/onesignal-node-3.4.0.tgz", + "integrity": "sha512-9dNpfU5Xp6VhJLkdZT4kVqmOaU36RJOgp+6REQHyv+hLOcgqqa4/FRXxuHbjRCE51x9BK4jIC/gn2Mnw0gQgFQ==", + "dependencies": { + "request": "^2.88.2", + "request-promise": "^4.2.6" + }, + "engines": { + "node": ">=8.13.0" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -7599,6 +7823,11 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", "peer": true }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/pg": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", @@ -7914,11 +8143,15 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -8104,6 +8337,99 @@ "node": ">=0.10" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8684,6 +9010,30 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -8718,6 +9068,14 @@ "node": ">= 0.8" } }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9161,6 +9519,18 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9335,6 +9705,22 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9607,7 +9993,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -9673,6 +10058,24 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -9878,6 +10281,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index a8502be..0136b92 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.8", "@tuya/tuya-connector-nodejs": "^2.1.2", "argon2": "^0.40.1", "axios": "^1.6.7", @@ -40,11 +41,13 @@ "ioredis": "^5.3.2", "morgan": "^1.10.0", "nodemailer": "^6.9.10", + "onesignal-node": "^3.4.0", "passport-jwt": "^4.0.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "ws": "^8.17.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", diff --git a/src/app.module.ts b/src/app.module.ts index 6add0be..9a4ef30 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,8 @@ import { FloorModule } from './floor/floor.module'; import { UnitModule } from './unit/unit.module'; import { RoleModule } from './role/role.module'; import { SeederModule } from '@app/common/seed/seeder.module'; +import { UserNotificationModule } from './user-notification/user-notification.module'; +import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -30,7 +32,9 @@ import { SeederModule } from '@app/common/seed/seeder.module'; RoomModule, GroupModule, DeviceModule, + DeviceMessagesSubscriptionModule, UserDevicePermissionModule, + UserNotificationModule, SeederModule, ], controllers: [AuthenticationController], diff --git a/src/device-messages/controllers/device-messages.controller.ts b/src/device-messages/controllers/device-messages.controller.ts new file mode 100644 index 0000000..c8f7834 --- /dev/null +++ b/src/device-messages/controllers/device-messages.controller.ts @@ -0,0 +1,98 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { DeviceMessagesSubscriptionService } from '../services/device-messages.service'; +import { DeviceMessagesAddDto } from '../dtos/device-messages.dto'; + +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Device Messages Status Module') +@Controller({ + version: '1', + path: 'device-messages/subscription', +}) +export class DeviceMessagesSubscriptionController { + constructor( + private readonly deviceMessagesSubscriptionService: DeviceMessagesSubscriptionService, + ) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addDeviceMessagesSubscription( + @Body() deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + const addDetails = + await this.deviceMessagesSubscriptionService.addDeviceMessagesSubscription( + deviceMessagesAddDto, + ); + return { + statusCode: HttpStatus.CREATED, + message: 'Device Messages Subscription Added Successfully', + data: addDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid/user/:userUuid') + async getDeviceMessagesSubscription( + @Param('deviceUuid') deviceUuid: string, + @Param('userUuid') userUuid: string, + ) { + try { + const deviceDetails = + await this.deviceMessagesSubscriptionService.getDeviceMessagesSubscription( + userUuid, + deviceUuid, + ); + return { + statusCode: HttpStatus.OK, + message: 'User Device Subscription fetched Successfully', + data: deviceDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete() + async deleteDeviceMessagesSubscription( + @Body() deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + await this.deviceMessagesSubscriptionService.deleteDeviceMessagesSubscription( + deviceMessagesAddDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User subscription deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/device-messages/controllers/index.ts b/src/device-messages/controllers/index.ts new file mode 100644 index 0000000..01e86df --- /dev/null +++ b/src/device-messages/controllers/index.ts @@ -0,0 +1 @@ +export * from './device-messages.controller'; diff --git a/src/device-messages/device-messages.module.ts b/src/device-messages/device-messages.module.ts new file mode 100644 index 0000000..451f013 --- /dev/null +++ b/src/device-messages/device-messages.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceMessagesSubscriptionController } from './controllers'; +import { DeviceMessagesSubscriptionService } from './services'; +import { DeviceNotificationRepositoryModule } from '@app/common/modules/device-notification/device.notification.module'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; + +@Module({ + imports: [ConfigModule, DeviceNotificationRepositoryModule], + controllers: [DeviceMessagesSubscriptionController], + providers: [DeviceNotificationRepository, DeviceMessagesSubscriptionService], + exports: [DeviceMessagesSubscriptionService], +}) +export class DeviceMessagesSubscriptionModule {} diff --git a/src/device-messages/dtos/device-messages.dto.ts b/src/device-messages/dtos/device-messages.dto.ts new file mode 100644 index 0000000..ae45970 --- /dev/null +++ b/src/device-messages/dtos/device-messages.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeviceMessagesAddDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'device uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + deviceUuid: string; +} diff --git a/src/device-messages/dtos/index.ts b/src/device-messages/dtos/index.ts new file mode 100644 index 0000000..ad01af8 --- /dev/null +++ b/src/device-messages/dtos/index.ts @@ -0,0 +1 @@ +export * from './device-messages.dto'; diff --git a/src/device-messages/services/device-messages.service.ts b/src/device-messages/services/device-messages.service.ts new file mode 100644 index 0000000..73c8182 --- /dev/null +++ b/src/device-messages/services/device-messages.service.ts @@ -0,0 +1,75 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeviceMessagesAddDto } from '../dtos/device-messages.dto'; +import { DeviceNotificationRepository } from '@app/common/modules/device-notification/repositories'; + +@Injectable() +export class DeviceMessagesSubscriptionService { + constructor( + private readonly deviceNotificationRepository: DeviceNotificationRepository, + ) {} + + async addDeviceMessagesSubscription( + deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + return await this.deviceNotificationRepository.save({ + user: { + uuid: deviceMessagesAddDto.userUuid, + }, + device: { + uuid: deviceMessagesAddDto.deviceUuid, + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This User already belongs to this device', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceMessagesSubscription(userUuid: string, deviceUuid: string) { + try { + const deviceUserSubscription = + await this.deviceNotificationRepository.findOne({ + where: { + user: { uuid: userUuid }, + device: { uuid: deviceUuid }, + }, + }); + + return { + uuid: deviceUserSubscription.uuid, + deviceUuid: deviceUserSubscription.deviceUuid, + userUuid: deviceUserSubscription.userUuid, + }; + } catch (error) { + throw new HttpException( + 'User device subscription not found', + HttpStatus.NOT_FOUND, + ); + } + } + async deleteDeviceMessagesSubscription( + deviceMessagesAddDto: DeviceMessagesAddDto, + ) { + try { + const result = await this.deviceNotificationRepository.delete({ + user: { uuid: deviceMessagesAddDto.userUuid }, + device: { uuid: deviceMessagesAddDto.deviceUuid }, + }); + + return result; + } catch (error) { + throw new HttpException( + error.message || 'device not found', + HttpStatus.NOT_FOUND, + ); + } + } +} diff --git a/src/device-messages/services/index.ts b/src/device-messages/services/index.ts new file mode 100644 index 0000000..8420bcb --- /dev/null +++ b/src/device-messages/services/index.ts @@ -0,0 +1 @@ +export * from './device-messages.service'; diff --git a/src/user-notification/controllers/index.ts b/src/user-notification/controllers/index.ts new file mode 100644 index 0000000..dcc9096 --- /dev/null +++ b/src/user-notification/controllers/index.ts @@ -0,0 +1 @@ +export * from './user-notification.controller'; diff --git a/src/user-notification/controllers/user-notification.controller.ts b/src/user-notification/controllers/user-notification.controller.ts new file mode 100644 index 0000000..83fd215 --- /dev/null +++ b/src/user-notification/controllers/user-notification.controller.ts @@ -0,0 +1,94 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { UserNotificationService } from '../services/user-notification.service'; +import { + UserNotificationAddDto, + UserNotificationUpdateDto, +} from '../dtos/user-notification.dto'; + +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('User Notification Module') +@Controller({ + version: '1', + path: 'user-notification/subscription', +}) +export class UserNotificationController { + constructor( + private readonly userNotificationService: UserNotificationService, + ) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addUserSubscription( + @Body() userNotificationAddDto: UserNotificationAddDto, + ) { + try { + const addDetails = await this.userNotificationService.addUserSubscription( + userNotificationAddDto, + ); + return { + statusCode: HttpStatus.CREATED, + message: 'User Notification Added Successfully', + data: addDetails, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':userUuid') + async fetchUserSubscriptions(@Param('userUuid') userUuid: string) { + try { + const userDetails = + await this.userNotificationService.fetchUserSubscriptions(userUuid); + return { + statusCode: HttpStatus.OK, + message: 'User Notification fetched Successfully', + data: { ...userDetails }, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put() + async updateUserSubscription( + @Body() userNotificationUpdateDto: UserNotificationUpdateDto, + ) { + try { + await this.userNotificationService.updateUserSubscription( + userNotificationUpdateDto, + ); + return { + statusCode: HttpStatus.OK, + message: 'User subscription updated Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user-notification/dtos/index.ts b/src/user-notification/dtos/index.ts new file mode 100644 index 0000000..d87c96f --- /dev/null +++ b/src/user-notification/dtos/index.ts @@ -0,0 +1 @@ +export * from './user-notification.dto'; diff --git a/src/user-notification/dtos/user-notification.dto.ts b/src/user-notification/dtos/user-notification.dto.ts new file mode 100644 index 0000000..46b9d7f --- /dev/null +++ b/src/user-notification/dtos/user-notification.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class UserNotificationAddDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'subscription uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + subscriptionUuid: string; +} +export class UserNotificationUpdateDto { + @ApiProperty({ + description: 'user uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; + + @ApiProperty({ + description: 'subscription uuid', + required: true, + }) + @IsString() + @IsNotEmpty() + subscriptionUuid: string; + @ApiProperty({ + description: 'active', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + active: boolean; +} diff --git a/src/user-notification/services/index.ts b/src/user-notification/services/index.ts new file mode 100644 index 0000000..73988b4 --- /dev/null +++ b/src/user-notification/services/index.ts @@ -0,0 +1 @@ +export * from './user-notification.service'; diff --git a/src/user-notification/services/user-notification.service.ts b/src/user-notification/services/user-notification.service.ts new file mode 100644 index 0000000..d8992c0 --- /dev/null +++ b/src/user-notification/services/user-notification.service.ts @@ -0,0 +1,83 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + UserNotificationAddDto, + UserNotificationUpdateDto, +} from '../dtos/user-notification.dto'; +import { UserNotificationRepository } from '@app/common/modules/user-notification/repositories'; + +@Injectable() +export class UserNotificationService { + constructor( + private readonly userNotificationRepository: UserNotificationRepository, + ) {} + + async addUserSubscription(userNotificationAddDto: UserNotificationAddDto) { + try { + return await this.userNotificationRepository.save({ + user: { + uuid: userNotificationAddDto.userUuid, + }, + subscriptionUuid: userNotificationAddDto.subscriptionUuid, + }); + } catch (error) { + if (error.code === '23505') { + throw new HttpException( + 'This User already has this subscription uuid', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async fetchUserSubscriptions(userUuid: string) { + try { + const userNotifications = await this.userNotificationRepository.find({ + where: { + user: { uuid: userUuid }, + active: true, + }, + }); + return { + userUuid, + subscriptionUuids: [ + ...userNotifications.map((sub) => sub.subscriptionUuid), + ], + }; + } catch (error) { + throw new HttpException( + 'User subscription not found', + HttpStatus.NOT_FOUND, + ); + } + } + async updateUserSubscription( + userNotificationUpdateDto: UserNotificationUpdateDto, + ) { + try { + const result = await this.userNotificationRepository.update( + { + user: { uuid: userNotificationUpdateDto.userUuid }, + subscriptionUuid: userNotificationUpdateDto.subscriptionUuid, + }, + { active: userNotificationUpdateDto.active }, + ); + + if (result.affected === 0) { + throw new HttpException( + 'Subscription uuid not found', + HttpStatus.NOT_FOUND, + ); + } + + return result; + } catch (error) { + throw new HttpException( + error.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user-notification/user-notification.module.ts b/src/user-notification/user-notification.module.ts new file mode 100644 index 0000000..0b03df1 --- /dev/null +++ b/src/user-notification/user-notification.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { UserNotificationRepositoryModule } from '@app/common/modules/user-notification/user.notification.repository.module'; +import { UserNotificationRepository } from '@app/common/modules/user-notification/repositories'; +import { UserNotificationService } from 'src/user-notification/services'; +import { UserNotificationController } from 'src/user-notification/controllers'; + +@Module({ + imports: [ConfigModule, UserNotificationRepositoryModule], + controllers: [UserNotificationController], + providers: [UserNotificationRepository, UserNotificationService], + exports: [UserNotificationService], +}) +export class UserNotificationModule {}