diff --git a/package.json b/package.json index 1c9ae8d5..b38e9eed 100755 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", + "@aws-sdk/client-sns": "^3.1049.0", "@aws-sdk/client-textract": "^3.873.0", "@aws-sdk/s3-request-presigner": "^3.828.0", "@elasticemail/elasticemail-client": "^4.0.23", diff --git a/src/controllers/authentication.controller.ts b/src/controllers/authentication.controller.ts index 7f1ef04b..f38a3d19 100755 --- a/src/controllers/authentication.controller.ts +++ b/src/controllers/authentication.controller.ts @@ -1,168 +1,195 @@ -import { Body, Controller, Get, HttpCode, HttpStatus, Logger, Param, ParseIntPipe, Patch, Post, Res, Headers } from '@nestjs/common'; -import { ApiBearerAuth, ApiHeader, ApiTags } from '@nestjs/swagger'; -import { Response } from 'express'; +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Logger, + Param, + ParseIntPipe, + Patch, + Post, + Res, + Headers, +} from "@nestjs/common"; +import { ApiBearerAuth, ApiHeader, ApiTags } from "@nestjs/swagger"; +import { Response } from "express"; import { ActiveUser } from "../decorators/active-user.decorator"; -import { Public } from '../decorators/public.decorator'; +import { Public } from "../decorators/public.decorator"; import { ChangePasswordDto } from "../dtos/change-password.dto"; -import { ConfirmForgotPasswordDto } from '../dtos/confirm-forgot-password.dto'; -import { CreateApiKeyDto } from '../dtos/create-api-key.dto'; -import { UpdateApiKeyDto } from '../dtos/update-api-key.dto'; -import { InitiateForgotPasswordDto } from '../dtos/initiate-forgot-password.dto'; -import { RefreshTokenDto } from '../dtos/refresh-token.dto'; -import { SsoExchangeDto } from '../dtos/sso-exchange.dto'; -import { SignInDto } from '../dtos/sign-in.dto'; -import { RegisterPrivateDto } from '../dtos/register-private.dto'; -import { SignUpDto } from '../dtos/sign-up.dto'; +import { ConfirmForgotPasswordDto } from "../dtos/confirm-forgot-password.dto"; +import { CreateApiKeyDto } from "../dtos/create-api-key.dto"; +import { UpdateApiKeyDto } from "../dtos/update-api-key.dto"; +import { InitiateForgotPasswordDto } from "../dtos/initiate-forgot-password.dto"; +import { RefreshTokenDto } from "../dtos/refresh-token.dto"; +import { SsoExchangeDto } from "../dtos/sso-exchange.dto"; +import { SignInDto } from "../dtos/sign-in.dto"; +import { RegisterPrivateDto } from "../dtos/register-private.dto"; +import { SignUpDto } from "../dtos/sign-up.dto"; import { ActiveUserData } from "../interfaces/active-user-data.interface"; -import { ApiKeyService } from '../services/api-key.service'; -import { AuthenticationService } from '../services/authentication.service'; - +import { ApiKeyService } from "../services/api-key.service"; +import { AuthenticationService } from "../services/authentication.service"; // @Auth(AuthType.None) -@Controller('iam') +@Controller("iam") @ApiTags("Solid Core") // @UseGuards(ThrottlerGuard) // @SkipThrottle({login: true, short: true, burst: true, sustained: true}) // disable all sets by default for this controller export class AuthenticationController { - private readonly logger = new Logger(AuthenticationController.name); - - constructor( - private readonly authService: AuthenticationService, - private readonly apiKeyService: ApiKeyService, - ) { } - - @Public() - // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only - @Post('register') - signUp(@Body() signUpDto: SignUpDto) { - return this.authService.signUp(signUpDto); - } - - @ApiBearerAuth("jwt") - @Post('register-private') - signUpPrivate(@Body() signUpDto: RegisterPrivateDto, @ActiveUser() activeUser: ActiveUserData) { - return this.authService.signUp(signUpDto, activeUser); - } - - @Public() - // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only - @HttpCode(HttpStatus.OK) // by default @Post does 201, we wanted 200 - hence using @HttpCode(HttpStatus.OK) - @Post('authenticate') - async signIn( - @Res({ passthrough: true }) response: Response, - @Body() signInDto: SignInDto - ) { - // This means that we are passing the token back in plain text. - // This is less secure. - // console.log("signInDto in Signin Controller", signInDto); - - return this.authService.signIn(signInDto); - - // This means we are setting the token as a http only cookie. - // const accessToken = await this.authService.signIn(signInDto); - // response.cookie('accessToken', accessToken, { - // secure: true, - // httpOnly: true, - // sameSite: true, - // }); - } - - @Public() - // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only - @HttpCode(HttpStatus.OK) // changed since the default is 201 - @Post('refresh-tokens') - refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) { - return this.authService.refreshTokens(refreshTokenDto); - } - - @Public() - // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only - @Post('initiate/forgot-password') - initiateForgotPassword(@Body() initiateForgotPasswordDto: InitiateForgotPasswordDto) { - return this.authService.initiateForgotPassword(initiateForgotPasswordDto); - } - - @Public() - // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only - @Post('confirm/forgot-password') - confirmForgotPassword(@Body() confirmForgotPasswordDto: ConfirmForgotPasswordDto) { - return this.authService.confirmForgotPassword(confirmForgotPasswordDto); - } - - @ApiBearerAuth("jwt") - @Post('change-password') - changePassword(@Body() changePasswordDto: ChangePasswordDto, @ActiveUser() activeUser: ActiveUserData) { - return this.authService.changePassword(changePasswordDto, activeUser); - } - - @ApiBearerAuth("jwt") - @Get('me') - me( - @ActiveUser() activeUser: ActiveUserData - ) { - return this.authService.me(activeUser); - } - - @ApiBearerAuth("jwt") - @Post('logout') - @Public() - @HttpCode(HttpStatus.OK) - async logout(@Body('refreshToken') refreshToken: string) { - return this.authService.logout(refreshToken); - } - - @ApiBearerAuth("jwt") - @Post('api-keys') - @HttpCode(HttpStatus.CREATED) - generateApiKey( - @Body() dto: CreateApiKeyDto, - @ActiveUser() activeUser: ActiveUserData, - ) { - return this.apiKeyService.generate(activeUser.sub, dto); - } - - @ApiBearerAuth("jwt") - @Post('api-keys/users/:userId') - @HttpCode(HttpStatus.CREATED) - generateApiKeyForUser( - @Param('userId', ParseIntPipe) userId: number, - @Body() dto: CreateApiKeyDto, - ) { - return this.apiKeyService.generate(userId, dto); - } - - @ApiBearerAuth("jwt") - @Patch('api-keys/:id') - @HttpCode(HttpStatus.OK) - updateApiKey( - @Param('id', ParseIntPipe) id: number, - @Body() dto: UpdateApiKeyDto, - @ActiveUser() activeUser: ActiveUserData, - ) { - return this.apiKeyService.updateKey(id, activeUser.sub, dto); - } - - @Public() - @ApiHeader({ name: 'solidx-api-key', required: true, description: 'API key for authenticating the request' }) - @Get('api-keys/me') - async apiKeyMe(@Headers('solidx-api-key') apiKey: string) { - return this.apiKeyService.apiKeyMe(apiKey); - } - - @Post('sso/code') - @HttpCode(HttpStatus.OK) - generateSsoCode( - @ActiveUser() activeUser: ActiveUserData, - @Headers('authorization') authorization: string, - ) { - const rawAccessToken = authorization?.replace(/^Bearer\s+/i, ''); - return this.authService.generateSsoCode(activeUser, rawAccessToken); - } - - @Public() - @Post('sso/exchange') - @HttpCode(HttpStatus.OK) - exchangeSsoCode(@Body() ssoExchangeDto: SsoExchangeDto) { - return this.authService.exchangeSsoCode(ssoExchangeDto.code); - } + private readonly logger = new Logger(AuthenticationController.name); + + constructor( + private readonly authService: AuthenticationService, + private readonly apiKeyService: ApiKeyService, + ) {} + + @Public() + // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only + @Post("register") + signUp(@Body() signUpDto: SignUpDto) { + return this.authService.signUp(signUpDto); + } + + @ApiBearerAuth("jwt") + @Post("register-private") + signUpPrivate( + @Body() signUpDto: RegisterPrivateDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.authService.signUp(signUpDto, activeUser); + } + + @Public() + // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only + @HttpCode(HttpStatus.OK) // by default @Post does 201, we wanted 200 - hence using @HttpCode(HttpStatus.OK) + @Post("authenticate") + async signIn( + @Res({ passthrough: true }) response: Response, + @Body() signInDto: SignInDto, + ) { + // This means that we are passing the token back in plain text. + // This is less secure. + // console.log("signInDto in Signin Controller", signInDto); + + return this.authService.signIn(signInDto); + + // This means we are setting the token as a http only cookie. + // const accessToken = await this.authService.signIn(signInDto); + // response.cookie('accessToken', accessToken, { + // secure: true, + // httpOnly: true, + // sameSite: true, + // }); + } + + @Public() + // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only + @HttpCode(HttpStatus.OK) // changed since the default is 201 + @Post("refresh-tokens") + refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) { + return this.authService.refreshTokens(refreshTokenDto); + } + + @Public() + // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only + @Post("initiate/forgot-password") + initiateForgotPassword( + @Body() initiateForgotPasswordDto: InitiateForgotPasswordDto, + ) { + return this.authService.initiateForgotPassword(initiateForgotPasswordDto); + } + + @Public() + // @SkipThrottle({ login: false, short: true, burst: true, sustained: true }) //Enable the login throttle only + @Post("confirm/forgot-password") + confirmForgotPassword( + @Body() confirmForgotPasswordDto: ConfirmForgotPasswordDto, + ) { + return this.authService.confirmForgotPassword(confirmForgotPasswordDto); + } + + @ApiBearerAuth("jwt") + @Post("change-password") + changePassword( + @Body() changePasswordDto: ChangePasswordDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.authService.changePassword(changePasswordDto, activeUser); + } + + @ApiBearerAuth("jwt") + @Get("me") + me(@ActiveUser() activeUser: ActiveUserData) { + return this.authService.me(activeUser); + } + + @ApiBearerAuth("jwt") + @Post("logout") + @Public() + @HttpCode(HttpStatus.OK) + async logout( + @Body("refreshToken") refreshToken: string, + @Body("deviceId") deviceId?: string, + ) { + return this.authService.logout(refreshToken, deviceId); + } + + @ApiBearerAuth("jwt") + @Post("api-keys") + @HttpCode(HttpStatus.CREATED) + generateApiKey( + @Body() dto: CreateApiKeyDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.apiKeyService.generate(activeUser.sub, dto); + } + + @ApiBearerAuth("jwt") + @Post("api-keys/users/:userId") + @HttpCode(HttpStatus.CREATED) + generateApiKeyForUser( + @Param("userId", ParseIntPipe) userId: number, + @Body() dto: CreateApiKeyDto, + ) { + return this.apiKeyService.generate(userId, dto); + } + + @ApiBearerAuth("jwt") + @Patch("api-keys/:id") + @HttpCode(HttpStatus.OK) + updateApiKey( + @Param("id", ParseIntPipe) id: number, + @Body() dto: UpdateApiKeyDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.apiKeyService.updateKey(id, activeUser.sub, dto); + } + + @Public() + @ApiHeader({ + name: "solidx-api-key", + required: true, + description: "API key for authenticating the request", + }) + @Get("api-keys/me") + async apiKeyMe(@Headers("solidx-api-key") apiKey: string) { + return this.apiKeyService.apiKeyMe(apiKey); + } + + @Post("sso/code") + @HttpCode(HttpStatus.OK) + generateSsoCode( + @ActiveUser() activeUser: ActiveUserData, + @Headers("authorization") authorization: string, + ) { + const rawAccessToken = authorization?.replace(/^Bearer\s+/i, ""); + return this.authService.generateSsoCode(activeUser, rawAccessToken); + } + + @Public() + @Post("sso/exchange") + @HttpCode(HttpStatus.OK) + exchangeSsoCode(@Body() ssoExchangeDto: SsoExchangeDto) { + return this.authService.exchangeSsoCode(ssoExchangeDto.code); + } } diff --git a/src/controllers/push-notification-template.controller.ts b/src/controllers/push-notification-template.controller.ts new file mode 100644 index 00000000..41cf70a9 --- /dev/null +++ b/src/controllers/push-notification-template.controller.ts @@ -0,0 +1,88 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UploadedFiles, + UseInterceptors, +} from "@nestjs/common"; +import { AnyFilesInterceptor } from "@nestjs/platform-express"; +import { ApiBearerAuth, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { CreatePushNotificationTemplateDto } from "src/dtos/create-push-notification-template.dto"; +import { UpdatePushNotificationTemplateDto } from "src/dtos/update-push-notification-template.dto"; +import { PushNotificationTemplateService } from "src/services/push-notification-template.service"; + +@Controller("push-notification-template") +@ApiTags("Solid Core") +export class PushNotificationTemplateController { + constructor(private readonly service: PushNotificationTemplateService) {} + + @ApiBearerAuth("jwt") + @Post() + @UseInterceptors(AnyFilesInterceptor()) + create( + @Body() createDto: CreatePushNotificationTemplateDto, + @UploadedFiles() files: Array, + ) { + return this.service.create(createDto, files); + } + + @ApiBearerAuth("jwt") + @Post("/bulk") + @UseInterceptors(AnyFilesInterceptor()) + insertMany( + @Body() createDtos: CreatePushNotificationTemplateDto[], + @UploadedFiles() filesArray: Express.Multer.File[][] = [], + ) { + return this.service.insertMany(createDtos, filesArray); + } + + @ApiBearerAuth("jwt") + @Put(":id") + @UseInterceptors(AnyFilesInterceptor()) + update( + @Param("id") id: number, + @Body() updateDto: UpdatePushNotificationTemplateDto, + @UploadedFiles() files: Array, + ) { + return this.service.update(id, updateDto, files); + } + + @ApiBearerAuth("jwt") + @ApiQuery({ name: "showSoftDeleted", required: false, type: Boolean }) + @ApiQuery({ name: "showOnlySoftDeleted", required: false, type: Boolean }) + @ApiQuery({ name: "limit", required: false, type: Number }) + @ApiQuery({ name: "offset", required: false, type: Number }) + @ApiQuery({ name: "fields", required: false, type: Array }) + @ApiQuery({ name: "sort", required: false, type: Array }) + @ApiQuery({ name: "groupBy", required: false, type: Array }) + @ApiQuery({ name: "populate", required: false, type: Array }) + @ApiQuery({ name: "populateMedia", required: false, type: Array }) + @ApiQuery({ name: "filters", required: false, type: Array }) + @Get() + async findMany(@Query() query: any) { + return this.service.find(query); + } + + @ApiBearerAuth("jwt") + @Get(":id") + async findOne(@Param("id") id: string, @Query() query: any) { + return this.service.findOne(+id, query); + } + + @ApiBearerAuth("jwt") + @Delete("/bulk") + async deleteMany(@Body() ids: number[]) { + return this.service.deleteMany(ids); + } + + @ApiBearerAuth("jwt") + @Delete(":id") + async delete(@Param("id") id: number) { + return this.service.delete(id); + } +} diff --git a/src/decorators/push-notification-provider.decorator.ts b/src/decorators/push-notification-provider.decorator.ts new file mode 100644 index 00000000..a4a6bb68 --- /dev/null +++ b/src/decorators/push-notification-provider.decorator.ts @@ -0,0 +1,7 @@ +export const IS_PUSH_NOTIFICATION_PROVIDER = "IS_PUSH_NOTIFICATION_PROVIDER"; + +export const PushNotificationProvider = () => { + return (target: Function) => { + Reflect.defineMetadata(IS_PUSH_NOTIFICATION_PROVIDER, true, target); + }; +}; diff --git a/src/dtos/create-push-notification-template.dto.ts b/src/dtos/create-push-notification-template.dto.ts new file mode 100644 index 00000000..01431d73 --- /dev/null +++ b/src/dtos/create-push-notification-template.dto.ts @@ -0,0 +1,52 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + IsBoolean, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + Matches, +} from "class-validator"; + +export class CreatePushNotificationTemplateDto { + @IsNotEmpty() + @Matches(/[a-z]+(-[a-z]+)*/) + @IsString() + @ApiProperty() + name: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + displayName: string; + + @IsOptional() + @IsString() + @ApiProperty() + title: string; + + @IsOptional() + @IsString() + @ApiProperty() + body: string; + + @IsOptional() + @IsObject() + @ApiProperty() + dataTemplate?: Record; + + @IsOptional() + @IsString() + @ApiProperty() + description?: string; + + @IsOptional() + @IsBoolean() + @ApiProperty() + active: boolean = true; + + @IsOptional() + @IsString() + @ApiProperty() + type?: string; +} diff --git a/src/dtos/device-metadata.dto.ts b/src/dtos/device-metadata.dto.ts new file mode 100644 index 00000000..f6e11fc8 --- /dev/null +++ b/src/dtos/device-metadata.dto.ts @@ -0,0 +1,38 @@ +import { IsOptional, IsString } from "class-validator"; + +export class DeviceMetadataDto { + @IsOptional() + @IsString() + deviceId?: string; + + @IsOptional() + userId?: number; + + @IsOptional() + @IsString() + deviceToken?: string; + + @IsOptional() + @IsString() + platform?: string; + + @IsOptional() + @IsString() + deviceName?: string; + + @IsOptional() + @IsString() + deviceType?: string; + + @IsOptional() + @IsString() + osName?: string; + + @IsOptional() + @IsString() + osVersion?: string; + + @IsOptional() + @IsString() + appVersion?: string; +} diff --git a/src/dtos/oauth-user-dto.ts b/src/dtos/oauth-user-dto.ts index 264424c0..a2578b57 100755 --- a/src/dtos/oauth-user-dto.ts +++ b/src/dtos/oauth-user-dto.ts @@ -1,5 +1,6 @@ +import { DeviceMetadataDto } from "./device-metadata.dto"; -export class OauthUserDto { +export class OauthUserDto extends DeviceMetadataDto { provider: string; providerId: string; email: string; diff --git a/src/dtos/otp-confirm-otp.dto.ts b/src/dtos/otp-confirm-otp.dto.ts index fa12987c..e8268949 100755 --- a/src/dtos/otp-confirm-otp.dto.ts +++ b/src/dtos/otp-confirm-otp.dto.ts @@ -1,11 +1,12 @@ import { IsEmail, IsEnum, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { DeviceMetadataDto } from './device-metadata.dto'; export enum SignInType { email = 'email', mobile = 'mobile', } -export class OTPConfirmOTPDto { +export class OTPConfirmOTPDto extends DeviceMetadataDto { @IsEnum(SignInType) @IsNotEmpty() type: string; @@ -15,4 +16,4 @@ export class OTPConfirmOTPDto { @IsNotEmpty() otp: string; -} \ No newline at end of file +} diff --git a/src/dtos/otp-sign-in.dto.ts b/src/dtos/otp-sign-in.dto.ts index 8c0efe75..464c7696 100755 --- a/src/dtos/otp-sign-in.dto.ts +++ b/src/dtos/otp-sign-in.dto.ts @@ -1,11 +1,12 @@ import { IsEnum, IsNotEmpty, IsOptional } from "class-validator"; import { SignInType } from "./otp-confirm-otp.dto"; +import { DeviceMetadataDto } from "./device-metadata.dto"; -export class OTPSignInDto { +export class OTPSignInDto extends DeviceMetadataDto { @IsEnum(SignInType) @IsOptional() type?: string; @IsNotEmpty() identifier: string; -} \ No newline at end of file +} diff --git a/src/dtos/otp-sign-up.dto.ts b/src/dtos/otp-sign-up.dto.ts index 8e16b8ac..8ae28076 100755 --- a/src/dtos/otp-sign-up.dto.ts +++ b/src/dtos/otp-sign-up.dto.ts @@ -1,23 +1,30 @@ -import { IsEmail, IsEnum, IsJSON, IsNotEmpty, IsOptional } from 'class-validator'; +import { + IsEmail, + IsEnum, + IsJSON, + IsNotEmpty, + IsOptional, +} from "class-validator"; import { PasswordlessRegistrationValidateWhatSources } from "../constants"; +import { DeviceMetadataDto } from "./device-metadata.dto"; -export class OTPSignUpDto { - @IsNotEmpty() - username: string; +export class OTPSignUpDto extends DeviceMetadataDto { + @IsNotEmpty() + username: string; - @IsOptional() - @IsEmail() - email: string; + @IsOptional() + @IsEmail() + email: string; - @IsOptional() - @IsNotEmpty() - mobile: string; + @IsOptional() + @IsNotEmpty() + mobile: string; - @IsOptional() - @IsEnum(PasswordlessRegistrationValidateWhatSources, { each: true }) - validationSources: PasswordlessRegistrationValidateWhatSources[] = []; + @IsOptional() + @IsEnum(PasswordlessRegistrationValidateWhatSources, { each: true }) + validationSources: PasswordlessRegistrationValidateWhatSources[] = []; - @IsOptional() - customPayload: any; -} \ No newline at end of file + @IsOptional() + customPayload: any; +} diff --git a/src/dtos/sign-in.dto.ts b/src/dtos/sign-in.dto.ts index 460ec890..ed42ffb3 100755 --- a/src/dtos/sign-in.dto.ts +++ b/src/dtos/sign-in.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; +import { DeviceMetadataDto } from './device-metadata.dto'; -export class SignInDto { +export class SignInDto extends DeviceMetadataDto { @ApiProperty({ default: 'sa@solidxai.com' }) @IsEmail() diff --git a/src/dtos/update-push-notification-template.dto.ts b/src/dtos/update-push-notification-template.dto.ts new file mode 100644 index 00000000..d90b1ca8 --- /dev/null +++ b/src/dtos/update-push-notification-template.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty, PartialType } from "@nestjs/swagger"; +import { CreatePushNotificationTemplateDto } from "./create-push-notification-template.dto"; +import { + IsBoolean, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + Matches, +} from "class-validator"; + +export class UpdatePushNotificationTemplateDto extends PartialType(CreatePushNotificationTemplateDto) { + @IsNotEmpty() + @IsOptional() + @Matches(/[a-z]+(-[a-z]+)*/) + @IsString() + @ApiProperty() + name: string; + + @IsOptional() + @IsString() + @ApiProperty() + displayName: string; + + @IsOptional() + @IsString() + @ApiProperty() + title: string; + + @IsOptional() + @IsString() + @ApiProperty() + body: string; + + @IsOptional() + @IsObject() + @ApiProperty() + dataTemplate?: Record; + + @IsOptional() + @IsString() + @ApiProperty() + description?: string; + + @IsOptional() + @IsBoolean() + @ApiProperty() + active: boolean; + + @IsOptional() + @IsString() + @ApiProperty() + type?: string; +} diff --git a/src/dtos/update-user.dto.ts b/src/dtos/update-user.dto.ts index ed976074..b55c4f52 100755 --- a/src/dtos/update-user.dto.ts +++ b/src/dtos/update-user.dto.ts @@ -1,8 +1,18 @@ -import { IsInt, IsOptional, IsString, IsNotEmpty, Matches, IsBoolean, IsDate, ValidateNested, IsArray } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { UpdateRoleMetadataDto } from 'src/dtos/update-role-metadata.dto'; -import { UpdateUserViewMetadataDto } from 'src/dtos/update-user-view-metadata.dto'; +import { + IsInt, + IsOptional, + IsString, + IsNotEmpty, + Matches, + IsBoolean, + IsDate, + ValidateNested, + IsArray, +} from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { UpdateRoleMetadataDto } from "src/dtos/update-role-metadata.dto"; +import { UpdateUserViewMetadataDto } from "src/dtos/update-user-view-metadata.dto"; export class UpdateUserDto { @IsOptional() @@ -165,4 +175,4 @@ export class UpdateUserDto { @IsInt() @ApiProperty() failedLoginAttempts: number; -} \ No newline at end of file +} diff --git a/src/entities/push-notification-template.entity.ts b/src/entities/push-notification-template.entity.ts new file mode 100644 index 00000000..c6b275b8 --- /dev/null +++ b/src/entities/push-notification-template.entity.ts @@ -0,0 +1,31 @@ +import { Column, Entity, Index } from "typeorm"; +import { CommonEntity } from "src/entities/common.entity"; +import { getColumnType } from "src/helpers/typeorm-db-helper"; + +@Entity("ss_push_template") +export class PushNotificationTemplate extends CommonEntity { + @Index({ unique: true }) + @Column({ name: "name", type: "varchar" }) + name: string; + + @Column({ name: "display_name", type: "varchar" }) + displayName: string; + + @Column({ name: "title", type: "varchar", default: "" }) + title: string; + + @Column({ name: "body", ...getColumnType("longText"), default: "" }) + body: string; + + @Column({ name: "data_template", type: "simple-json", nullable: true }) + dataTemplate?: Record; + + @Column({ name: "description", nullable: true }) + description?: string; + + @Column({ name: "active", nullable: true, default: true }) + active: boolean = true; + + @Column({ name: "type", type: "varchar", nullable: true }) + type?: string; +} diff --git a/src/entities/user-device-metadata.entity.ts b/src/entities/user-device-metadata.entity.ts new file mode 100644 index 00000000..4128bacd --- /dev/null +++ b/src/entities/user-device-metadata.entity.ts @@ -0,0 +1,52 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; + +import { CommonEntity } from "./common.entity"; +import { User } from "./user.entity"; + +@Entity("ss_user_device_metadata") +@Index("ss_user_device_metadata_user_device", ["user", "deviceId"], { + unique: true, +}) +@Index("ss_user_device_metadata_push_endpoint_arn", ["pushEndpointArn"]) +@Index("ss_user_device_metadata_user_active", ["user", "isActive"]) +export class UserDeviceMetadata extends CommonEntity { + @ManyToOne(() => User, { onDelete: "CASCADE" }) + @JoinColumn() + user: User; + + @Column({ name: "device_id" }) + deviceId: string; + + @Column({ nullable: true }) + pushDeviceToken?: string; + + @Column({ nullable: true }) + pushEndpointArn?: string; + + @Column({ default: "unknown" }) + platform: string; + + @Column({ nullable: true }) + osName?: string; + + @Column({ nullable: true }) + osVersion?: string; + + @Column({ nullable: true }) + appVersion?: string; + + @Column({ nullable: true }) + deviceName?: string; + + @Column({ nullable: true }) + deviceType?: string; + + @Column({ default: true }) + isActive: boolean; + + @Column({ type: "timestamp", nullable: true }) + pushTokenUpdatedAt?: Date; + + @Column({ type: "timestamp", nullable: true }) + lastActiveAt?: Date; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 67478f1d..69a6afc3 100755 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,60 +1,71 @@ -import { CommonEntity } from "src/entities/common.entity" -import { Entity, Column, Index, JoinTable, ManyToMany, OneToMany, TableInheritance } from "typeorm"; -import { RoleMetadata } from 'src/entities/role-metadata.entity'; -import { UserViewMetadata } from 'src/entities/user-view-metadata.entity' -import { UserApiKey } from 'src/entities/user-api-key.entity' +import { CommonEntity } from "src/entities/common.entity"; +import { + Entity, + Column, + Index, + JoinTable, + ManyToMany, + OneToMany, + TableInheritance, +} from "typeorm"; +import { RoleMetadata } from "src/entities/role-metadata.entity"; +import { UserViewMetadata } from "src/entities/user-view-metadata.entity"; +import { UserApiKey } from "src/entities/user-api-key.entity"; import { Exclude, Expose } from "class-transformer"; +import { UserDeviceMetadata } from "./user-device-metadata.entity"; @Entity("ss_user") -@TableInheritance({ column: { type: "varchar", name: "type", default: "User" } }) +@TableInheritance({ + column: { type: "varchar", name: "type", default: "User" }, +}) @Exclude() export class User extends CommonEntity { - @Column({ type: "varchar", nullable: true }) - @Expose() - fullName: string; + @Column({ type: "varchar", nullable: true }) + @Expose() + fullName: string; - @Index({ unique: true }) - @Column({ type: "varchar" }) - @Expose() - username: string; + @Index({ unique: true }) + @Column({ type: "varchar" }) + @Expose() + username: string; - @Index() - @Column({ type: "varchar", nullable: true }) - @Expose() - email: string; + @Index() + @Column({ type: "varchar", nullable: true }) + @Expose() + email: string; - @Index() - @Column({ type: "varchar", nullable: true }) - @Expose() - mobile: string; + @Index() + @Column({ type: "varchar", nullable: true }) + @Expose() + mobile: string; - @Column({ type: "varchar", nullable: true }) - // don't send to client - password: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client + password: string; - @Column({ nullable: true, default: true }) - @Expose() - forcePasswordChange: boolean = true; + @Column({ nullable: true, default: true }) + @Expose() + forcePasswordChange: boolean = true; - @Column({ type: "varchar", default: "local" }) - // don't send to client - lastLoginProvider: string = "local"; + @Column({ type: "varchar", default: "local" }) + // don't send to client + lastLoginProvider: string = "local"; - @Column({ type: "varchar", nullable: true }) - // don't send to client (test) - accessCode: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client (test) + accessCode: string; - @Column({ type: "varchar", nullable: true }) - // don't send to client - googleAccessToken: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client + googleAccessToken: string; - @Column({ type: "varchar", nullable: true }) - // don't send to client - googleId: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client + googleId: string; - @Column({ type: "varchar", nullable: true }) - // don't send to client - googleProfilePicture: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client + googleProfilePicture: string; @Column({ type: "varchar", nullable: true }) // don't send to client @@ -191,5 +202,7 @@ export class User extends CommonEntity { @OneToMany(() => UserApiKey, key => key.user) @Expose() apiKeys: UserApiKey[]; - -} \ No newline at end of file + + @OneToMany(() => UserDeviceMetadata, (device) => device.user) + devices?: UserDeviceMetadata[]; +} diff --git a/src/factories/push-notification.factory.ts b/src/factories/push-notification.factory.ts new file mode 100644 index 00000000..627a1f86 --- /dev/null +++ b/src/factories/push-notification.factory.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { ModuleRef } from "@nestjs/core"; +import { SolidRegistry } from "src/helpers/solid-registry"; +import { IPushNotification } from "src/interfaces"; +import { SettingService } from "src/services/setting.service"; +import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; + +@Injectable() +export class PushNotificationFactory { + private readonly logger = new Logger(this.constructor.name); + + constructor( + private readonly moduleRef: ModuleRef, + private readonly solidRegistry: SolidRegistry, + private readonly settingService: SettingService, + ) {} + + getPushNotificationService(name: string = null): IPushNotification { + const pushNotificationServiceName = + name || + this.settingService.getConfigValue( + "pushNotificationProvider", + ); + const pushNotificationProviders = + this.solidRegistry.getPushNotificationProviders(); + + if (!pushNotificationProviders.length) { + this.logger.error("No push notification providers are registered."); + } + + const pushNotificationProvider = pushNotificationProviders.find( + (provider) => provider.name === pushNotificationServiceName, + ); + + return pushNotificationProvider.instance as IPushNotification; + } +} diff --git a/src/helpers/solid-registry.ts b/src/helpers/solid-registry.ts index ee65f982..afbb144a 100755 --- a/src/helpers/solid-registry.ts +++ b/src/helpers/solid-registry.ts @@ -1,14 +1,25 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; -import { IExtensionUserCreationProvider } from 'src/interfaces'; -import { User } from 'src/entities/user.entity'; -import { ComputedFieldTriggerConfig, ComputedFieldValueType } from 'src/dtos/create-field-metadata.dto'; -import { CommonEntity } from 'src/entities/common.entity'; -import { Locale } from 'src/entities/locale.entity'; -import { SecurityRule } from 'src/entities/security-rule.entity'; -import { IScheduledJob } from 'src/services/scheduled-jobs/scheduled-job.interface'; -import { IDashboardQuestionDataProvider, IDashboardVariableSelectionProvider, IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces"; -import { ObjectLiteral } from 'typeorm'; +import { Injectable, Logger } from "@nestjs/common"; +import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper"; +import { IExtensionUserCreationProvider } from "src/interfaces"; +import { User } from "src/entities/user.entity"; +import { + ComputedFieldTriggerConfig, + ComputedFieldValueType, +} from "src/dtos/create-field-metadata.dto"; +import { CommonEntity } from "src/entities/common.entity"; +import { Locale } from "src/entities/locale.entity"; +import { SecurityRule } from "src/entities/security-rule.entity"; +import { IScheduledJob } from "src/services/scheduled-jobs/scheduled-job.interface"; +import { + IDashboardQuestionDataProvider, + IDashboardVariableSelectionProvider, + IErrorCodeProvider, + ISecurityRuleConfigProvider, + ISelectionProvider, + ISelectionProviderContext, + ISolidDatabaseModule, +} from "../interfaces"; +import { ObjectLiteral } from "typeorm"; type ControllerMetadata = { name: string; @@ -38,7 +49,7 @@ export enum RESERVED_SOLID_KEYWORDS { smsTemplate = "smsTemplate", userMetadata = "userMetadata", user = "user", - locale = "locale" + locale = "locale", } export interface TypeOrmEventContext { @@ -83,6 +94,7 @@ export class SolidRegistry { private mailProviders: Set = new Set(); private whatsappProviders: Set = new Set(); private smsProviders: Set = new Set(); + private pushNotificationProviders: Set = new Set(); private securityRuleConfigProviders: Set = new Set(); private errorCodeProviders: Set = new Set(); private settingsProviders: Set = new Set(); @@ -98,8 +110,13 @@ export class SolidRegistry { this.extensionUserCreationProvider = provider; } - getExtensionUserCreationProvider(): IExtensionUserCreationProvider | null { - return (this.extensionUserCreationProvider?.instance as IExtensionUserCreationProvider) ?? null; + getExtensionUserCreationProvider< + T extends User = User, + >(): IExtensionUserCreationProvider | null { + return ( + (this.extensionUserCreationProvider + ?.instance as IExtensionUserCreationProvider) ?? null + ); } registerErrorCodeProvider(errorCodeProvider: InstanceWrapper): void { @@ -114,7 +131,9 @@ export class SolidRegistry { this.smsProviders.add(smsProvider); } - registerSecurityRuleConfigProvider(securityRuleConfigProvider: InstanceWrapper): void { + registerSecurityRuleConfigProvider( + securityRuleConfigProvider: InstanceWrapper, + ): void { this.securityRuleConfigProviders.add(securityRuleConfigProvider); } @@ -122,6 +141,12 @@ export class SolidRegistry { this.mailProviders.add(mailProvider); } + registerPushNotificationProvider( + pushNotificationProvider: InstanceWrapper, + ): void { + this.pushNotificationProviders.add(pushNotificationProvider); + } + registerController(name: string, methodNames: string[]): void { this.controllers.add({ name: name, methods: methodNames }); } @@ -138,11 +163,15 @@ export class SolidRegistry { this.selectionProviders.add(selectionProvider); } - registerDashboardVariableSelectionProvider(dashboardSelectionProvider: InstanceWrapper): void { + registerDashboardVariableSelectionProvider( + dashboardSelectionProvider: InstanceWrapper, + ): void { this.dashboardVariableSelectionProviders.add(dashboardSelectionProvider); } - registerDashboardQuestionDataProvider(dashboardQuestionDataProvider: InstanceWrapper): void { + registerDashboardQuestionDataProvider( + dashboardQuestionDataProvider: InstanceWrapper, + ): void { this.dashboardQuestionDataProviders.add(dashboardQuestionDataProvider); } @@ -174,7 +203,9 @@ export class SolidRegistry { this.locales = locales; } - registerComputedFieldMetadata(computedFieldMetadata: ComputedFieldMetadata[]) { + registerComputedFieldMetadata( + computedFieldMetadata: ComputedFieldMetadata[], + ) { this.computedFieldMetadata = computedFieldMetadata; } @@ -182,7 +213,9 @@ export class SolidRegistry { return Array.from(this.settingsProviders); } - getSettingsProviderInstance(name: string): ISelectionProvider { + getSettingsProviderInstance( + name: string, + ): ISelectionProvider { const settingsProviders = this.getSettingsProviders(); for (let i = 0; i < settingsProviders.length; i++) { @@ -205,11 +238,17 @@ export class SolidRegistry { return Array.from(this.smsProviders); } + getPushNotificationProviders(): Array { + return Array.from(this.pushNotificationProviders); + } + getSecurityRuleConfigProviders(): Array { return Array.from(this.securityRuleConfigProviders); } - getSecurityRuleConfigProviderInstance(name: string): ISecurityRuleConfigProvider { + getSecurityRuleConfigProviderInstance( + name: string, + ): ISecurityRuleConfigProvider { const securityRuleConfigProviders = this.getSecurityRuleConfigProviders(); for (let i = 0; i < securityRuleConfigProviders.length; i++) { @@ -232,7 +271,9 @@ export class SolidRegistry { return Array.from(this.selectionProviders); } - getSelectionProviderInstance(name: string): ISelectionProvider { + getSelectionProviderInstance( + name: string, + ): ISelectionProvider { const selectionProviders = this.getSelectionProviders(); for (let i = 0; i < selectionProviders.length; i++) { @@ -247,8 +288,11 @@ export class SolidRegistry { return Array.from(this.dashboardVariableSelectionProviders); } - getDashboardVariableSelectionProviderInstance(name: string): IDashboardVariableSelectionProvider { - const dashboardSelectionProviders = this.getDashboardVariableSelectionProviders(); + getDashboardVariableSelectionProviderInstance< + T extends ISelectionProviderContext, + >(name: string): IDashboardVariableSelectionProvider { + const dashboardSelectionProviders = + this.getDashboardVariableSelectionProviders(); for (let i = 0; i < dashboardSelectionProviders.length; i++) { const dashboardSelectionProvider = dashboardSelectionProviders[i]; @@ -266,17 +310,21 @@ export class SolidRegistry { const providers = this.getErrorCodeProviders(); for (let i = 0; i < providers.length; i++) { const p = providers[i]; - if (p.instance?.name?.() === name) return p.instance as IErrorCodeProvider; + if (p.instance?.name?.() === name) + return p.instance as IErrorCodeProvider; } return undefined; } getDashboardQuestionDataProviders(): Array { - return Array.from(this.dashboardQuestionDataProviders) + return Array.from(this.dashboardQuestionDataProviders); } - getDashboardQuestionDataProviderInstance(name: string): IDashboardQuestionDataProvider { - const dashboardQuestionDataProviders = this.getDashboardQuestionDataProviders(); + getDashboardQuestionDataProviderInstance( + name: string, + ): IDashboardQuestionDataProvider { + const dashboardQuestionDataProviders = + this.getDashboardQuestionDataProviders(); for (let i = 0; i < dashboardQuestionDataProviders.length; i++) { const dasbhoardQuestionDataProvider = dashboardQuestionDataProviders[i]; @@ -306,11 +354,13 @@ export class SolidRegistry { } getComputedFieldProvider(name: string): InstanceWrapper { - const provider = this.getComputedFieldProviders().filter((provider) => provider.name === name).pop(); + const provider = this.getComputedFieldProviders() + .filter((provider) => provider.name === name) + .pop(); if (!provider) { throw new Error(`Computed Field Provider with name ${name} not found`); } - return provider + return provider; } getSolidDatabaseModules(): Array { @@ -321,8 +371,9 @@ export class SolidRegistry { const solidDatabaseModulesAsArray = Array.from(this.solidDatabaseModules); for (let i = 0; i < solidDatabaseModulesAsArray.length; i++) { const solidDatabaseModule = solidDatabaseModulesAsArray[i]; - const solidDatabaseModuleInstance: ISolidDatabaseModule = solidDatabaseModule.instance; - if (solidDatabaseModuleInstance.name() === 'default') { + const solidDatabaseModuleInstance: ISolidDatabaseModule = + solidDatabaseModule.instance; + if (solidDatabaseModuleInstance.name() === "default") { return solidDatabaseModuleInstance; } } @@ -333,27 +384,37 @@ export class SolidRegistry { } getModule(name: string): InstanceWrapper { - const module = this.getModules().filter((module) => module.name === name).pop(); - return module + const module = this.getModules() + .filter((module) => module.name === name) + .pop(); + return module; } getComputedFieldMetadata(): ComputedFieldMetadata[] { return this.computedFieldMetadata; } - //TODO:getlocales from locale model and return default locale where isDefault:true + //TODO:getlocales from locale model and return default locale where isDefault:true getDefaultLocale(): Locale | null { - return this.locales.find(locale => locale.isDefault === true) || null; + return this.locales.find((locale) => locale.isDefault === true) || null; } - getSecurityRules(modelSingularName: string, roleNames: string[] = []): SecurityRule[] { + getSecurityRules( + modelSingularName: string, + roleNames: string[] = [], + ): SecurityRule[] { // If no role is provided, return all security rules for the model if (roleNames.length === 0) { - return this.securityRules.filter((rule) => rule.modelMetadata.singularName === modelSingularName); + return this.securityRules.filter( + (rule) => rule.modelMetadata.singularName === modelSingularName, + ); } // If roles are provided, filter the security rules by model and roles return this.securityRules.filter((rule) => { - return rule.modelMetadata.singularName === modelSingularName && roleNames.includes(rule.role.name); + return ( + rule.modelMetadata.singularName === modelSingularName && + roleNames.includes(rule.role.name) + ); }); } @@ -365,13 +426,25 @@ export class SolidRegistry { return this.auditableModels.has(modelSingularName.toLowerCase()); } - getCommonEntityKeys(): (keyof CommonEntity | 'createdBy' | 'updatedBy')[] { - return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt']; + getCommonEntityKeys(): (keyof CommonEntity | "createdBy" | "updatedBy")[] { + return [ + "id", + "createdAt", + "updatedAt", + "deletedAt", + "createdBy", + "updatedBy", + "deletedTracker", + "localeName", + "defaultEntityLocaleId", + "publishedAt", + ]; // return Reflect.getMetadataKeys(CommonEntity.prototype) as (keyof CommonEntity)[]; } - - getAllSettingsProviderInstance(): ISelectionProvider { + getAllSettingsProviderInstance< + T extends ISelectionProviderContext, + >(): ISelectionProvider { const settingsProviders = this.getSettingsProviders(); for (let i = 0; i < settingsProviders.length; i++) { @@ -380,4 +453,3 @@ export class SolidRegistry { } } } - diff --git a/src/index.ts b/src/index.ts index 800b4617..87a3f30e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export * from './decorators/solid-service.decorator' export * from './decorators/mail-provider.decorator' export * from './decorators/security-rule-config-provider.decorator' export * from './decorators/sms-provider.decorator' +export * from './decorators/push-notification-provider.decorator' export * from './decorators/settings-provider.decorator' export * from './decorators/extension-user-creation-provider.decorator' @@ -56,6 +57,7 @@ export * from './dtos/create-permission-metadata.dto' export * from './dtos/create-role-metadata.dto' export * from './dtos/create-short-url.dto' export * from './dtos/create-sms-template.dto' +export * from './dtos/create-push-notification-template.dto' export * from './dtos/create-user.dto' export * from './dtos/create-view-metadata.dto' export * from './dtos/create-chatter-message.dto' @@ -96,6 +98,7 @@ export * from './dtos/update-scheduled-job.dto' export * from './dtos/update-permission-metadata.dto' export * from './dtos/update-role-metadata.dto' export * from './dtos/update-sms-template.dto' +export * from './dtos/update-push-notification-template.dto' export * from './dtos/update-user.dto' export * from './dtos/update-view-metadata.dto' export * from './dtos/create-setting.dto' @@ -107,6 +110,7 @@ export * from './dtos/update-chatter-message-details.dto' export * from './dtos/update-locale.dto' export * from './dtos/create-user-activity-history.dto' export * from './dtos/update-user-activity-history.dto' +export * from './dtos/device-metadata.dto' export * from './entities/action-metadata.entity' export * from './entities/common.entity' @@ -130,8 +134,10 @@ export * from './entities/scheduled-job.entity' export * from './entities/permission-metadata.entity' export * from './entities/role-metadata.entity' export * from './entities/sms-template.entity' +export * from './entities/push-notification-template.entity' export * from './entities/user.entity' export * from './entities/user-api-key.entity' +export * from './entities/user-device-metadata.entity' export * from './entities/view-metadata.entity' export * from './entities/setting.entity' export * from './entities/saved-filters.entity' @@ -261,6 +267,15 @@ export * from './jobs/redis/trigger-mcp-client-subscriber-redis.service' export * from './jobs/redis/twilio-sms-publisher-redis.service' export * from './jobs/redis/twilio-sms-queue-options-redis' export * from './jobs/redis/twilio-sms-subscriber-redis.service' +export * from './jobs/rabbitmq/amazon-sns-push-notification-publisher.service' +export * from './jobs/rabbitmq/amazon-sns-push-notification-queue-options' +export * from './jobs/rabbitmq/amazon-sns-push-notification-subscriber.service' +export * from './jobs/redis/amazon-sns-push-notification-publisher-redis.service' +export * from './jobs/redis/amazon-sns-push-notification-queue-options-redis' +export * from './jobs/redis/amazon-sns-push-notification-subscriber-redis.service' +export * from './jobs/database/amazon-sns-push-notification-publisher-database.service' +export * from './jobs/database/amazon-sns-push-notification-queue-options-database' +export * from './jobs/database/amazon-sns-push-notification-subscriber-database.service' export * from './listeners/user-registration.listener' @@ -317,8 +332,10 @@ export * from './services/sms/Msg91BaseSMSService' //rename export * from './services/sms/Msg91OTPService' //rename export * from './services/sms/Msg91SMSService' //rename export * from './services/sms/TwilioSMSService' //rename +export * from './services/push/amazon-sns-push-notification.service' export * from './services/poller.service' export * from './services/sms-template.service' +export * from './services/push-notification-template.service' export * from './services/solid-introspect.service' export * from './services/user.service' export * from './services/view-metadata.service' @@ -348,6 +365,7 @@ export * from './services/ai-interaction.service' export * from './factories/mail.factory' export * from './factories/sms.factory' export * from './factories/whatsapp.factory' +export * from './factories/push-notification.factory' // Repositories export * from './repository/solid-base.repository' diff --git a/src/interfaces.ts b/src/interfaces.ts index 20e0d511..871ab907 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,25 +1,27 @@ -import { Repository } from 'typeorm'; -import { User } from 'src/entities/user.entity'; -import { CreateUserDto } from 'src/dtos/create-user.dto'; -import { CreateEmailTemplateDto } from 'src/dtos/create-email-template.dto'; -import { CreateSmsTemplateDto } from 'src/dtos/create-sms-template.dto'; -import { SignUpDto } from 'src/dtos/sign-up.dto'; -import { Readable } from 'stream'; -import { CreateMediaStorageProviderMetadataDto } from './dtos/create-media-storage-provider-metadata.dto'; -import { DatasourceType } from './dtos/create-model-metadata.dto'; -import { CreateModuleMetadataDto } from './dtos/create-module-metadata.dto'; -import { CreateRoleMetadataDto } from './dtos/create-role-metadata.dto'; -import { CreateSecurityRuleDto } from './dtos/create-security-rule.dto'; -import { FieldMetadata } from './entities/field-metadata.entity'; -import { Media } from './entities/media.entity'; -import { DashboardQuestion } from './entities/dashboard-question.entity'; -import { ComputedFieldMetadata } from './helpers/solid-registry'; -import { SqlExpression } from './services/question-data-providers/chartjs-sql-data-provider.service'; -import { CreateDashboardDto } from './dtos/create-dashboard.dto'; -import { AiInteraction } from './entities/ai-interaction.entity'; -import { ActiveUserData } from './interfaces/active-user-data.interface'; -import { SecurityRuleConfig } from './dtos/security-rule-config.dto'; -import { SecurityRule } from './entities/security-rule.entity'; +import { Repository } from "typeorm"; +import { User } from "src/entities/user.entity"; +import { CreateUserDto } from "src/dtos/create-user.dto"; +import { CreateEmailTemplateDto } from "src/dtos/create-email-template.dto"; +import { CreatePushNotificationTemplateDto } from "src/dtos/create-push-notification-template.dto"; +import { CreateSmsTemplateDto } from "src/dtos/create-sms-template.dto"; +import { SignUpDto } from "src/dtos/sign-up.dto"; +import { Readable } from "stream"; +import { CreateMediaStorageProviderMetadataDto } from "./dtos/create-media-storage-provider-metadata.dto"; +import { DatasourceType } from "./dtos/create-model-metadata.dto"; +import { CreateModuleMetadataDto } from "./dtos/create-module-metadata.dto"; +import { CreateRoleMetadataDto } from "./dtos/create-role-metadata.dto"; +import { CreateSecurityRuleDto } from "./dtos/create-security-rule.dto"; +import { FieldMetadata } from "./entities/field-metadata.entity"; +import { Media } from "./entities/media.entity"; +import { DashboardQuestion } from "./entities/dashboard-question.entity"; +import { ComputedFieldMetadata } from "./helpers/solid-registry"; +import { SqlExpression } from "./services/question-data-providers/chartjs-sql-data-provider.service"; +import { CreateDashboardDto } from "./dtos/create-dashboard.dto"; +import { AiInteraction } from "./entities/ai-interaction.entity"; +import { ActiveUserData } from "./interfaces/active-user-data.interface"; +import { SecurityRuleConfig } from "./dtos/security-rule-config.dto"; +import { SecurityRule } from "./entities/security-rule.entity"; +import { PublishCommandOutput } from "@aws-sdk/client-sns"; export interface FieldCrudManager { // fieldMetadata: FieldMetadata; @@ -40,45 +42,54 @@ export interface ValidationError { // export interface MediaStorage export interface MediaStorageProvider { - store(files: Express.Multer.File[], entity: T, mediaFieldMetadata: FieldMetadata): Promise; + store( + files: Express.Multer.File[], + entity: T, + mediaFieldMetadata: FieldMetadata, + ): Promise; delete(entity: T, mediaFieldMetadata: FieldMetadata): Promise; deleteByMediaRecord(media: Media): Promise; retrieve(entity: T, mediaFieldMetadata: FieldMetadata): Promise; - storeStreams(streamPairs: [Readable, string][], entity: T, mediaFieldMetadata: FieldMetadata): Promise; + storeStreams( + streamPairs: [Readable, string][], + entity: T, + mediaFieldMetadata: FieldMetadata, + ): Promise; // delete(file: string): Promise; } export interface ModuleMetadataConfiguration { - moduleMetadata?: CreateModuleMetadataDto, - roles?: CreateRoleMetadataDto[], - users?: SignUpDto[], - actions?: any[], - menus?: any[], - views?: any[], - emailTemplates?: CreateEmailTemplateDto[], - smsTemplates?: CreateSmsTemplateDto[], - mediaStorageProviders?: CreateMediaStorageProviderMetadataDto[] - securityRules?: CreateSecurityRuleDto[], - dashboards?: CreateDashboardDto[], + moduleMetadata?: CreateModuleMetadataDto; + roles?: CreateRoleMetadataDto[]; + users?: SignUpDto[]; + actions?: any[]; + menus?: any[]; + views?: any[]; + emailTemplates?: CreateEmailTemplateDto[]; + smsTemplates?: CreateSmsTemplateDto[]; + pushNotificationTemplates?: CreatePushNotificationTemplateDto[]; + mediaStorageProviders?: CreateMediaStorageProviderMetadataDto[]; + securityRules?: CreateSecurityRuleDto[]; + dashboards?: CreateDashboardDto[]; } export enum SettingLevel { - SystemEnv = 'system-env', - SystemAdminReadonly = 'system-admin-readonly', - SystemAdminEditable = 'system-admin-editable', - InternalUser = "internal-user" + SystemEnv = "system-env", + SystemAdminReadonly = "system-admin-readonly", + SystemAdminEditable = "system-admin-editable", + InternalUser = "internal-user", } export type SettingControlType = - | 'shortText' - | 'longText' - | 'numeric' - | 'boolean' - | 'date' - | 'datetime' - | 'mediaSingle' - | 'selectionStatic' - | 'custom'; + | "shortText" + | "longText" + | "numeric" + | "boolean" + | "date" + | "datetime" + | "mediaSingle" + | "selectionStatic" + | "custom"; export interface SettingOption { label: string; @@ -166,25 +177,29 @@ export interface ISelectionProvider { values(query: any, ctxt: T): Promise; } -export interface IDashboardVariableSelectionProvider extends ISelectionProvider { -} +export interface IDashboardVariableSelectionProvider< + T extends ISelectionProviderContext, +> extends ISelectionProvider {} export interface IMcpToolResponseHandler { apply(aiInteraction: AiInteraction); } export interface QuestionSqlDataProviderContext { - // questionSqlDatasetConfig: QuestionSqlDatasetConfig; - // questionId: number; - // question: Question; - expressions?: SqlExpression[] + // questionSqlDatasetConfig: QuestionSqlDatasetConfig; + // questionId: number; + // question: Question; + expressions?: SqlExpression[]; } export interface IDashboardQuestionDataProvider { help(): string; name(): string; - getData(question: DashboardQuestion, ctxt?: TContext): Promise; + getData( + question: DashboardQuestion, + ctxt?: TContext, + ): Promise; } /** @@ -206,12 +221,25 @@ export interface IEntityComputedFieldProvider { name(): string; } -export interface IEntityPreComputeFieldProvider extends IEntityComputedFieldProvider { - preComputeValue(triggerEntity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata): Promise; +export interface IEntityPreComputeFieldProvider< + TTriggerEntity, + TContext, + TValue = void, +> extends IEntityComputedFieldProvider { + preComputeValue( + triggerEntity: TTriggerEntity, + computedFieldMetadata: ComputedFieldMetadata, + ): Promise; } -export interface IEntityPostComputeFieldProvider extends IEntityComputedFieldProvider { - postComputeAndSaveValue(triggerEntity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata): Promise; +export interface IEntityPostComputeFieldProvider< + TTriggerEntity, + TContext, +> extends IEntityComputedFieldProvider { + postComputeAndSaveValue( + triggerEntity: TTriggerEntity, + computedFieldMetadata: ComputedFieldMetadata, + ): Promise; } export interface ISolidDatabaseModule { @@ -221,17 +249,20 @@ export interface ISolidDatabaseModule { } export enum EventType { - USER_REGISTERED = 'user.registered', + USER_REGISTERED = "user.registered", } export class EventDetails { constructor( public type: any, public payload: T, - ) { } + ) {} } -export interface IExtensionUserCreationProvider { +export interface IExtensionUserCreationProvider< + T extends User = User, + TDto extends CreateUserDto = CreateUserDto, +> { readonly repo: Repository; buildExtensionEntity(dto: TDto): Promise; roles(dto: TDto): string[]; @@ -284,10 +315,56 @@ export interface IWhatsAppTransport { templateId: string, parameters: any, parentEntity?: any, - parentEntityId?: any + parentEntityId?: any, ): Promise; } +export interface IPushNotification { + sendPushNotification( + endpointArn: string, + payload: PushNotificationPayload, + shouldQueuePush?: boolean, + ): Promise; + + sendPushNotificationUsingTemplate( + endpointArn: string, + templateName: string, + templateParams: any, + shouldQueuePush?: boolean, + ): Promise; + + sendPushNotificationSynchronously( + message: PushNotificationQueuePayload, + ): Promise; + + registerDevice(payload: RegisterDevicePayload): Promise; + + unregisterDevice(userId: number, deviceId: string): Promise; +} + +export interface PushNotificationQueuePayload { + endpointArn: string; + payload: PushNotificationPayload; +} + +export interface PushNotificationPayload { + title: string; + body: string; + data?: Record; +} + +export interface RegisterDevicePayload { + userId: number; + deviceId: string; + deviceToken: string; + platform: string; + deviceName?: string; + deviceType?: string; + osName?: string; + osVersion?: string; + appVersion?: string; +} + export interface MailAttachmentWrapper { relativePath?: string; attachment?: MailAttachment; @@ -299,13 +376,13 @@ export interface MailAttachment { templateParams?: any; // deprecated content?: string | Buffer; contentType?: string; - path?: string; //Filesystem absolute path or URL. + path?: string; //Filesystem absolute path or URL. } export enum BrokerType { - RabbitMQ = 'rabbitmq', - Database = 'database', - Redis = 'redis', + RabbitMQ = "rabbitmq", + Database = "database", + Redis = "redis", } export interface QueuesModuleOptions { @@ -320,7 +397,6 @@ export type MediaWithFullUrl = Media & { _full_url: string; }; - export type ErrorCode = string; export type ErrorMeta = { @@ -358,47 +434,54 @@ export interface IErrorCodeProvider { // MCP Tool Related -export type PlanStep = CreateNewFileStep | RegisterNestProviderStep | AddMethodToExistingClassStep | RegisterSolidXExtensionComponentStep | AddListViewButtonStep | AddFormViewButtonStep | AddImportStep; +export type PlanStep = + | CreateNewFileStep + | RegisterNestProviderStep + | AddMethodToExistingClassStep + | RegisterSolidXExtensionComponentStep + | AddListViewButtonStep + | AddFormViewButtonStep + | AddImportStep; export interface AddImportStep { type: "addImport"; - path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts - importStatement: string; // e.g. import { Something } from 'somewhere'; - overwrite?: boolean; // default=false - rationale?: string; // optional, ignored by executor + path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts + importStatement: string; // e.g. import { Something } from 'somewhere'; + overwrite?: boolean; // default=false + rationale?: string; // optional, ignored by executor } export interface CreateNewFileStep { type: "createNewFile"; - path: string; // repo-relative e.g. solid-api/api/src/computed-providers/foo.provider.ts - content: string; // full file content - overwrite?: boolean; // default=false - rationale?: string; // optional, ignored by executor + path: string; // repo-relative e.g. solid-api/api/src/computed-providers/foo.provider.ts + content: string; // full file content + overwrite?: boolean; // default=false + rationale?: string; // optional, ignored by executor } export interface RegisterNestProviderStep { type: "registerNestProvider"; - modulePath: string; // e.g. apps/api/src/address-master/address-master.module.ts - providerClassName: string; // e.g. StateTotalCitiesComputedFieldProvider - importFrom: string; // e.g. "@/computed-providers/state-total-cities.provider" + modulePath: string; // e.g. apps/api/src/address-master/address-master.module.ts + providerClassName: string; // e.g. StateTotalCitiesComputedFieldProvider + importFrom: string; // e.g. "@/computed-providers/state-total-cities.provider" registerIn: Array<"providers" | "exports">; // which arrays to add to - uniqueGuard?: boolean; // default=true - rationale?: string; // optional, ignored by executor + uniqueGuard?: boolean; // default=true + rationale?: string; // optional, ignored by executor } export interface AddMethodToExistingClassStep { type: "addMethodToExistingClass"; - path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts - className: string // e.g. CountryService - methodName: string // e.g. addCountry - content: string // Full Method Code - importStatements?: string[]; // e.g. [ "import { X } from 'y';" ] - rationale?: string; // optional, ignored by executor + path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts + className: string; // e.g. CountryService + methodName: string; // e.g. addCountry + content: string; // Full Method Code + importStatements?: string[]; // e.g. [ "import { X } from 'y';" ] + rationale?: string; // optional, ignored by executor } export interface RegisterSolidXExtensionComponentStep { type: "registerSolidXExtensionComponent"; - path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts - content?: string; // Code + path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts + content?: string; // Code importExtensionComponent?: string; } @@ -407,7 +490,7 @@ export interface AddListViewButtonStep { moduleName?: string; modelName?: string; buttonType?: string; - content?: string; // Code + content?: string; // Code } export interface AddFormViewButtonStep { @@ -415,7 +498,7 @@ export interface AddFormViewButtonStep { moduleName?: string; modelName?: string; buttonType?: string; - content?: string; // Code + content?: string; // Code } export interface McpComputedProviderResponse { @@ -424,10 +507,12 @@ export interface McpComputedProviderResponse { } export interface ISecurityRuleConfigProvider { - securityRuleConfig(activeUser: ActiveUserData, securityRule: SecurityRule): Promise; + securityRuleConfig( + activeUser: ActiveUserData, + securityRule: SecurityRule, + ): Promise; } - export interface AwsS3Config { S3_AWS_ACCESS_KEY: string; S3_AWS_SECRET_KEY: string; @@ -437,15 +522,15 @@ export interface AwsS3Config { // Prevents inference so callers must provide explicit type arguments; reusable for other APIs. export type NoInfer = [T][T extends any ? 0 : never]; -export type AuditEventType = 'insert' | 'update' | 'delete'; +export type AuditEventType = "insert" | "update" | "delete"; export interface AuditQueuePayload { - eventType: AuditEventType; - modelName: string; - entityId: string | number | null; - occurredAt: string; - after?: any; - before?: any; - updatedColumnNames?: string[]; - userId?: number | null; + eventType: AuditEventType; + modelName: string; + entityId: string | number | null; + occurredAt: string; + after?: any; + before?: any; + updatedColumnNames?: string[]; + userId?: number | null; } diff --git a/src/jobs/database/amazon-sns-push-notification-publisher-database.service.ts b/src/jobs/database/amazon-sns-push-notification-publisher-database.service.ts new file mode 100644 index 00000000..ff3b8807 --- /dev/null +++ b/src/jobs/database/amazon-sns-push-notification-publisher-database.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { QueuesModuleOptions } from "src/interfaces"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { DatabasePublisher } from "src/services/queues/database-publisher.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options-database"; + +@Injectable() +export class AmazonSnsPushNotificationQueuePublisherDatabase extends DatabasePublisher { + constructor( + protected readonly mqMessageService: MqMessageService, + protected readonly mqMessageQueueService: MqMessageQueueService, + ) { + super(mqMessageService, mqMessageQueueService); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } +} diff --git a/src/jobs/database/amazon-sns-push-notification-queue-options-database.ts b/src/jobs/database/amazon-sns-push-notification-queue-options-database.ts new file mode 100644 index 00000000..a9e0a02d --- /dev/null +++ b/src/jobs/database/amazon-sns-push-notification-queue-options-database.ts @@ -0,0 +1,9 @@ +import { BrokerType } from "src/interfaces"; + +const QUEUE_NAME = "solid_amazon_sns_push_notification_db_queue_v1"; + +export default { + name: QUEUE_NAME, + type: BrokerType.Database, + queueName: QUEUE_NAME, +}; diff --git a/src/jobs/database/amazon-sns-push-notification-subscriber-database.service.ts b/src/jobs/database/amazon-sns-push-notification-subscriber-database.service.ts new file mode 100644 index 00000000..9901f25d --- /dev/null +++ b/src/jobs/database/amazon-sns-push-notification-subscriber-database.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@nestjs/common"; +import { PushNotificationFactory } from "src/factories/push-notification.factory"; +import { QueuesModuleOptions } from "src/interfaces"; +import { QueueMessage } from "src/interfaces/mq"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { PollerService } from "src/services/poller.service"; +import { AmazonSNSPushNotificationService } from "src/services/push/amazon-sns-push-notification.service"; +import { DatabaseSubscriber } from "src/services/queues/database-subscriber.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options-database"; + +@Injectable() +export class AmazonSnsPushNotificationQueueSubscriberDatabase extends DatabaseSubscriber { + constructor( + private readonly pushNotificationFactory: PushNotificationFactory, + readonly mqMessageService: MqMessageService, + readonly mqMessageQueueService: MqMessageQueueService, + readonly poller: PollerService, + ) { + super(mqMessageService, mqMessageQueueService, poller); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } + + subscribe(message: QueueMessage) { + const pushNotificationService = + this.pushNotificationFactory.getPushNotificationService( + AmazonSNSPushNotificationService.name, + ) as AmazonSNSPushNotificationService; + + return pushNotificationService.sendPushNotificationSynchronously( + message.payload, + ); + } +} diff --git a/src/jobs/rabbitmq/amazon-sns-push-notification-publisher.service.ts b/src/jobs/rabbitmq/amazon-sns-push-notification-publisher.service.ts new file mode 100644 index 00000000..6b2b14be --- /dev/null +++ b/src/jobs/rabbitmq/amazon-sns-push-notification-publisher.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { QueuesModuleOptions } from "src/interfaces"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { RabbitMqPublisher } from "src/services/queues/rabbitmq-publisher.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options"; + +@Injectable() +export class AmazonSnsPushNotificationQueuePublisherRabbitmq extends RabbitMqPublisher { + constructor( + protected readonly mqMessageService: MqMessageService, + protected readonly mqMessageQueueService: MqMessageQueueService, + ) { + super(mqMessageService, mqMessageQueueService); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } +} diff --git a/src/jobs/rabbitmq/amazon-sns-push-notification-queue-options.ts b/src/jobs/rabbitmq/amazon-sns-push-notification-queue-options.ts new file mode 100644 index 00000000..40544368 --- /dev/null +++ b/src/jobs/rabbitmq/amazon-sns-push-notification-queue-options.ts @@ -0,0 +1,9 @@ +import { BrokerType } from "../../interfaces"; + +const QUEUE_NAME = "solid_amazon_sns_push_notification_queue_v1"; + +export default { + name: QUEUE_NAME, + type: BrokerType.RabbitMQ, + queueName: QUEUE_NAME, +}; diff --git a/src/jobs/rabbitmq/amazon-sns-push-notification-subscriber.service.ts b/src/jobs/rabbitmq/amazon-sns-push-notification-subscriber.service.ts new file mode 100644 index 00000000..8d0500b1 --- /dev/null +++ b/src/jobs/rabbitmq/amazon-sns-push-notification-subscriber.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@nestjs/common"; +import { PushNotificationFactory } from "src/factories/push-notification.factory"; +import { QueueMessage } from "src/interfaces/mq"; +import { QueuesModuleOptions } from "src/interfaces"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { PollerService } from "src/services/poller.service"; +import { RabbitMqSubscriber } from "src/services/queues/rabbitmq-subscriber.service"; +import { AmazonSNSPushNotificationService } from "src/services/push/amazon-sns-push-notification.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options"; + +@Injectable() +export class AmazonSnsPushNotificationQueueSubscriberRabbitmq extends RabbitMqSubscriber { + constructor( + private readonly pushNotificationFactory: PushNotificationFactory, + readonly mqMessageService: MqMessageService, + readonly mqMessageQueueService: MqMessageQueueService, + readonly poller: PollerService, + ) { + super(mqMessageService, mqMessageQueueService); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } + + subscribe(message: QueueMessage) { + const pushNotificationService = + this.pushNotificationFactory.getPushNotificationService( + AmazonSNSPushNotificationService.name, + ) as AmazonSNSPushNotificationService; + + return pushNotificationService.sendPushNotificationSynchronously( + message.payload, + ); + } +} diff --git a/src/jobs/redis/amazon-sns-push-notification-publisher-redis.service.ts b/src/jobs/redis/amazon-sns-push-notification-publisher-redis.service.ts new file mode 100644 index 00000000..515fc51b --- /dev/null +++ b/src/jobs/redis/amazon-sns-push-notification-publisher-redis.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { QueuesModuleOptions } from "src/interfaces"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { RedisPublisher } from "src/services/queues/redis-publisher.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options-redis"; + +@Injectable() +export class AmazonSnsPushNotificationQueuePublisherRedis extends RedisPublisher { + constructor( + protected readonly mqMessageService: MqMessageService, + protected readonly mqMessageQueueService: MqMessageQueueService, + ) { + super(mqMessageService, mqMessageQueueService); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } +} diff --git a/src/jobs/redis/amazon-sns-push-notification-queue-options-redis.ts b/src/jobs/redis/amazon-sns-push-notification-queue-options-redis.ts new file mode 100644 index 00000000..5c5eb0ea --- /dev/null +++ b/src/jobs/redis/amazon-sns-push-notification-queue-options-redis.ts @@ -0,0 +1,9 @@ +import { BrokerType } from "../../interfaces"; + +const QUEUE_NAME = "solid_amazon_sns_push_notification_queue_redis_v1"; + +export default { + name: QUEUE_NAME, + type: BrokerType.Redis, + queueName: QUEUE_NAME, +}; diff --git a/src/jobs/redis/amazon-sns-push-notification-subscriber-redis.service.ts b/src/jobs/redis/amazon-sns-push-notification-subscriber-redis.service.ts new file mode 100644 index 00000000..64f3f707 --- /dev/null +++ b/src/jobs/redis/amazon-sns-push-notification-subscriber-redis.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@nestjs/common"; +import { PushNotificationFactory } from "src/factories/push-notification.factory"; +import { QueueMessage } from "src/interfaces/mq"; +import { QueuesModuleOptions } from "src/interfaces"; +import { MqMessageQueueService } from "src/services/mq-message-queue.service"; +import { MqMessageService } from "src/services/mq-message.service"; +import { PollerService } from "src/services/poller.service"; +import { RedisSubscriber } from "src/services/queues/redis-subscriber.service"; +import { AmazonSNSPushNotificationService } from "src/services/push/amazon-sns-push-notification.service"; +import queueOptions from "./amazon-sns-push-notification-queue-options-redis"; + +@Injectable() +export class AmazonSnsPushNotificationQueueSubscriberRedis extends RedisSubscriber { + constructor( + private readonly pushNotificationFactory: PushNotificationFactory, + readonly mqMessageService: MqMessageService, + readonly mqMessageQueueService: MqMessageQueueService, + readonly poller: PollerService, + ) { + super(mqMessageService, mqMessageQueueService); + } + + options(): QueuesModuleOptions { + return { + ...queueOptions, + }; + } + + subscribe(message: QueueMessage) { + const pushNotificationService = + this.pushNotificationFactory.getPushNotificationService( + AmazonSNSPushNotificationService.name, + ) as AmazonSNSPushNotificationService; + + return pushNotificationService.sendPushNotificationSynchronously( + message.payload, + ); + } +} diff --git a/src/repository/push-notification-template.repository.ts b/src/repository/push-notification-template.repository.ts new file mode 100644 index 00000000..5c838fc2 --- /dev/null +++ b/src/repository/push-notification-template.repository.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { DataSource } from "typeorm"; +import { RequestContextService } from "src/services/request-context.service"; +import { SecurityRuleRepository } from "./security-rule.repository"; +import { SolidBaseRepository } from "./solid-base.repository"; +import { PushNotificationTemplate } from "src/entities/push-notification-template.entity"; + +@Injectable() +export class PushNotificationTemplateRepository extends SolidBaseRepository { + constructor( + readonly dataSource: DataSource, + readonly requestContextService: RequestContextService, + readonly securityRuleRepository: SecurityRuleRepository, + ) { + super( + PushNotificationTemplate, + dataSource, + requestContextService, + securityRuleRepository, + ); + } +} diff --git a/src/repository/user-device-metadata.repository.ts b/src/repository/user-device-metadata.repository.ts new file mode 100644 index 00000000..11acc460 --- /dev/null +++ b/src/repository/user-device-metadata.repository.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@nestjs/common"; +import { DataSource } from "typeorm"; +import { UserDeviceMetadata } from "src/entities/user-device-metadata.entity"; +import { RequestContextService } from "src/services/request-context.service"; +import { SecurityRuleRepository } from "./security-rule.repository"; +import { SolidBaseRepository } from "./solid-base.repository"; + +@Injectable() +export class UserDeviceMetadataRepository extends SolidBaseRepository { + constructor( + readonly dataSource: DataSource, + readonly requestContextService: RequestContextService, + readonly securityRuleRepository: SecurityRuleRepository, + ) { + super( + UserDeviceMetadata, + dataSource, + requestContextService, + securityRuleRepository, + ); + } +} diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index d5700452..e9afe10f 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { CreateDashboardDto } from 'src/dtos/create-dashboard.dto'; import { CreateEmailTemplateDto } from 'src/dtos/create-email-template.dto'; +import { CreatePushNotificationTemplateDto } from 'src/dtos/create-push-notification-template.dto'; import { CreateListOfValuesDto } from 'src/dtos/create-list-of-values.dto'; import { CreateSecurityRuleDto } from 'src/dtos/create-security-rule.dto'; import { CreateSmsTemplateDto } from 'src/dtos/create-sms-template.dto'; @@ -15,6 +16,7 @@ import { EmailTemplateService } from 'src/services/email-template.service'; import { ListOfValuesService } from 'src/services/list-of-values.service'; import { SettingService } from 'src/services/setting.service'; import { SmsTemplateService } from 'src/services/sms-template.service'; +import { PushNotificationTemplateService } from 'src/services/push-notification-template.service'; import { UserService } from 'src/services/user.service'; import { DataSource, In } from 'typeorm'; import { CreateModelMetadataDto } from '../dtos/create-model-metadata.dto'; @@ -77,6 +79,7 @@ export class ModuleMetadataSeederService { private readonly solidViewService: ViewMetadataService, private readonly emailTemplateService: EmailTemplateService, private readonly smsTemplateService: SmsTemplateService, + private readonly pushNotificationTemplateService: PushNotificationTemplateService, private readonly listOfValuesService: ListOfValuesService, // @InjectRepository(PermissionMetadata) // private readonly permissionRepo: Repository, @@ -197,6 +200,11 @@ export class ModuleMetadataSeederService { const smsTemplateCounts = await this.seedSmsTemplates(overallMetadata, moduleMetadata.name); console.log(`${this.formatSeedResult(moduleMetadata.name, 'Sms Templates', smsTemplateCounts)}`); + currentStep = 'seedPushNotificationTemplates'; + this.logger.log(`Seeding Push Notification Templates`); + const pushNotificationTemplateCounts = await this.seedPushNotificationTemplates(overallMetadata, moduleMetadata.name); + console.log(`${this.formatSeedResult(moduleMetadata.name, 'Push Notification Templates', pushNotificationTemplateCounts)}`); + currentStep = 'seedSecurityRules'; this.logger.log(`Seeding Security Rules`); const securityRuleCounts = await this.seedSecurityRules(overallMetadata); @@ -326,6 +334,14 @@ export class ModuleMetadataSeederService { return { pruned: 0, upserted: smsTemplates?.length ?? 0 }; } + private async seedPushNotificationTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> { + this.logger.debug(`[Start] Processing push notification templates`); + const pushNotificationTemplates: CreatePushNotificationTemplateDto[] = overallMetadata.pushNotificationTemplates; + await this.handleSeedPushNotificationTemplates(pushNotificationTemplates, moduleName); + this.logger.debug(`[End] Processing push notification templates`); + return { pruned: 0, upserted: pushNotificationTemplates?.length ?? 0 }; + } + // OK private async seedEmailTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing email templates`); @@ -657,6 +673,67 @@ export class ModuleMetadataSeederService { } } + private async handleSeedPushNotificationTemplates(pushNotificationTemplates: CreatePushNotificationTemplateDto[], moduleName: string) { + if (!pushNotificationTemplates) { + return; + } + + for (let i = 0; i < pushNotificationTemplates.length; i++) { + const pushNotificationTemplate = pushNotificationTemplates[i]; + + // We need to load the actual template contents. + if (moduleName === 'solid-core') { + let moduleRoot: string | null = null; + + try { + // Always resolve package.json, never the module entry + moduleRoot = path.dirname( + require.resolve('@solidxai/core/package.json'), + ); + } catch (err: any) { + this.logger.debug( + 'Could not resolve @solidxai/core from node_modules, assuming local execution', + ); + } + + const filePathInternal = 'src/seeders/seed-data/push-notification-templates/'; + let filePath: string; + // Case 1: solid-core installed as dependency + if (moduleRoot) { + filePath = path.join( + moduleRoot, + filePathInternal, + pushNotificationTemplate.body, + ); + } + else { + // Case 2: running INSIDE solid-core repo + const localCoreRoot = process.cwd(); + filePath = path.join( + localCoreRoot, + filePathInternal, + pushNotificationTemplate.body, + ); + } + + if (fs.existsSync(filePath)) { + pushNotificationTemplate.body = fs.readFileSync(filePath, 'utf-8'); + } + } + else { + const pushNotificationTemplateHandlebar = `module-metadata/${moduleName}/push-notification-templates/${pushNotificationTemplate.body}`; + const fullPath = path.join(process.cwd(), pushNotificationTemplateHandlebar); + if (fs.existsSync(fullPath)) { + pushNotificationTemplate.body = fs.readFileSync(fullPath, 'utf-8').toString(); + } + } + + // Save to DB. + await this.pushNotificationTemplateService.removeByName(pushNotificationTemplate.name); + await this.pushNotificationTemplateService.createFromSeed(pushNotificationTemplate); + } + } + // Ok private async handleSeedMenus(menus: any) { if (!menus) { diff --git a/src/seeders/seed-data/push-notification-templates/forgot-password.handlebars.txt b/src/seeders/seed-data/push-notification-templates/forgot-password.handlebars.txt new file mode 100644 index 00000000..290375db --- /dev/null +++ b/src/seeders/seed-data/push-notification-templates/forgot-password.handlebars.txt @@ -0,0 +1 @@ +Use code {{token}} to reset your {{solidAppName}} password. diff --git a/src/seeders/seed-data/push-notification-templates/new-login-detected.handlebars.txt b/src/seeders/seed-data/push-notification-templates/new-login-detected.handlebars.txt new file mode 100644 index 00000000..44907e4a --- /dev/null +++ b/src/seeders/seed-data/push-notification-templates/new-login-detected.handlebars.txt @@ -0,0 +1,2 @@ +Your account logged in from {{device}}. + diff --git a/src/seeders/seed-data/push-notification-templates/otp-on-login.handlebars.txt b/src/seeders/seed-data/push-notification-templates/otp-on-login.handlebars.txt new file mode 100644 index 00000000..f83cc222 --- /dev/null +++ b/src/seeders/seed-data/push-notification-templates/otp-on-login.handlebars.txt @@ -0,0 +1 @@ +Your login OTP for {{solidAppName}} is {{otp}}. diff --git a/src/seeders/seed-data/push-notification-templates/otp-on-register.handlebars.txt b/src/seeders/seed-data/push-notification-templates/otp-on-register.handlebars.txt new file mode 100644 index 00000000..62d683a4 --- /dev/null +++ b/src/seeders/seed-data/push-notification-templates/otp-on-register.handlebars.txt @@ -0,0 +1 @@ +Hi {{name}}, your verification code for {{solidAppName}} is {{otp}}. diff --git a/src/seeders/seed-data/push-notification-templates/welcome-on-signup.handlebars.txt b/src/seeders/seed-data/push-notification-templates/welcome-on-signup.handlebars.txt new file mode 100644 index 00000000..976a6515 --- /dev/null +++ b/src/seeders/seed-data/push-notification-templates/welcome-on-signup.handlebars.txt @@ -0,0 +1 @@ +Welcome to {{solidAppName}}, {{name}}. diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 855c9e2f..81921699 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -1279,10 +1279,7 @@ "isSystem": true, "columnName": "type", "selectionValueType": "string", - "selectionStaticValues": [ - "solid:solid", - "custom:custom" - ] + "selectionStaticValues": ["solid:solid", "custom:custom"] }, { "name": "domain", @@ -2139,9 +2136,7 @@ "columnName": "password_scheme", "defaultValue": "bcrypt", "selectionValueType": "string", - "selectionStaticValues": [ - "bcrypt:bcrypt" - ] + "selectionStaticValues": ["bcrypt:bcrypt"] }, { "name": "passwordSchemeVersion", @@ -3021,10 +3016,7 @@ "isSystem": true, "columnName": "type", "selectionValueType": "string", - "selectionStaticValues": [ - "system:System", - "user:User" - ] + "selectionStaticValues": ["system:System", "user:User"] }, { "name": "user", @@ -3387,10 +3379,7 @@ "isSystem": true, "columnName": "type", "selectionValueType": "string", - "selectionStaticValues": [ - "text:Text", - "html:HTML" - ] + "selectionStaticValues": ["text:Text", "html:HTML"] }, { "name": "subject", @@ -3593,10 +3582,7 @@ "isSystem": true, "columnName": "type", "selectionValueType": "string", - "selectionStaticValues": [ - "text:Text", - "html:HTML" - ] + "selectionStaticValues": ["text:Text", "html:HTML"] }, { "name": "smsProviderTemplateId", @@ -3774,10 +3760,7 @@ "index": true, "isSystem": false, "selectionValueType": "string", - "selectionStaticValues": [ - "audit:audit", - "custom:custom" - ] + "selectionStaticValues": ["audit:audit", "custom:custom"] }, { "name": "messageSubType", @@ -3843,10 +3826,7 @@ "type": "mediaMultiple", "ormType": "varchar", "isSystem": false, - "mediaTypes": [ - "image", - "file" - ], + "mediaTypes": ["image", "file"], "mediaMaxSizeKb": 102400, "required": false, "unique": false, @@ -3917,10 +3897,7 @@ "index": false, "isSystem": false, "selectionValueType": "string", - "selectionStaticValues": [ - "pending:pending", - "completed:completed" - ] + "selectionStaticValues": ["pending:pending", "completed:completed"] } ] }, @@ -4070,10 +4047,7 @@ "index": false, "isSystem": true, "selectionValueType": "string", - "selectionStaticValues": [ - "csv:csv", - "excel:excel" - ] + "selectionStaticValues": ["csv:csv", "excel:excel"] }, { "name": "notifyOnEmail", @@ -4625,10 +4599,7 @@ "encrypt": false, "isSystem": true, "selectionValueType": "string", - "selectionStaticValues": [ - "sql:SQL", - "provider:Provider" - ] + "selectionStaticValues": ["sql:SQL", "provider:Provider"] }, { "name": "selectionDynamicSQL", @@ -4747,9 +4718,7 @@ { "modelName": "dashboardVariable", "moduleName": "solid-core", - "operations": [ - "before-insert" - ] + "operations": ["before-insert"] } ], "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", @@ -4800,10 +4769,7 @@ "index": true, "isSystem": true, "selectionValueType": "string", - "selectionStaticValues": [ - "sql:SQL", - "provider:Provider" - ] + "selectionStaticValues": ["sql:SQL", "provider:Provider"] }, { "name": "visualisedAs", @@ -4942,9 +4908,7 @@ { "modelName": "dashboardQuestion", "moduleName": "solid-core", - "operations": [ - "before-insert" - ] + "operations": ["before-insert"] } ], "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", @@ -5093,9 +5057,7 @@ { "modelName": "dashboardQuestionSqlDatasetConfig", "moduleName": "solid-core", - "operations": [ - "before-insert" - ] + "operations": ["before-insert"] } ], "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", @@ -5220,9 +5182,7 @@ { "modelName": "aiInteraction", "moduleName": "solid-core", - "operations": [ - "before-insert" - ] + "operations": ["before-insert"] } ], "computedFieldValueProvider": "AlphaNumExternalIdComputationProvider", @@ -5648,7 +5608,12 @@ "private": false, "encrypt": false, "isSystem": true, - "selectionStaticValues": ["active:Active", "completed:Completed", "failed:Failed", "cancelled:Cancelled"], + "selectionStaticValues": [ + "active:Active", + "completed:Completed", + "failed:Failed", + "cancelled:Cancelled" + ], "selectionValueType": "string" }, { @@ -6143,11 +6108,7 @@ } ] }, - "permissions": [ - "mcp:invoke", - "agent:invoke", - "settings:view_encrypted" - ], + "permissions": ["mcp:invoke", "agent:invoke", "settings:view_encrypted"], "roles": [ { "name": "Admin" @@ -6228,9 +6189,7 @@ "password": "", "forcePasswordChange": true, "fullName": "Default Admin", - "roles": [ - "Admin" - ] + "roles": ["Admin"] } ], "actions": [ @@ -7222,11 +7181,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -7404,11 +7359,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "truncateAfter": 50, "create": true, @@ -7609,11 +7560,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -7805,11 +7752,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -7878,11 +7821,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -8037,11 +7976,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -8171,11 +8106,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -8323,11 +8254,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -8426,11 +8353,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -8645,11 +8568,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -8809,11 +8728,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -9056,11 +8971,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -9265,19 +9176,12 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, "delete": true, - "allowedViews": [ - "list", - "card" - ] + "allowedViews": ["list", "card"] }, "children": [ { @@ -9331,19 +9235,12 @@ "attrs": { "pagination": true, "pageSize": 24, - "pageSizeOptions": [ - 12, - 24, - 48 - ], + "pageSizeOptions": [12, 24, 48], "enableGlobalSearch": true, "create": true, "edit": true, "delete": true, - "allowedViews": [ - "list", - "card" - ] + "allowedViews": ["list", "card"] }, "children": [ { @@ -9491,11 +9388,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -10046,11 +9939,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -10320,11 +10209,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -10396,11 +10281,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -10569,11 +10450,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -10658,11 +10535,7 @@ "type": "tree", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -10754,10 +10627,7 @@ "delete": true, "groupBy": "stage", "draggable": false, - "allowedViews": [ - "list", - "kanban" - ] + "allowedViews": ["list", "kanban"] }, "children": [ { @@ -11113,11 +10983,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -11244,11 +11110,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -11332,11 +11194,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -11636,11 +11494,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -11810,11 +11664,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -11950,11 +11800,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12083,11 +11929,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12215,11 +12057,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12315,11 +12153,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12380,11 +12214,7 @@ "type": "tree", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12522,11 +12352,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12706,11 +12532,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -12855,11 +12677,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -13128,11 +12946,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -13265,11 +13079,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -13370,11 +13180,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": false, "edit": false, @@ -13687,11 +13493,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -13783,11 +13585,7 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], + "pageSizeOptions": [10, 25, 50], "enableGlobalSearch": true, "create": true, "edit": true, @@ -13962,14 +13760,26 @@ "delete": true }, "children": [ - { "type": "field", "attrs": { "name": "sessionId", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "modelName", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "status", "isSearchable": true } }, + { + "type": "field", + "attrs": { "name": "sessionId", "isSearchable": true } + }, + { + "type": "field", + "attrs": { "name": "modelName", "isSearchable": true } + }, + { + "type": "field", + "attrs": { "name": "status", "isSearchable": true } + }, { "type": "field", "attrs": { "name": "totalSteps" } }, { "type": "field", "attrs": { "name": "totalCost" } }, { "type": "field", "attrs": { "name": "totalInputTokens" } }, { "type": "field", "attrs": { "name": "totalOutputTokens" } }, - { "type": "field", "attrs": { "name": "projectRoot", "isSearchable": true } }, + { + "type": "field", + "attrs": { "name": "projectRoot", "isSearchable": true } + }, { "type": "field", "attrs": { "name": "createdAt" } }, { "type": "field", "attrs": { "name": "updatedAt" } } ] @@ -14039,16 +13849,28 @@ }, "children": [ { "type": "field", "attrs": { "name": "id" } }, - { "type": "field", "attrs": { "name": "sessionId", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "eventType", "isSearchable": true } }, + { + "type": "field", + "attrs": { "name": "sessionId", "isSearchable": true } + }, + { + "type": "field", + "attrs": { "name": "eventType", "isSearchable": true } + }, { "type": "field", "attrs": { "name": "turnNumber" } }, { "type": "field", "attrs": { "name": "stepNumber" } }, - { "type": "field", "attrs": { "name": "toolName", "isSearchable": true } }, + { + "type": "field", + "attrs": { "name": "toolName", "isSearchable": true } + }, { "type": "field", "attrs": { "name": "durationMs" } }, { "type": "field", "attrs": { "name": "cost" } }, { "type": "field", "attrs": { "name": "inputTokens" } }, { "type": "field", "attrs": { "name": "outputTokens" } }, - { "type": "field", "attrs": { "name": "modelUsed", "isSearchable": true } }, + { + "type": "field", + "attrs": { "name": "modelUsed", "isSearchable": true } + }, { "type": "field", "attrs": { "name": "createdAt" } } ] } @@ -14415,6 +14237,72 @@ "type": "text" } ], + "pushNotificationTemplates": [ + { + "name": "otp-on-register", + "displayName": "Default: Otp On Register", + "title": "Verify Your Account", + "body": "otp-on-register.handlebars.txt", + "dataTemplate": { + "type": "otp-register", + "otp": "{{otp}}" + }, + "description": "Push template used to send OTP when a user registers.", + "active": true, + "type": "text" + }, + { + "name": "otp-on-login", + "displayName": "Default: Otp On Login", + "title": "Your Login OTP", + "body": "otp-on-login.handlebars.txt", + "dataTemplate": { + "type": "otp-login", + "otp": "{{otp}}" + }, + "description": "Push template used to send OTP when a user logs in.", + "active": true, + "type": "text" + }, + { + "name": "forgot-password", + "displayName": "Default: Forgot Password", + "title": "Reset Password Request", + "body": "forgot-password.handlebars.txt", + "dataTemplate": { + "type": "forgot-password", + "token": "{{token}}" + }, + "description": "Push template used to send reset-password verification token.", + "active": true, + "type": "text" + }, + { + "name": "welcome-on-signup", + "displayName": "Default: Welcome On Signup", + "title": "Welcome to {{solidAppName}}", + "body": "welcome-on-signup.handlebars.txt", + "dataTemplate": { + "type": "welcome-signup" + }, + "description": "Welcome push notification sent to users after signup.", + "active": true, + "type": "text" + }, + { + "name": "new-login-detected", + "displayName": "Default: New Login Detected", + "title": "New Login Detected", + "body": "new-login-detected.handlebars.txt", + "dataTemplate": { + "type": "security-alert", + "device": "{{device}}" + }, + "description": "Push template used when a new login is detected on the account.", + "active": true, + "type": "text" + } + ], "mediaStorageProviders": [ { "name": "default-filesystem", diff --git a/src/services/authentication.service.ts b/src/services/authentication.service.ts index 4d46daa2..e16c348b 100755 --- a/src/services/authentication.service.ts +++ b/src/services/authentication.service.ts @@ -37,7 +37,7 @@ import { RefreshTokenDto } from "../dtos/refresh-token.dto"; import { SignInDto } from "../dtos/sign-in.dto"; import { SignUpDto } from "../dtos/sign-up.dto"; import { User } from "../entities/user.entity"; -import { EventDetails, EventType } from "../interfaces"; +import { EventDetails, EventType, RegisterDevicePayload } from "../interfaces"; import { ActiveUserData } from "../interfaces/active-user-data.interface"; import { HashingService } from "./hashing.service"; import { @@ -52,6 +52,8 @@ import { UserService } from "./user.service"; import { SmsFactory } from "src/factories/sms.factory"; import { WhatsAppFactory } from "src/factories/whatsapp.factory"; import { SolidRegistry } from "src/helpers/solid-registry"; +import { PushNotificationFactory } from "src/factories/push-notification.factory"; +import { DeviceMetadataDto } from "src/dtos/device-metadata.dto"; enum LoginProvider { LOCAL = "local", @@ -80,6 +82,7 @@ export class AuthenticationService { private readonly mailServiceFactory: MailFactory, // private readonly smsService: Msg91OTPService, private readonly smsFactory: SmsFactory, + private readonly pushNotificationFactory: PushNotificationFactory, private readonly whatsAppFactory: WhatsAppFactory, private readonly eventEmitter: EventEmitter2, private readonly settingService: SettingService, @@ -852,6 +855,7 @@ export class AuthenticationService { )); const savedUser: User = await this.userRepository.save(user); + await this.registerDeviceIfProvided(savedUser.id, confirmSignUpDto); this.triggerRegistrationEvent(savedUser); return { active: savedUser.active, @@ -992,6 +996,7 @@ export class AuthenticationService { await this.resetFailedAttempts(user); const tokens = await this.generateTokens(user); + await this.registerDeviceIfProvided(user.id, signInDto); await this.userActivityHistoryService.logEvent("login", user); @@ -1294,6 +1299,7 @@ export class AuthenticationService { } await this.clearLoginOtp(user, type); + await this.registerDeviceIfProvided(user.id, confirmSignInDto); await this.userActivityHistoryService.logEvent("login", user); await this.resetFailedAttempts(user); return this.buildLoginTokenResponse(user); @@ -2072,7 +2078,7 @@ export class AuthenticationService { // // Invalidate the refresh token // // await this.refreshTokenIdsStorage.invalidate(user.id); // } - async logout(refreshToken: string) { + async logout(refreshToken: string, deviceId?: string) { try { // const activeUser = this.requestContextService.getActiveUser(); // const userId = activeUser?.sub; @@ -2094,6 +2100,11 @@ export class AuthenticationService { const userId = payload.sub; await this.refreshTokenIdsStorage.invalidate(userId); + if (deviceId) { + await this.pushNotificationFactory + .getPushNotificationService() + .unregisterDevice(userId, deviceId); + } const user = await this.userRepository.findOne({ where: { id: userId, @@ -2182,6 +2193,29 @@ export class AuthenticationService { } return { accessToken, refreshToken, user: this.buildUserPayload(user) }; } + + private async registerDeviceIfProvided( + userId: number, + dto: DeviceMetadataDto, + ): Promise { + if (!dto?.deviceId || !dto?.deviceToken || !dto?.platform) { + return; + } + + await this.pushNotificationFactory + .getPushNotificationService() + .registerDevice({ + userId, + deviceId: dto.deviceId, + deviceToken: dto.deviceToken, + platform: dto.platform, + deviceName: dto.deviceName, + deviceType: dto.deviceType, + osName: dto.osName, + osVersion: dto.osVersion, + appVersion: dto.appVersion, + }); + } } function parseUniqueConstraintError(detail: string): string { diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index ad8c75ae..7f46699e 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -189,6 +189,7 @@ export class ModuleMetadataService { views: [], emailTemplates: [], smsTemplates: [], + pushNotificationTemplates: [], mediaStorageProviders: [], securityRules: [], }; @@ -276,6 +277,7 @@ export class ModuleMetadataService { views: [], emailTemplates: [], smsTemplates: [], + pushNotificationTemplates: [], mediaStorageProviders: [], }; } diff --git a/src/services/push-notification-template.service.ts b/src/services/push-notification-template.service.ts new file mode 100644 index 00000000..09e6d169 --- /dev/null +++ b/src/services/push-notification-template.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from "@nestjs/common"; +import { ModuleRef } from "@nestjs/core"; +import { InjectEntityManager } from "@nestjs/typeorm"; +import { EntityManager } from "typeorm"; +import { CRUDService } from "./crud.service"; +import { PushNotificationTemplate } from "src/entities/push-notification-template.entity"; +import { PushNotificationTemplateRepository } from "src/repository/push-notification-template.repository"; +import { MediaStorageProviderMetadataService } from "./media-storage-provider-metadata.service"; +import { MediaService } from "./media.service"; +import { CreatePushNotificationTemplateDto } from "src/dtos/create-push-notification-template.dto"; + +@Injectable() +export class PushNotificationTemplateService extends CRUDService { + constructor( + readonly mediaStorageProviderService: MediaStorageProviderMetadataService, + readonly mediaService: MediaService, + @InjectEntityManager() + readonly entityManager: EntityManager, + readonly repo: PushNotificationTemplateRepository, + readonly moduleRef: ModuleRef, + ) { + super( + entityManager, + repo, + "pushNotificationTemplate", + "app-builder", + moduleRef, + ); + } + + async findOneByName(name: string): Promise { + return this.repo.findOne({ + where: { name }, + }); + } + + async removeByName(name: string): Promise { + const existing = await this.findOneByName(name); + if (existing) { + await this.repo.remove(existing); + } + } + + // Seeder-safe create path that does not require app-builder model metadata. + async createFromSeed( + dto: CreatePushNotificationTemplateDto, + ): Promise { + return this.repo.save(this.repo.create(dto as PushNotificationTemplate)); + } +} diff --git a/src/services/push/amazon-sns-push-notification.service.ts b/src/services/push/amazon-sns-push-notification.service.ts new file mode 100644 index 00000000..23487a85 --- /dev/null +++ b/src/services/push/amazon-sns-push-notification.service.ts @@ -0,0 +1,400 @@ +import { Injectable, Logger } from "@nestjs/common"; +import Handlebars from "handlebars"; +import { + CreatePlatformEndpointCommand, + GetEndpointAttributesCommand, + PublishCommandOutput, + PublishCommand, + SetEndpointAttributesCommand, + SNSClient, +} from "@aws-sdk/client-sns"; +import { PushNotificationProvider } from "src/decorators/push-notification-provider.decorator"; +import { + IPushNotification, + RegisterDevicePayload, + PushNotificationPayload, + PushNotificationQueuePayload, +} from "src/interfaces"; +import { QueueMessage } from "src/interfaces/mq"; +import { PublisherFactory } from "../queues/publisher-factory.service"; +import { SettingService } from "../setting.service"; +import type { SolidCoreSetting } from "../settings/default-settings-provider.service"; +import { UserDeviceMetadataService } from "../user-device-metadata.service"; +import { DeviceMetadataDto } from "src/dtos/device-metadata.dto"; +import { PushNotificationTemplateService } from "../push-notification-template.service"; + +@Injectable() +@PushNotificationProvider() +export class AmazonSNSPushNotificationService implements IPushNotification { + private readonly logger = new Logger(AmazonSNSPushNotificationService.name); + private snsClient: SNSClient; + + private getSNSClient(): SNSClient { + if (!this.snsClient) { + this.snsClient = new SNSClient({ + region: + this.settingService.getConfigValue("awsSnsRegion"), + + credentials: { + accessKeyId: + this.settingService.getConfigValue( + "awsSnsAccessKeyId", + ), + + secretAccessKey: this.settingService.getConfigValue( + "awsSnsSecretAccessKey", + ), + }, + }); + } + + return this.snsClient; + } + + constructor( + private readonly publisherFactory: PublisherFactory, + private readonly settingService: SettingService, + private readonly userDeviceMetadataService: UserDeviceMetadataService, + private readonly pushNotificationTemplateService: PushNotificationTemplateService, + ) {} + + async sendPushNotification( + endpointArn: string, + payload: PushNotificationPayload, + shouldQueuePush = false, + ): Promise { + const message: QueueMessage = { + payload: { + endpointArn, + payload, + }, + }; + + if (shouldQueuePush === true) { + return this.sendPushNotificationAsynchronously(message); + } + + if ( + shouldQueuePush === false && + this.settingService.getConfigValue( + "shouldQueuePush", + ) === true + ) { + return this.sendPushNotificationAsynchronously(message); + } + + return this.sendPushNotificationSynchronously(message.payload); + } + + private async sendPushNotificationAsynchronously( + message: QueueMessage, + ): Promise { + this.logger.debug( + `Queueing SNS push notification for endpoint ${message.payload.endpointArn}`, + ); + + return this.publisherFactory.publish( + message, + "AmazonSnsPushNotificationQueuePublisher", + ); + } + + async sendPushNotificationSynchronously( + message: PushNotificationQueuePayload, + ): Promise { + const { endpointArn, payload } = message; + if (!endpointArn) { + throw new Error("endpointArn is required for push notification."); + } + + const command = new PublishCommand({ + TargetArn: endpointArn, + Message: JSON.stringify(this.buildPlatformPayload(payload)), + MessageStructure: "json", + }); + + const response = await this.getSNSClient().send(command); + this.logger.debug( + `SNS push notification sent successfully. MessageId=${response.MessageId}`, + ); + return response; + } + + async sendPushNotificationUsingTemplate( + endpointArn: string, + templateName: string, + templateParams: any, + shouldQueuePush = false, + ): Promise { + const pushNotificationTemplate = + await this.pushNotificationTemplateService.findOneByName(templateName); + if (!pushNotificationTemplate) { + throw new Error(`Invalid template name ${templateName}`); + } + if (pushNotificationTemplate.active === false) { + throw new Error(`Template '${templateName}' is inactive`); + } + + const titleTemplate = Handlebars.compile( + pushNotificationTemplate.title || "", + ); + const bodyTemplate = Handlebars.compile( + pushNotificationTemplate.body || "", + ); + + const payload: PushNotificationPayload = { + title: titleTemplate(templateParams), + body: bodyTemplate(templateParams), + }; + + if (pushNotificationTemplate.dataTemplate) { + payload.data = Object.entries( + pushNotificationTemplate.dataTemplate, + ).reduce>((acc, [key, templateValue]) => { + const compiled = Handlebars.compile(templateValue || ""); + acc[key] = compiled(templateParams); + return acc; + }, {}); + } + + return this.sendPushNotification(endpointArn, payload, shouldQueuePush); + } + + async registerDevice(payload: RegisterDevicePayload): Promise { + const { userId, deviceId, deviceToken, platform } = payload; + const platformApplicationArn = this.resolvePlatformApplicationArn(platform); + const existingDevice = + await this.userDeviceMetadataService.findActiveDevice(userId, deviceId); + const endpointArn = await this.createOrUpdateEndpoint( + platformApplicationArn, + deviceToken, + existingDevice?.pushEndpointArn, + ); + + await this.userDeviceMetadataService.upsertDeviceForUser({ + userId, + deviceId, + deviceToken, + platform, + endpointArn, + osName: payload.osName, + osVersion: payload.osVersion, + appVersion: payload.appVersion, + deviceName: payload.deviceName, + deviceType: payload.deviceType, + }); + + return endpointArn; + } + + async unregisterDevice(userId: number, deviceId: string): Promise { + const device = await this.userDeviceMetadataService.findActiveDevice( + userId, + deviceId, + ); + + if (!device) { + return; + } + + if (device.pushEndpointArn) { + try { + await this.getSNSClient().send( + new SetEndpointAttributesCommand({ + EndpointArn: device.pushEndpointArn, + Attributes: { + Enabled: "false", + }, + }), + ); + } catch (error) { + this.logger.warn( + `Failed to disable SNS endpoint ${device.pushEndpointArn}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + await this.userDeviceMetadataService.markDeviceInactive(userId, deviceId); + } + + private async createOrUpdateEndpoint( + platformApplicationArn: string, + deviceToken: string, + existingEndpointArn?: string, + ): Promise { + if (existingEndpointArn) { + await this.getSNSClient().send( + new SetEndpointAttributesCommand({ + EndpointArn: existingEndpointArn, + Attributes: { + Token: deviceToken, + Enabled: "true", + }, + }), + ); + + return existingEndpointArn; + } + + const createResult = await this.getSNSClient().send( + new CreatePlatformEndpointCommand({ + PlatformApplicationArn: platformApplicationArn, + Token: deviceToken, + }), + ); + + const endpointArn = createResult.EndpointArn; + if (!endpointArn) { + throw new Error("SNS endpoint ARN was not returned."); + } + + await this.getSNSClient().send( + new SetEndpointAttributesCommand({ + EndpointArn: endpointArn, + Attributes: { + Token: deviceToken, + Enabled: "true", + }, + }), + ); + + await this.getSNSClient().send( + new GetEndpointAttributesCommand({ + EndpointArn: endpointArn, + }), + ); + + return endpointArn; + } + + private resolvePlatformApplicationArn(platform: string): string { + const normalized = (platform || "").trim().toLowerCase(); + + switch (normalized) { + case "android": { + const arn = + this.settingService.getConfigValue( + "awsFcmPlatformArn", + ); + if (!arn) { + throw new Error("Missing setting: awsFcmPlatformArn"); + } + return arn; + } + + case "ios": { + const arn = + this.settingService.getConfigValue( + "awsApnsPlatformArn", + ); + if (!arn) { + throw new Error("Missing setting: awsApnsPlatformArn"); + } + return arn; + } + + case "web": + case "windows": + case "linux": + case "macos": + throw new Error( + `Push notifications are not configured for platform: ${normalized}`, + ); + + default: + throw new Error(`Invalid platform received: ${platform}`); + } + } + + private buildPlatformPayload( + payload: PushNotificationPayload, + ): Record { + const data = payload.data ?? {}; + + const apnsPayload = { + aps: { + alert: { + title: payload.title, + body: payload.body, + }, + sound: "default", + }, + data, + }; + + const gcmPayload = { + notification: { + title: payload.title, + body: payload.body, + }, + data, + }; + + return { + default: payload.body, + APNS: JSON.stringify(apnsPayload), + APNS_SANDBOX: JSON.stringify(apnsPayload), + GCM: JSON.stringify(gcmPayload), + }; + } + + async testPushNotification(dto: DeviceMetadataDto) { + if (!dto.userId) { + throw new Error("userId is required"); + } + const registerPayload: RegisterDevicePayload = { + userId: dto.userId, + deviceId: dto.deviceId || "unknown-device", + deviceToken: dto.deviceToken, + platform: dto.platform || "unknown", + deviceName: dto.deviceName || "unknown", + deviceType: dto.deviceType, + osName: dto.osName, + osVersion: dto.osVersion, + appVersion: dto.appVersion, + }; + + await this.registerDevice(registerPayload); + + const endpointArn = + await this.userDeviceMetadataService.resolveEndpointArnForUserDevice( + registerPayload.userId, + registerPayload.deviceId, + ); + + if (!endpointArn) { + throw new Error("Unable to resolve endpoint ARN"); + } + + // Payload Structure + // const notificationPayload: PushNotificationPayload = { + // title: "SNS Test", + // body: "Push working successfully", + // data: { + // source: "solidx-core-module", + // type: "test", + // }, + // }; + + const templateParams = { + device: + dto.deviceName || + dto.deviceType || + `${dto.osName || "Unknown"} ${dto.osVersion || ""}`, + }; + + const response = await this.sendPushNotificationUsingTemplate( + endpointArn, + "new-login-detected", + templateParams, + false, + ); + + return { + success: true, + messageId: typeof response === "string" ? response : response?.MessageId, + }; + } +} diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index bdb7d4f3..204b644c 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -1143,6 +1143,60 @@ const getSolidCoreSettings = (isProd: boolean) => controlType: "shortText", }, + // push-notification-settings-provider.service.ts + { + moduleName: "solid-core", + key: "pushNotificationProvider", + value: + process.env.COMMON_PUSH_NOTIFICATION_PROVIDER ?? + "AmazonSNSPushNotificationService", + level: SettingLevel.SystemAdminReadonly, + label: "Push Notification Provider", + group: "push-settings", + sortOrder: 10, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "shouldQueuePush", + value: (process.env.COMMON_PUSH_SHOULD_QUEUE ?? "false") === "true", + level: SettingLevel.SystemAdminReadonly, + label: "Queue Push Notifications", + group: "push-settings", + sortOrder: 20, + controlType: "boolean", + }, + { + moduleName: "solid-core", + key: "awsSnsAccessKeyId", + value: process.env.AWS_SNS_ACCESS_KEY_ID, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "awsSnsSecretAccessKey", + value: process.env.AWS_SNS_SECRET_ACCESS_KEY, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "awsSnsRegion", + value: process.env.AWS_SNS_REGION, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "awsFcmPlatformArn", + value: process.env.AWS_FCM_PLATFORM_ARN, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "awsApnsPlatformArn", + value: process.env.AWS_APNS_PLATFORM_ARN, + level: SettingLevel.SystemEnv, + }, + // tiny-url-settings-provider.service.ts { moduleName: "solid-core", diff --git a/src/services/solid-introspect.service.ts b/src/services/solid-introspect.service.ts index a58b8b20..5b192f62 100755 --- a/src/services/solid-introspect.service.ts +++ b/src/services/solid-introspect.service.ts @@ -25,6 +25,7 @@ import { DataSource } from 'typeorm'; import { CRUDService } from './crud.service'; import { IS_SETTINGS_PROVIDER } from 'src/decorators/settings-provider.decorator'; import { IS_SMS_PROVIDER } from 'src/decorators/sms-provider.decorator'; +import { IS_PUSH_NOTIFICATION_PROVIDER } from 'src/decorators/push-notification-provider.decorator'; import { SettingService } from 'src/services/setting.service'; export const coreSubscriberClasses = [ @@ -139,6 +140,12 @@ export class SolidIntrospectService implements OnApplicationBootstrap { this.solidRegistry.registerSmsProvider(smsProvider); }); + // Register all IPushNotification implementations + const pushNotificationProviders = this.discoveryService.getProviders().filter((provider) => this.isPushNotificationProvider(provider)); + pushNotificationProviders.forEach((pushNotificationProvider) => { + this.solidRegistry.registerPushNotificationProvider(pushNotificationProvider); + }); + // Register all ISecurityRuleConfigProvider implementations const securityRuleConfigProviders = this.discoveryService.getProviders().filter((provider) => this.isSecurityRuleConfigProvider(provider)); securityRuleConfigProviders.forEach((securityRuleConfigProvider) => { @@ -394,6 +401,18 @@ export class SolidIntrospectService implements OnApplicationBootstrap { return !!isSmsProvider; } + private isPushNotificationProvider(provider: InstanceWrapper) { + const { instance } = provider; + if (!instance) return false; + + const isPushNotificationProvider = this.reflector.get( + IS_PUSH_NOTIFICATION_PROVIDER, + instance.constructor, + ); + + return !!isPushNotificationProvider; + } + private isSecurityRuleConfigProvider(provider: InstanceWrapper) { const { instance } = provider; if (!instance) return false; diff --git a/src/services/user-device-metadata.service.ts b/src/services/user-device-metadata.service.ts new file mode 100644 index 00000000..a10b1837 --- /dev/null +++ b/src/services/user-device-metadata.service.ts @@ -0,0 +1,106 @@ +import { Injectable } from "@nestjs/common"; +import { UserDeviceMetadata } from "src/entities/user-device-metadata.entity"; +import { UserDeviceMetadataRepository } from "src/repository/user-device-metadata.repository"; + +export interface UpsertUserDeviceInput { + userId: number; + deviceId: string; + deviceToken: string; + platform: string; + endpointArn?: string; + osName?: string; + osVersion?: string; + appVersion?: string; + deviceType?: string; + deviceName?: string; +} + +@Injectable() +export class UserDeviceMetadataService { + constructor( + private readonly userDeviceMetadataRepository: UserDeviceMetadataRepository, + ) {} + + async upsertDeviceForUser( + payload: UpsertUserDeviceInput, + ): Promise { + const now = new Date(); + const existing = await this.userDeviceMetadataRepository.findOne({ + where: { + user: { id: payload.userId }, + deviceId: payload.deviceId, + }, + }); + + if (existing) { + existing.pushDeviceToken = payload.deviceToken; + existing.platform = payload.platform; + existing.pushEndpointArn = payload.endpointArn; + existing.osName = payload.osName; + existing.osVersion = payload.osVersion; + existing.appVersion = payload.appVersion; + existing.deviceName = payload.deviceName; + existing.deviceType = payload.deviceType; + existing.isActive = true; + existing.lastActiveAt = now; + existing.pushTokenUpdatedAt = now; + return this.userDeviceMetadataRepository.save(existing); + } + + return this.userDeviceMetadataRepository.save( + this.userDeviceMetadataRepository.create({ + user: { id: payload.userId } as any, + deviceId: payload.deviceId, + pushDeviceToken: payload.deviceToken, + pushEndpointArn: payload.endpointArn, + platform: payload.platform, + osName: payload.osName, + osVersion: payload.osVersion, + appVersion: payload.appVersion, + deviceName: payload.deviceName, + deviceType: payload.deviceType, + isActive: true, + lastActiveAt: now, + pushTokenUpdatedAt: now, + }), + ); + } + + async findActiveDevice( + userId: number, + deviceId: string, + ): Promise { + return this.userDeviceMetadataRepository.findOne({ + where: { user: { id: userId }, deviceId, isActive: true }, + }); + } + + async markDeviceInactive(userId: number, deviceId: string): Promise { + const existing = await this.findActiveDevice(userId, deviceId); + + if (!existing) { + return; + } + + existing.isActive = false; + existing.lastActiveAt = new Date(); + + await this.userDeviceMetadataRepository.save(existing); + } + + async resolveEndpointArnForUserDevice( + userId: number, + deviceId: string, + ): Promise { + const activeDevice = await this.findActiveDevice(userId, deviceId); + return activeDevice?.pushEndpointArn ?? null; + } + + async findActiveDevicesByUserId( + userId: number, + ): Promise { + return this.userDeviceMetadataRepository.find({ + where: { user: { id: userId }, isActive: true }, + }); + } +} diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index a2a640d5..08328a63 100755 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -79,6 +79,7 @@ import { MenuItemMetadataController } from "./controllers/menu-item-metadata.con import { MqMessageQueueController } from "./controllers/mq-message-queue.controller"; import { MqMessageController } from "./controllers/mq-message.controller"; import { OTPAuthenticationController } from "./controllers/otp-authentication.controller"; +import { PushNotificationTemplateController } from "./controllers/push-notification-template.controller"; import { ServiceController } from "./controllers/service.controller"; import { SmsTemplateController } from "./controllers/sms-template.controller"; import { TestQueueController } from "./controllers/test-queue.controller"; @@ -88,6 +89,7 @@ import { MenuItemMetadata } from "./entities/menu-item-metadata.entity"; import { MqMessageQueue } from "./entities/mq-message-queue.entity"; import { MqMessage } from "./entities/mq-message.entity"; import { SmsTemplate } from "./entities/sms-template.entity"; +import { PushNotificationTemplate } from "./entities/push-notification-template.entity"; import { AccessTokenGuard } from "./guards/access-token.guard"; import { ApiKeyGuard } from "./guards/api-key.guard"; import { AuthenticationGuard } from "./guards/authentication.guard"; @@ -164,6 +166,7 @@ import { SsoCodeStorageService } from "./services/sso-code-storage.service"; import { ListOfModelsSelectionProvider } from "./services/selection-providers/list-of-models-selection-provider.service"; import { TinyUrlService } from "./services/short-url/tiny-url.service"; import { SmsTemplateService } from "./services/sms-template.service"; +import { PushNotificationTemplateService } from "./services/push-notification-template.service"; import { Msg91OTPService } from "./services/sms/Msg91OTPService"; import { Msg91SMSService } from "./services/sms/Msg91SMSService"; // import { UserService } from './services/user.service'; @@ -184,67 +187,69 @@ import { DashboardQuestionController } from "./controllers/dashboard-question.co import { DashboardVariableController } from "./controllers/dashboard-variable.controller"; import { DashboardLayoutController } from "./controllers/dashboard-layout.controller"; -import { DashboardController } from './controllers/dashboard.controller'; -import { ExportTemplateController } from './controllers/export-template.controller'; -import { ExportTransactionController } from './controllers/export-transaction.controller'; -import { ImportTransactionErrorLogController } from './controllers/import-transaction-error-log.controller'; -import { ImportTransactionController } from './controllers/import-transaction.controller'; -import { ListOfValuesController } from './controllers/list-of-values.controller'; -import { LocaleController } from './controllers/locale.controller'; -import { RoleMetadataController } from './controllers/role-metadata.controller'; -import { SavedFiltersController } from './controllers/saved-filters.controller'; -import { ScheduledJobController } from './controllers/scheduled-job.controller'; -import { AgentSessionController } from './controllers/agent-session.controller'; -import { AgentEventController } from './controllers/agent-event.controller'; -import { McpAuditLogController } from './controllers/mcp-audit-log.controller'; -import { SecurityRuleController } from './controllers/security-rule.controller'; -import { SettingController } from './controllers/setting.controller'; -import { InfoController } from './controllers/info.controller'; -import { InfoService } from './services/info.service'; -import { UserActivityHistoryController } from './controllers/user-activity-history.controller'; -import { UserViewMetadataController } from './controllers/user-view-metadata.controller'; -import { UserController } from './controllers/user.controller'; -import { AiInteraction } from './entities/ai-interaction.entity'; -import { ChatterMessageDetails } from './entities/chatter-message-details.entity'; -import { ChatterMessage } from './entities/chatter-message.entity'; -import { DashboardQuestionSqlDatasetConfig } from './entities/dashboard-question-sql-dataset-config.entity'; -import { DashboardQuestion } from './entities/dashboard-question.entity'; -import { DashboardVariable } from './entities/dashboard-variable.entity'; -import { DashboardLayout } from './entities/dashboard-layout.entity'; +import { DashboardController } from "./controllers/dashboard.controller"; +import { ExportTemplateController } from "./controllers/export-template.controller"; +import { ExportTransactionController } from "./controllers/export-transaction.controller"; +import { ImportTransactionErrorLogController } from "./controllers/import-transaction-error-log.controller"; +import { ImportTransactionController } from "./controllers/import-transaction.controller"; +import { ListOfValuesController } from "./controllers/list-of-values.controller"; +import { LocaleController } from "./controllers/locale.controller"; +import { RoleMetadataController } from "./controllers/role-metadata.controller"; +import { SavedFiltersController } from "./controllers/saved-filters.controller"; +import { ScheduledJobController } from "./controllers/scheduled-job.controller"; +import { AgentSessionController } from "./controllers/agent-session.controller"; +import { AgentEventController } from "./controllers/agent-event.controller"; +import { SecurityRuleController } from "./controllers/security-rule.controller"; +import { SettingController } from "./controllers/setting.controller"; +import { InfoController } from "./controllers/info.controller"; +import { InfoService } from "./services/info.service"; +import { UserActivityHistoryController } from "./controllers/user-activity-history.controller"; +import { UserViewMetadataController } from "./controllers/user-view-metadata.controller"; +import { UserController } from "./controllers/user.controller"; +import { AiInteraction } from "./entities/ai-interaction.entity"; +import { ChatterMessageDetails } from "./entities/chatter-message-details.entity"; +import { ChatterMessage } from "./entities/chatter-message.entity"; +import { DashboardQuestionSqlDatasetConfig } from "./entities/dashboard-question-sql-dataset-config.entity"; +import { DashboardQuestion } from "./entities/dashboard-question.entity"; +import { DashboardVariable } from "./entities/dashboard-variable.entity"; +import { DashboardLayout } from "./entities/dashboard-layout.entity"; -import { Dashboard } from './entities/dashboard.entity'; -import { ExportTemplate } from './entities/export-template.entity'; -import { ExportTransaction } from './entities/export-transaction.entity'; -import { ImportTransactionErrorLog } from './entities/import-transaction-error-log.entity'; -import { ImportTransaction } from './entities/import-transaction.entity'; -import { Locale } from './entities/locale.entity'; -import { RoleMetadata } from './entities/role-metadata.entity'; -import { SavedFilters } from './entities/saved-filters.entity'; -import { ScheduledJob } from './entities/scheduled-job.entity'; -import { AgentSession } from './entities/agent-session.entity'; -import { AgentEvent } from './entities/agent-event.entity'; -import { McpAuditLog } from './entities/mcp-audit-log.entity'; -import { SecurityRule } from './entities/security-rule.entity'; -import { Setting } from './entities/setting.entity'; -import { UserActivityHistory } from './entities/user-activity-history.entity'; -import { UserViewMetadata } from './entities/user-view-metadata.entity'; -import { UserApiKey } from './entities/user-api-key.entity'; -import { User } from './entities/user.entity'; -import { HttpExceptionFilter } from './filters/http-exception.filter'; -import { ModelMetadataHelperService } from './helpers/model-metadata-helper.service'; -import { ModuleMetadataHelperService } from './helpers/module-metadata-helper.service'; -import { ApiEmailQueuePublisherDatabase } from './jobs/database/api-email-publisher-database.service'; -import { ApiEmailQueueSubscriberDatabase } from './jobs/database/api-email-subscriber-database.service'; -import { ComputedFieldEvaluationPublisherDatabase } from './jobs/database/computed-field-evaluation-publisher-database.service'; -import { ComputedFieldEvaluationSubscriberDatabase } from './jobs/database/computed-field-evaluation-subscriber-database.service'; -import { GenerateCodePublisherDatabase } from './jobs/database/generate-code-publisher-database.service'; -import { GenerateCodeSubscriberDatabase } from './jobs/database/generate-code-subscriber-database.service'; -import { OTPQueuePublisherDatabase } from './jobs/database/otp-publisher-database.service'; -import { OTPQueueSubscriberDatabase } from './jobs/database/otp-subscriber-database.service'; -import { Msg91SmsQueuePublisherDatabase } from './jobs/database/msg91-sms-publisher-database.service'; -import { Msg91SmsQueueSubscriberDatabase } from './jobs/database/msg91-sms-subscriber-database.service'; -import { SmtpEmailQueuePublisherDatabase } from './jobs/database/smtp-email-publisher-database.service'; -import { SmtpEmailQueueSubscriberDatabase } from './jobs/database/smtp-email-subscriber-database.service'; +import { Dashboard } from "./entities/dashboard.entity"; +import { ExportTemplate } from "./entities/export-template.entity"; +import { ExportTransaction } from "./entities/export-transaction.entity"; +import { ImportTransactionErrorLog } from "./entities/import-transaction-error-log.entity"; +import { ImportTransaction } from "./entities/import-transaction.entity"; +import { Locale } from "./entities/locale.entity"; +import { RoleMetadata } from "./entities/role-metadata.entity"; +import { SavedFilters } from "./entities/saved-filters.entity"; +import { ScheduledJob } from "./entities/scheduled-job.entity"; +import { AgentSession } from "./entities/agent-session.entity"; +import { AgentEvent } from "./entities/agent-event.entity"; +import { SecurityRule } from "./entities/security-rule.entity"; +import { Setting } from "./entities/setting.entity"; +import { UserActivityHistory } from "./entities/user-activity-history.entity"; +import { UserViewMetadata } from "./entities/user-view-metadata.entity"; +import { UserApiKey } from "./entities/user-api-key.entity"; +import { User } from "./entities/user.entity"; +import { HttpExceptionFilter } from "./filters/http-exception.filter"; +import { ModelMetadataHelperService } from "./helpers/model-metadata-helper.service"; +import { ModuleMetadataHelperService } from "./helpers/module-metadata-helper.service"; +import { ApiEmailQueuePublisherDatabase } from "./jobs/database/api-email-publisher-database.service"; +import { ApiEmailQueueSubscriberDatabase } from "./jobs/database/api-email-subscriber-database.service"; +import { ComputedFieldEvaluationPublisherDatabase } from "./jobs/database/computed-field-evaluation-publisher-database.service"; +import { ComputedFieldEvaluationSubscriberDatabase } from "./jobs/database/computed-field-evaluation-subscriber-database.service"; +import { GenerateCodePublisherDatabase } from "./jobs/database/generate-code-publisher-database.service"; +import { GenerateCodeSubscriberDatabase } from "./jobs/database/generate-code-subscriber-database.service"; +import { OTPQueuePublisherDatabase } from "./jobs/database/otp-publisher-database.service"; +import { OTPQueueSubscriberDatabase } from "./jobs/database/otp-subscriber-database.service"; +import { Msg91SmsQueuePublisherDatabase } from "./jobs/database/msg91-sms-publisher-database.service"; +import { Msg91SmsQueueSubscriberDatabase } from "./jobs/database/msg91-sms-subscriber-database.service"; +import { SmtpEmailQueuePublisherDatabase } from "./jobs/database/smtp-email-publisher-database.service"; +import { SmtpEmailQueueSubscriberDatabase } from "./jobs/database/smtp-email-subscriber-database.service"; + +import { McpAuditLogController } from "./controllers/mcp-audit-log.controller"; +import { McpAuditLog } from "./entities/mcp-audit-log.entity"; +import { UserDeviceMetadata } from "./entities/user-device-metadata.entity"; import { TwilioSmsQueuePublisherDatabase } from "./jobs/database/twilio-sms-publisher-database.service"; import { TwilioSmsQueueSubscriberDatabase } from "./jobs/database/twilio-sms-subscriber-database.service"; @@ -252,6 +257,7 @@ import { TwilioSmsQueueSubscriberDatabase } from "./jobs/database/twilio-sms-sub // import { ThrottlerModule } from '@nestjs/throttler'; import { IngestCommand } from "./commands/ingest.command"; import { MailFactory } from "./factories/mail.factory"; +import { PushNotificationFactory } from "./factories/push-notification.factory"; import { ErrorMapperService } from "./helpers/error-mapper.service"; import { SolidCoreErrorCodesProvider } from "./helpers/solid-core-error-codes-provider.service"; import { ComputedFieldEvaluationPublisherRabbitmq } from "./jobs/rabbitmq/computed-field-evaluation-publisher.service"; @@ -282,124 +288,133 @@ import { DashboardVariableRepository } from "./repository/dashboard-variable.rep import { DashboardRepository } from "./repository/dashboard.repository"; import { DashboardLayoutRepository } from "./repository/dashboard-layout.repository"; -import { EmailTemplateRepository } from './repository/email-template.repository'; -import { ExportTemplateRepository } from './repository/export-template.repository'; -import { ExportTransactionRepository } from './repository/export-transaction.repository'; -import { FieldMetadataRepository } from './repository/field-metadata.repository'; -import { ImportTransactionErrorLogRepository } from './repository/import-transaction-error-log.repository'; -import { ImportTransactionRepository } from './repository/import-transaction.repository'; -import { ListOfValuesRepository } from './repository/list-of-values.repository'; -import { LocaleRepository } from './repository/locale.repository'; -import { MediaRepository } from './repository/media.repository'; -import { MenuItemMetadataRepository } from './repository/menu-item-metadata.repository'; -import { ModelMetadataRepository } from './repository/model-metadata.repository'; -import { ModuleMetadataRepository } from './repository/module-metadata.repository'; -import { MqMessageQueueRepository } from './repository/mq-message-queue.repository'; -import { MqMessageRepository } from './repository/mq-message.repository'; -import { PermissionMetadataRepository } from './repository/permission-metadata.repository'; -import { RoleMetadataRepository } from './repository/role-metadata.repository'; -import { SavedFiltersRepository } from './repository/saved-filters.repository'; -import { ScheduledJobRepository } from './repository/scheduled-job.repository'; -import { AgentSessionRepository } from './repository/agent-session.repository'; -import { AgentEventRepository } from './repository/agent-event.repository'; -import { McpAuditLogRepository } from './repository/mcp-audit-log.repository'; -import { SecurityRuleRepository } from './repository/security-rule.repository'; -import { SettingRepository } from './repository/setting.repository'; -import { SmsTemplateRepository } from './repository/sms-template.repository'; -import { UserActivityHistoryRepository } from './repository/user-activity-history.repository'; -import { UserViewMetadataRepository } from './repository/user-view-metadata.repository'; -import { UserApiKeyRepository } from './repository/user-api-key.repository'; -import { UserRepository } from './repository/user.repository'; -import { ViewMetadataRepository } from './repository/view-metadata.repository'; -import { PermissionMetadataSeederService } from './seeders/permission-metadata-seeder.service'; -import { SystemFieldsSeederService } from './seeders/system-fields-seeder.service'; -import { AiInteractionService } from './services/ai-interaction.service'; -import { ChatterMessageDetailsService } from './services/chatter-message-details.service'; -import { ChatterMessageService } from './services/chatter-message.service'; -import { ConcatComputedFieldProvider } from './services/computed-fields/concat-computed-field-provider.service'; -import { AlphaNumExternalIdComputationProvider } from './services/computed-fields/entity/alpha-num-external-id-computed-field-provider'; -import { ConcatEntityComputedFieldProvider } from './services/computed-fields/entity/concat-entity-computed-field-provider.service'; -import { NoopsEntityComputedFieldProviderService } from './services/computed-fields/entity/noops-entity-computed-field-provider.service'; -import { CRUDService } from './services/crud.service'; -import { CsvService } from './services/csv.service'; -import { DashboardQuestionSqlDatasetConfigService } from './services/dashboard-question-sql-dataset-config.service'; -import { DashboardQuestionService } from './services/dashboard-question.service'; -import { DashboardVariableSQLDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service'; -import { DasbhoardVariableTestDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service'; -import { DashboardVariableService } from './services/dashboard-variable.service'; -import { DashboardService } from './services/dashboard.service'; -import { DashboardLayoutService } from './services/dashboard-layout.service'; +import { EmailTemplateRepository } from "./repository/email-template.repository"; +import { ExportTemplateRepository } from "./repository/export-template.repository"; +import { ExportTransactionRepository } from "./repository/export-transaction.repository"; +import { FieldMetadataRepository } from "./repository/field-metadata.repository"; +import { ImportTransactionErrorLogRepository } from "./repository/import-transaction-error-log.repository"; +import { ImportTransactionRepository } from "./repository/import-transaction.repository"; +import { ListOfValuesRepository } from "./repository/list-of-values.repository"; +import { LocaleRepository } from "./repository/locale.repository"; +import { MediaRepository } from "./repository/media.repository"; +import { MenuItemMetadataRepository } from "./repository/menu-item-metadata.repository"; +import { ModelMetadataRepository } from "./repository/model-metadata.repository"; +import { ModuleMetadataRepository } from "./repository/module-metadata.repository"; +import { MqMessageQueueRepository } from "./repository/mq-message-queue.repository"; +import { MqMessageRepository } from "./repository/mq-message.repository"; +import { PermissionMetadataRepository } from "./repository/permission-metadata.repository"; +import { RoleMetadataRepository } from "./repository/role-metadata.repository"; +import { SavedFiltersRepository } from "./repository/saved-filters.repository"; +import { ScheduledJobRepository } from "./repository/scheduled-job.repository"; +import { AgentSessionRepository } from "./repository/agent-session.repository"; +import { AgentEventRepository } from "./repository/agent-event.repository"; +import { SecurityRuleRepository } from "./repository/security-rule.repository"; +import { SettingRepository } from "./repository/setting.repository"; +import { SmsTemplateRepository } from "./repository/sms-template.repository"; +import { PushNotificationTemplateRepository } from "./repository/push-notification-template.repository"; +import { UserActivityHistoryRepository } from "./repository/user-activity-history.repository"; +import { UserViewMetadataRepository } from "./repository/user-view-metadata.repository"; +import { UserApiKeyRepository } from "./repository/user-api-key.repository"; +import { UserDeviceMetadataRepository } from "./repository/user-device-metadata.repository"; +import { UserRepository } from "./repository/user.repository"; +import { ViewMetadataRepository } from "./repository/view-metadata.repository"; +import { PermissionMetadataSeederService } from "./seeders/permission-metadata-seeder.service"; +import { SystemFieldsSeederService } from "./seeders/system-fields-seeder.service"; +import { AiInteractionService } from "./services/ai-interaction.service"; +import { ChatterMessageDetailsService } from "./services/chatter-message-details.service"; +import { ChatterMessageService } from "./services/chatter-message.service"; +import { ConcatComputedFieldProvider } from "./services/computed-fields/concat-computed-field-provider.service"; +import { AlphaNumExternalIdComputationProvider } from "./services/computed-fields/entity/alpha-num-external-id-computed-field-provider"; +import { ConcatEntityComputedFieldProvider } from "./services/computed-fields/entity/concat-entity-computed-field-provider.service"; +import { NoopsEntityComputedFieldProviderService } from "./services/computed-fields/entity/noops-entity-computed-field-provider.service"; +import { CRUDService } from "./services/crud.service"; +import { CsvService } from "./services/csv.service"; +import { DashboardQuestionSqlDatasetConfigService } from "./services/dashboard-question-sql-dataset-config.service"; +import { DashboardQuestionService } from "./services/dashboard-question.service"; +import { DashboardVariableSQLDynamicProvider } from "./services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service"; +import { DasbhoardVariableTestDynamicProvider } from "./services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service"; +import { DashboardVariableService } from "./services/dashboard-variable.service"; +import { DashboardService } from "./services/dashboard.service"; +import { DashboardLayoutService } from "./services/dashboard-layout.service"; -import { ExcelService } from './services/excel.service'; -import { ExportTemplateService } from './services/export-template.service'; -import { ExportTransactionService } from './services/export-transaction.service'; -import { IngestMetadataService } from './services/genai/ingest-metadata.service'; -import { McpHandlerFactory } from './services/genai/mcp-handlers/mcp-handler-factory.service'; -import { R2RHelperService } from './services/genai/r2r-helper.service'; -import { ImportTransactionErrorLogService } from './services/import-transaction-error-log.service'; -import { ImportTransactionService } from './services/import-transaction.service'; -import { LocaleService } from './services/locale.service'; -import { FileS3StorageProvider } from './services/mediaStorageProviders/file-s3-storage-provider'; -import { FileStorageProvider } from './services/mediaStorageProviders/file-storage-provider'; -import { PollerService } from './services/poller.service'; -import { ChartJsSqlDataProvider } from './services/question-data-providers/chartjs-sql-data-provider.service'; -import { PrimeReactDatatableSqlDataProvider } from './services/question-data-providers/prime-react-datatable-sql-data-provider.service'; -import { PrimeReactMeterGroupSqlDataProvider } from './services/question-data-providers/prime-react-meter-group-sql-data-provider.service'; -import { PublisherFactory } from './services/queues/publisher-factory.service'; -import { RequestContextService } from './services/request-context.service'; -import { RoleMetadataService } from './services/role-metadata.service'; -import { SavedFiltersService } from './services/saved-filters.service'; -import { ScheduledJobService } from './services/scheduled-job.service'; -import { AgentSessionService } from './services/agent-session.service'; -import { AgentEventService } from './services/agent-event.service'; -import { McpAuditLogService } from './services/mcp-audit-log.service'; -import { SchedulerServiceImpl } from './services/scheduled-jobs/scheduler.service'; -import { SecurityRuleService } from './services/security-rule.service'; -import { ListOfDashboardQuestionProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-question-providers-selection-provider.service'; -import { ListOfDashboardVariableProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service'; -import { ListOfScheduledJobsSelectionProvider } from './services/selection-providers/list-of-scheduled-jobs-selection-provider.service'; -import { LocaleListSelectionProvider } from './services/selection-providers/locale-list-selection-provider.service'; -import { SettingService } from './services/setting.service'; -import { TwilioSMSService } from './services/sms/TwilioSMSService'; -import { SolidTsMorphService } from './services/solid-ts-morph.service'; -import { SqlExpressionResolverService } from './services/sql-expression-resolver.service'; -import { TextractService } from './services/textract.service'; -import { UserActivityHistoryService } from './services/user-activity-history.service'; -import { UserViewMetadataService } from './services/user-view-metadata.service'; -import { UserService } from './services/user.service'; -import { Three60WhatsappService } from './services/whatsapp/Three60WhatsappService'; -import { AuditSubscriber } from './subscribers/audit.subscriber'; -import { ComputedEntityFieldSubscriber } from './subscribers/computed-entity-field.subscriber'; -import { CreatedByUpdatedBySubscriber } from './subscribers/created-by-updated-by.subscriber'; -import { DashboardQuestionSqlDatasetConfigSubscriber } from './subscribers/dashboard-question-sql-dataset-config.subscriber'; -import { DashboardQuestionSubscriber } from './subscribers/dashboard-question.subscriber'; -import { DashboardVariableSubscriber } from './subscribers/dashboard-variable.subscriber'; -import { DashboardSubscriber } from './subscribers/dashboard.subscriber'; -import { ListOfValuesSubscriber } from './subscribers/list-of-values.subscriber'; -import { ScheduledJobSubscriber } from './subscribers/scheduled-job.subscriber'; -import { SecurityRuleSubscriber } from './subscribers/security-rule.subscriber'; -import { ViewMetadataSubsciber } from './subscribers/view-metadata.subscriber'; -import { MediaStorageProviderMetadataRepository } from './repository/media-storage-provider-metadata.repository'; -import { McpCommand } from './commands/mcp.command'; -import { FixturesService } from './services/fixtures.service'; -import { FixturesSetupCommand } from './commands/fixtures/fixtures-setup.command'; -import { FixturesTearDownCommand } from './commands/fixtures/fixtures-tear-down.command'; -import { DatabaseBootstrapService } from './services/database/database-bootstrap.service'; -import { SequenceNumComputedFieldProvider } from './services/computed-fields/entity/sequence-num-computed-field-provider'; -import { ModelSequence } from './entities/model-sequence.entity'; -import { ModelSequenceService } from './services/model-sequence.service'; -import { ModelSequenceController } from './controllers/model-sequence.controller'; -import { ModelSequenceRepository } from './repository/model-sequence.repository'; -import { CacheModule } from '@nestjs/cache-manager'; -import { CacheManagerOptions } from './config/cache.options'; -import { SolidCoreDefaultSettingsProvider } from './services/settings/default-settings-provider.service'; -import { SmsFactory } from './factories/sms.factory'; -import { WhatsAppFactory } from './factories/whatsapp.factory'; -import { ImageEncodingService } from './helpers/image-encoding.helper'; -import { SolidMicroserviceAdapter } from './helpers/solid-microservice-adapter.service'; -import { InfoCommand } from './commands/info.command'; -import { ListOfRolesSelectionProvider } from './services/selection-providers/list-of-roles-selectionproviders.service'; -import { Entity } from 'typeorm'; +import { ExcelService } from "./services/excel.service"; +import { ExportTemplateService } from "./services/export-template.service"; +import { ExportTransactionService } from "./services/export-transaction.service"; +import { IngestMetadataService } from "./services/genai/ingest-metadata.service"; +import { McpHandlerFactory } from "./services/genai/mcp-handlers/mcp-handler-factory.service"; +import { R2RHelperService } from "./services/genai/r2r-helper.service"; +import { ImportTransactionErrorLogService } from "./services/import-transaction-error-log.service"; +import { ImportTransactionService } from "./services/import-transaction.service"; +import { LocaleService } from "./services/locale.service"; +import { FileS3StorageProvider } from "./services/mediaStorageProviders/file-s3-storage-provider"; +import { FileStorageProvider } from "./services/mediaStorageProviders/file-storage-provider"; +import { PollerService } from "./services/poller.service"; +import { ChartJsSqlDataProvider } from "./services/question-data-providers/chartjs-sql-data-provider.service"; +import { PrimeReactDatatableSqlDataProvider } from "./services/question-data-providers/prime-react-datatable-sql-data-provider.service"; +import { PrimeReactMeterGroupSqlDataProvider } from "./services/question-data-providers/prime-react-meter-group-sql-data-provider.service"; +import { PublisherFactory } from "./services/queues/publisher-factory.service"; +import { RequestContextService } from "./services/request-context.service"; +import { RoleMetadataService } from "./services/role-metadata.service"; +import { SavedFiltersService } from "./services/saved-filters.service"; +import { ScheduledJobService } from "./services/scheduled-job.service"; +import { AgentSessionService } from "./services/agent-session.service"; +import { AgentEventService } from "./services/agent-event.service"; +import { SchedulerServiceImpl } from "./services/scheduled-jobs/scheduler.service"; +import { SecurityRuleService } from "./services/security-rule.service"; +import { ListOfDashboardQuestionProvidersSelectionProvider } from "./services/selection-providers/list-of-dashboard-question-providers-selection-provider.service"; +import { ListOfDashboardVariableProvidersSelectionProvider } from "./services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service"; +import { ListOfScheduledJobsSelectionProvider } from "./services/selection-providers/list-of-scheduled-jobs-selection-provider.service"; +import { LocaleListSelectionProvider } from "./services/selection-providers/locale-list-selection-provider.service"; +import { SettingService } from "./services/setting.service"; +import { TwilioSMSService } from "./services/sms/TwilioSMSService"; +import { SolidTsMorphService } from "./services/solid-ts-morph.service"; +import { SqlExpressionResolverService } from "./services/sql-expression-resolver.service"; +import { TextractService } from "./services/textract.service"; +import { UserActivityHistoryService } from "./services/user-activity-history.service"; +import { UserDeviceMetadataService } from "./services/user-device-metadata.service"; +import { UserViewMetadataService } from "./services/user-view-metadata.service"; +import { UserService } from "./services/user.service"; +import { Three60WhatsappService } from "./services/whatsapp/Three60WhatsappService"; +import { AuditSubscriber } from "./subscribers/audit.subscriber"; +import { ComputedEntityFieldSubscriber } from "./subscribers/computed-entity-field.subscriber"; +import { CreatedByUpdatedBySubscriber } from "./subscribers/created-by-updated-by.subscriber"; +import { DashboardQuestionSqlDatasetConfigSubscriber } from "./subscribers/dashboard-question-sql-dataset-config.subscriber"; +import { DashboardQuestionSubscriber } from "./subscribers/dashboard-question.subscriber"; +import { DashboardVariableSubscriber } from "./subscribers/dashboard-variable.subscriber"; +import { DashboardSubscriber } from "./subscribers/dashboard.subscriber"; +import { ListOfValuesSubscriber } from "./subscribers/list-of-values.subscriber"; +import { ScheduledJobSubscriber } from "./subscribers/scheduled-job.subscriber"; +import { SecurityRuleSubscriber } from "./subscribers/security-rule.subscriber"; +import { ViewMetadataSubsciber } from "./subscribers/view-metadata.subscriber"; +import { MediaStorageProviderMetadataRepository } from "./repository/media-storage-provider-metadata.repository"; +import { McpCommand } from "./commands/mcp.command"; +import { FixturesService } from "./services/fixtures.service"; +import { FixturesSetupCommand } from "./commands/fixtures/fixtures-setup.command"; +import { FixturesTearDownCommand } from "./commands/fixtures/fixtures-tear-down.command"; +import { DatabaseBootstrapService } from "./services/database/database-bootstrap.service"; +import { SequenceNumComputedFieldProvider } from "./services/computed-fields/entity/sequence-num-computed-field-provider"; +import { ModelSequence } from "./entities/model-sequence.entity"; +import { ModelSequenceService } from "./services/model-sequence.service"; +import { ModelSequenceController } from "./controllers/model-sequence.controller"; +import { ModelSequenceRepository } from "./repository/model-sequence.repository"; +import { CacheModule } from "@nestjs/cache-manager"; +import { CacheManagerOptions } from "./config/cache.options"; +import { SolidCoreDefaultSettingsProvider } from "./services/settings/default-settings-provider.service"; +import { SmsFactory } from "./factories/sms.factory"; +import { WhatsAppFactory } from "./factories/whatsapp.factory"; +import { AmazonSnsPushNotificationQueuePublisherDatabase } from "./jobs/database/amazon-sns-push-notification-publisher-database.service"; +import { AmazonSnsPushNotificationQueueSubscriberDatabase } from "./jobs/database/amazon-sns-push-notification-subscriber-database.service"; +import { AmazonSnsPushNotificationQueuePublisherRabbitmq } from "./jobs/rabbitmq/amazon-sns-push-notification-publisher.service"; +import { AmazonSnsPushNotificationQueueSubscriberRabbitmq } from "./jobs/rabbitmq/amazon-sns-push-notification-subscriber.service"; +import { AmazonSnsPushNotificationQueuePublisherRedis } from "./jobs/redis/amazon-sns-push-notification-publisher-redis.service"; +import { AmazonSnsPushNotificationQueueSubscriberRedis } from "./jobs/redis/amazon-sns-push-notification-subscriber-redis.service"; +import { AmazonSNSPushNotificationService } from "./services/push/amazon-sns-push-notification.service"; +import { ImageEncodingService } from "./helpers/image-encoding.helper"; +import { SolidMicroserviceAdapter } from "./helpers/solid-microservice-adapter.service"; +import { InfoCommand } from "./commands/info.command"; +import { ListOfRolesSelectionProvider } from "./services/selection-providers/list-of-roles-selectionproviders.service"; +import { McpAuditLogRepository } from "./repository/mcp-audit-log.repository"; +import { McpAuditLogService } from "./services/mcp-audit-log.service"; @Global() @Module({ @@ -440,8 +455,10 @@ import { Entity } from 'typeorm'; SecurityRule, Setting, SmsTemplate, + PushNotificationTemplate, User, UserApiKey, + UserDeviceMetadata, UserActivityHistory, UserViewMetadata, ViewMetadata, @@ -511,6 +528,7 @@ import { Entity } from 'typeorm'; GupshupWebhookController, MetaCloudWhatsappWebhookController, OTPAuthenticationController, + PushNotificationTemplateController, PermissionMetadataController, RoleMetadataController, SavedFiltersController, @@ -609,7 +627,9 @@ import { Entity } from 'typeorm'; MetaCloudWhatsappService, GupshupOtpWhatsappService, TwilioSMSService, + AmazonSNSPushNotificationService, SmsTemplateService, + PushNotificationTemplateService, EmailTemplateService, PublisherFactory, PollerService, @@ -637,6 +657,8 @@ import { Entity } from 'typeorm'; TwilioSmsQueueSubscriberDatabase, TwilioSmsQueuePublisherRabbitmq, TwilioSmsQueueSubscriberRabbitmq, + AmazonSnsPushNotificationQueuePublisherRabbitmq, + AmazonSnsPushNotificationQueueSubscriberRabbitmq, Msg91OTPQueuePublisher, Msg91OTPQueueSubscriber, OTPQueuePublisherDatabase, @@ -705,6 +727,8 @@ import { Entity } from 'typeorm'; TriggerMcpClientSubscriberRedis, TwilioSmsQueuePublisherRedis, TwilioSmsQueueSubscriberRedis, + AmazonSnsPushNotificationQueuePublisherRedis, + AmazonSnsPushNotificationQueueSubscriberRedis, GenerateCodePublisherDatabase, GenerateCodeSubscriberDatabase, GenerateCodePublisherRabbitmq, @@ -718,7 +742,9 @@ import { Entity } from 'typeorm'; RoleMetadataService, PermissionMetadataSeederService, UserService, + UserDeviceMetadataService, UserApiKeyRepository, + UserDeviceMetadataRepository, UserRepository, SettingService, ConcatComputedFieldProvider, @@ -800,6 +826,7 @@ import { Entity } from 'typeorm'; MailFactory, WhatsAppFactory, SmsFactory, + PushNotificationFactory, ChatterMessageRepository, ChatterMessageDetailsRepository, AiInteractionRepository, @@ -822,6 +849,7 @@ import { Entity } from 'typeorm'; SavedFiltersRepository, SettingRepository, SmsTemplateRepository, + PushNotificationTemplateRepository, UserActivityHistoryRepository, UserViewMetadataRepository, ModelMetadataRepository, @@ -839,6 +867,8 @@ import { Entity } from 'typeorm'; ImageEncodingService, SolidMicroserviceAdapter, ListOfRolesSelectionProvider, + AmazonSnsPushNotificationQueuePublisherDatabase, + AmazonSnsPushNotificationQueueSubscriberDatabase, ], exports: [ AiInteractionService, @@ -868,6 +898,7 @@ import { Entity } from 'typeorm'; MailFactory, WhatsAppFactory, SmsFactory, + PushNotificationFactory, MediaService, MediaStorageProviderMetadataService, ModelMetadataHelperService, @@ -888,14 +919,17 @@ import { Entity } from 'typeorm'; SchedulerServiceImpl, SecurityRuleRepository, SmsTemplateService, + PushNotificationTemplateService, SMTPEMailService, SolidIntrospectService, SolidRegistry, TextractService, TinyUrlService, TwilioSMSService, + AmazonSNSPushNotificationService, TypeOrmModule, UserActivityHistoryService, + UserDeviceMetadataService, ImageEncodingService, SolidMicroserviceAdapter, UserService,