From b5fa0f42b63948a50e79497609eb3df29ed39afe Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:07:38 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(mana-notify):=20add=20central?= =?UTF-8?q?=20notification=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestJS notification microservice for email, push, Matrix, and webhook notifications across all ManaCore apps. Features: - Multi-channel delivery (email, push, Matrix, webhook) - Handlebars template engine with defaults - User notification preferences - BullMQ async job processing - Delivery tracking and logging - Prometheus metrics Includes @manacore/notify-client package for NestJS integration. --- packages/notify-client/.eslintignore | 2 + packages/notify-client/README.md | 149 +++++++ packages/notify-client/package.json | 44 ++ packages/notify-client/src/client.ts | 354 +++++++++++++++ packages/notify-client/src/index.ts | 2 + .../notify-client/src/nestjs/constants.ts | 2 + packages/notify-client/src/nestjs/index.ts | 2 + .../notify-client/src/nestjs/notify.module.ts | 106 +++++ packages/notify-client/src/types.ts | 104 +++++ packages/notify-client/tsconfig.json | 17 + packages/notify-client/tsup.config.ts | 15 + services/mana-notify/CLAUDE.md | 390 +++++++++++++++++ services/mana-notify/Dockerfile | 46 ++ services/mana-notify/docker-compose.dev.yml | 23 + services/mana-notify/drizzle.config.ts | 12 + services/mana-notify/nest-cli.json | 10 + services/mana-notify/package.json | 60 +++ services/mana-notify/src/app.module.ts | 44 ++ .../src/channels/channels.module.ts | 11 + .../src/channels/email/email.service.ts | 95 +++++ .../src/channels/matrix/matrix.service.ts | 85 ++++ .../src/channels/push/push.service.ts | 178 ++++++++ .../src/channels/webhook/webhook.service.ts | 91 ++++ .../common/filters/http-exception.filter.ts | 45 ++ .../src/common/guards/jwt-auth.guard.ts | 75 ++++ .../src/common/guards/service-auth.guard.ts | 39 ++ .../mana-notify/src/config/configuration.ts | 44 ++ services/mana-notify/src/db/connection.ts | 33 ++ .../mana-notify/src/db/database.module.ts | 24 ++ services/mana-notify/src/db/migrate.ts | 29 ++ .../src/db/schema/delivery-logs.schema.ts | 49 +++ .../src/db/schema/devices.schema.ts | 46 ++ services/mana-notify/src/db/schema/index.ts | 5 + .../src/db/schema/notifications.schema.ts | 75 ++++ .../src/db/schema/preferences.schema.ts | 46 ++ .../src/db/schema/templates.schema.ts | 50 +++ .../src/devices/devices.controller.ts | 63 +++ .../mana-notify/src/devices/devices.module.ts | 10 + .../src/devices/devices.service.ts | 141 ++++++ .../src/health/health.controller.ts | 46 ++ .../mana-notify/src/health/health.module.ts | 7 + services/mana-notify/src/main.ts | 42 ++ .../src/metrics/metrics.controller.ts | 13 + .../mana-notify/src/metrics/metrics.module.ts | 10 + .../src/metrics/metrics.service.ts | 138 ++++++ .../src/notifications/dto/index.ts | 1 + .../dto/send-notification.dto.ts | 161 +++++++ .../notifications/notifications.controller.ts | 68 +++ .../src/notifications/notifications.module.ts | 15 + .../notifications/notifications.service.ts | 403 ++++++++++++++++++ .../src/preferences/preferences.controller.ts | 62 +++ .../src/preferences/preferences.module.ts | 10 + .../src/preferences/preferences.service.ts | 123 ++++++ .../src/queue/processors/email.processor.ts | 125 ++++++ .../src/queue/processors/matrix.processor.ts | 121 ++++++ .../src/queue/processors/push.processor.ts | 154 +++++++ .../src/queue/processors/webhook.processor.ts | 123 ++++++ .../mana-notify/src/queue/queue.module.ts | 73 ++++ .../src/templates/defaults/password-reset.hbs | 30 ++ .../src/templates/defaults/reminder.hbs | 37 ++ .../src/templates/defaults/verification.hbs | 30 ++ .../src/templates/defaults/welcome.hbs | 30 ++ .../src/templates/templates.controller.ts | 117 +++++ .../src/templates/templates.module.ts | 10 + .../src/templates/templates.service.ts | 234 ++++++++++ services/mana-notify/tsconfig.json | 25 ++ 66 files changed, 4824 insertions(+) create mode 100644 packages/notify-client/.eslintignore create mode 100644 packages/notify-client/README.md create mode 100644 packages/notify-client/package.json create mode 100644 packages/notify-client/src/client.ts create mode 100644 packages/notify-client/src/index.ts create mode 100644 packages/notify-client/src/nestjs/constants.ts create mode 100644 packages/notify-client/src/nestjs/index.ts create mode 100644 packages/notify-client/src/nestjs/notify.module.ts create mode 100644 packages/notify-client/src/types.ts create mode 100644 packages/notify-client/tsconfig.json create mode 100644 packages/notify-client/tsup.config.ts create mode 100644 services/mana-notify/CLAUDE.md create mode 100644 services/mana-notify/Dockerfile create mode 100644 services/mana-notify/docker-compose.dev.yml create mode 100644 services/mana-notify/drizzle.config.ts create mode 100644 services/mana-notify/nest-cli.json create mode 100644 services/mana-notify/package.json create mode 100644 services/mana-notify/src/app.module.ts create mode 100644 services/mana-notify/src/channels/channels.module.ts create mode 100644 services/mana-notify/src/channels/email/email.service.ts create mode 100644 services/mana-notify/src/channels/matrix/matrix.service.ts create mode 100644 services/mana-notify/src/channels/push/push.service.ts create mode 100644 services/mana-notify/src/channels/webhook/webhook.service.ts create mode 100644 services/mana-notify/src/common/filters/http-exception.filter.ts create mode 100644 services/mana-notify/src/common/guards/jwt-auth.guard.ts create mode 100644 services/mana-notify/src/common/guards/service-auth.guard.ts create mode 100644 services/mana-notify/src/config/configuration.ts create mode 100644 services/mana-notify/src/db/connection.ts create mode 100644 services/mana-notify/src/db/database.module.ts create mode 100644 services/mana-notify/src/db/migrate.ts create mode 100644 services/mana-notify/src/db/schema/delivery-logs.schema.ts create mode 100644 services/mana-notify/src/db/schema/devices.schema.ts create mode 100644 services/mana-notify/src/db/schema/index.ts create mode 100644 services/mana-notify/src/db/schema/notifications.schema.ts create mode 100644 services/mana-notify/src/db/schema/preferences.schema.ts create mode 100644 services/mana-notify/src/db/schema/templates.schema.ts create mode 100644 services/mana-notify/src/devices/devices.controller.ts create mode 100644 services/mana-notify/src/devices/devices.module.ts create mode 100644 services/mana-notify/src/devices/devices.service.ts create mode 100644 services/mana-notify/src/health/health.controller.ts create mode 100644 services/mana-notify/src/health/health.module.ts create mode 100644 services/mana-notify/src/main.ts create mode 100644 services/mana-notify/src/metrics/metrics.controller.ts create mode 100644 services/mana-notify/src/metrics/metrics.module.ts create mode 100644 services/mana-notify/src/metrics/metrics.service.ts create mode 100644 services/mana-notify/src/notifications/dto/index.ts create mode 100644 services/mana-notify/src/notifications/dto/send-notification.dto.ts create mode 100644 services/mana-notify/src/notifications/notifications.controller.ts create mode 100644 services/mana-notify/src/notifications/notifications.module.ts create mode 100644 services/mana-notify/src/notifications/notifications.service.ts create mode 100644 services/mana-notify/src/preferences/preferences.controller.ts create mode 100644 services/mana-notify/src/preferences/preferences.module.ts create mode 100644 services/mana-notify/src/preferences/preferences.service.ts create mode 100644 services/mana-notify/src/queue/processors/email.processor.ts create mode 100644 services/mana-notify/src/queue/processors/matrix.processor.ts create mode 100644 services/mana-notify/src/queue/processors/push.processor.ts create mode 100644 services/mana-notify/src/queue/processors/webhook.processor.ts create mode 100644 services/mana-notify/src/queue/queue.module.ts create mode 100644 services/mana-notify/src/templates/defaults/password-reset.hbs create mode 100644 services/mana-notify/src/templates/defaults/reminder.hbs create mode 100644 services/mana-notify/src/templates/defaults/verification.hbs create mode 100644 services/mana-notify/src/templates/defaults/welcome.hbs create mode 100644 services/mana-notify/src/templates/templates.controller.ts create mode 100644 services/mana-notify/src/templates/templates.module.ts create mode 100644 services/mana-notify/src/templates/templates.service.ts create mode 100644 services/mana-notify/tsconfig.json diff --git a/packages/notify-client/.eslintignore b/packages/notify-client/.eslintignore new file mode 100644 index 000000000..d75ac3167 --- /dev/null +++ b/packages/notify-client/.eslintignore @@ -0,0 +1,2 @@ +tsup.config.ts +dist/ diff --git a/packages/notify-client/README.md b/packages/notify-client/README.md new file mode 100644 index 000000000..0fe2ac146 --- /dev/null +++ b/packages/notify-client/README.md @@ -0,0 +1,149 @@ +# @manacore/notify-client + +Client SDK for the mana-notify notification service. + +## Installation + +```bash +pnpm add @manacore/notify-client +``` + +## Usage + +### Basic Usage + +```typescript +import { NotifyClient } from '@manacore/notify-client'; + +const notify = new NotifyClient({ + serviceUrl: 'http://localhost:3040', + serviceKey: process.env.MANA_NOTIFY_SERVICE_KEY, + appId: 'your-app-id', +}); + +// Send email +await notify.sendEmail({ + to: 'user@example.com', + template: 'auth-password-reset', + data: { resetUrl: 'https://...', userName: 'Max' }, +}); + +// Send push notification +await notify.sendPush({ + userId: 'user-uuid', + title: 'New Message', + body: 'You have a new message', + data: { messageId: 'xxx' }, +}); + +// Send to specific token +await notify.sendPush({ + token: 'ExponentPushToken[xxx]', + title: 'Hello', + body: 'World', +}); + +// Schedule notification +await notify.scheduleEmail({ + to: 'user@example.com', + template: 'calendar-reminder', + data: { eventTitle: 'Meeting' }, + scheduledFor: new Date('2024-12-20T13:45:00Z'), +}); +``` + +### NestJS Integration + +```typescript +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { NotifyModule } from '@manacore/notify-client/nestjs'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + NotifyModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + serviceUrl: config.get('MANA_NOTIFY_URL', 'http://localhost:3040'), + serviceKey: config.get('MANA_NOTIFY_SERVICE_KEY'), + appId: config.get('APP_ID'), + }), + inject: [ConfigService], + }), + ], +}) +export class AppModule {} +``` + +Then inject the client: + +```typescript +import { Injectable, Inject } from '@nestjs/common'; +import { NOTIFY_CLIENT, NotifyClient } from '@manacore/notify-client/nestjs'; + +@Injectable() +export class NotificationService { + constructor(@Inject(NOTIFY_CLIENT) private readonly notify: NotifyClient) {} + + async sendWelcomeEmail(email: string, name: string) { + await this.notify.sendEmail({ + to: email, + template: 'auth-welcome', + data: { userName: name }, + }); + } +} +``` + +## API Reference + +### NotifyClient + +#### Constructor + +```typescript +new NotifyClient({ + serviceUrl: string; // mana-notify service URL + serviceKey: string; // Service authentication key + appId: string; // Your application ID + timeout?: number; // Request timeout in ms (default: 30000) +}); +``` + +#### Methods + +##### Email + +- `sendEmail(options)` - Send an email immediately +- `scheduleEmail(options)` - Schedule an email for later + +##### Push Notifications + +- `sendPush(options)` - Send a push notification +- `schedulePush(options)` - Schedule a push notification + +##### Other Channels + +- `sendMatrix(options)` - Send a Matrix message +- `sendWebhook(options)` - Send a webhook + +##### Batch & Management + +- `sendBatch(notifications)` - Send multiple notifications +- `getNotification(id)` - Get notification status +- `cancelNotification(id)` - Cancel a pending notification + +##### Templates + +- `listTemplates(appId?)` - List available templates +- `getTemplate(slug, locale?)` - Get a template +- `previewTemplate(slug, data, locale?)` - Preview a rendered template + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MANA_NOTIFY_URL` | mana-notify service URL | +| `MANA_NOTIFY_SERVICE_KEY` | Service authentication key | +| `APP_ID` | Your application ID | diff --git a/packages/notify-client/package.json b/packages/notify-client/package.json new file mode 100644 index 000000000..71bbd6ee2 --- /dev/null +++ b/packages/notify-client/package.json @@ -0,0 +1,44 @@ +{ + "name": "@manacore/notify-client", + "version": "1.0.0", + "description": "Client SDK for mana-notify notification service", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./nestjs": { + "types": "./dist/nestjs/index.d.ts", + "import": "./dist/nestjs/index.mjs", + "require": "./dist/nestjs/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": {}, + "devDependencies": { + "@nestjs/common": "^10.4.17", + "@types/node": "^22.10.5", + "tsup": "^8.3.5", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "@nestjs/common": ">=10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + } + } +} diff --git a/packages/notify-client/src/client.ts b/packages/notify-client/src/client.ts new file mode 100644 index 000000000..3ae108c61 --- /dev/null +++ b/packages/notify-client/src/client.ts @@ -0,0 +1,354 @@ +import type { + SendEmailOptions, + SendPushOptions, + SendMatrixOptions, + SendWebhookOptions, + ScheduleOptions, + NotificationResponse, + BatchNotificationResponse, + Template, + RenderedTemplate, +} from './types'; + +export interface NotifyClientOptions { + serviceUrl: string; + serviceKey: string; + appId: string; + timeout?: number; +} + +export class NotifyClient { + private readonly serviceUrl: string; + private readonly serviceKey: string; + private readonly appId: string; + private readonly timeout: number; + + constructor(options: NotifyClientOptions) { + this.serviceUrl = options.serviceUrl.replace(/\/$/, ''); + this.serviceKey = options.serviceKey; + this.appId = options.appId; + this.timeout = options.timeout || 30000; + } + + // ==================== Notifications ==================== + + /** + * Send an email notification + */ + async sendEmail(options: SendEmailOptions): Promise { + return this.send({ + channel: 'email', + appId: this.appId, + recipient: options.to, + template: options.template, + subject: options.subject, + body: options.body, + data: options.data, + emailOptions: { + from: options.from, + replyTo: options.replyTo, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a push notification + */ + async sendPush(options: SendPushOptions): Promise { + return this.send({ + channel: 'push', + appId: this.appId, + userId: options.userId, + recipient: options.token, + recipients: options.tokens, + subject: options.title, + body: options.body, + data: options.data, + pushOptions: { + sound: options.sound, + badge: options.badge, + channelId: options.channelId, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a Matrix message + */ + async sendMatrix(options: SendMatrixOptions): Promise { + return this.send({ + channel: 'matrix', + appId: this.appId, + recipient: options.roomId, + body: options.body, + matrixOptions: { + formattedBody: options.formattedBody, + msgtype: options.msgtype, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a webhook notification + */ + async sendWebhook(options: SendWebhookOptions): Promise { + return this.send({ + channel: 'webhook', + appId: this.appId, + recipient: options.url, + data: options.body, + webhookOptions: { + method: options.method, + headers: options.headers, + timeout: options.timeout, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Schedule an email notification + */ + async scheduleEmail(options: SendEmailOptions & ScheduleOptions): Promise { + return this.schedule({ + channel: 'email', + appId: this.appId, + recipient: options.to, + template: options.template, + subject: options.subject, + body: options.body, + data: options.data, + emailOptions: { + from: options.from, + replyTo: options.replyTo, + }, + priority: options.priority, + externalId: options.externalId, + scheduledFor: + options.scheduledFor instanceof Date + ? options.scheduledFor.toISOString() + : options.scheduledFor, + }); + } + + /** + * Schedule a push notification + */ + async schedulePush(options: SendPushOptions & ScheduleOptions): Promise { + return this.schedule({ + channel: 'push', + appId: this.appId, + userId: options.userId, + recipient: options.token, + recipients: options.tokens, + subject: options.title, + body: options.body, + data: options.data, + pushOptions: { + sound: options.sound, + badge: options.badge, + channelId: options.channelId, + }, + priority: options.priority, + externalId: options.externalId, + scheduledFor: + options.scheduledFor instanceof Date + ? options.scheduledFor.toISOString() + : options.scheduledFor, + }); + } + + /** + * Send multiple notifications in batch + */ + async sendBatch( + notifications: Array< + | ({ type: 'email' } & SendEmailOptions) + | ({ type: 'push' } & SendPushOptions) + | ({ type: 'matrix' } & SendMatrixOptions) + | ({ type: 'webhook' } & SendWebhookOptions) + > + ): Promise { + const mapped = notifications.map((n) => { + if (n.type === 'email') { + return { + channel: 'email' as const, + appId: this.appId, + recipient: n.to, + template: n.template, + subject: n.subject, + body: n.body, + data: n.data, + priority: n.priority, + externalId: n.externalId, + }; + } else if (n.type === 'push') { + return { + channel: 'push' as const, + appId: this.appId, + userId: n.userId, + recipient: n.token, + recipients: n.tokens, + subject: n.title, + body: n.body, + data: n.data, + priority: n.priority, + externalId: n.externalId, + }; + } else if (n.type === 'matrix') { + return { + channel: 'matrix' as const, + appId: this.appId, + recipient: n.roomId, + body: n.body, + priority: n.priority, + externalId: n.externalId, + }; + } else { + return { + channel: 'webhook' as const, + appId: this.appId, + recipient: n.url, + data: n.body, + priority: n.priority, + externalId: n.externalId, + }; + } + }); + + const response = await this.request('/notifications/batch', { + method: 'POST', + body: JSON.stringify({ notifications: mapped }), + }); + + return response; + } + + /** + * Get notification status + */ + async getNotification(id: string): Promise { + const response = await this.request<{ notification: NotificationResponse | null }>( + `/notifications/${id}` + ); + return response.notification; + } + + /** + * Cancel a pending notification + */ + async cancelNotification(id: string): Promise { + const response = await this.request<{ notification: NotificationResponse }>( + `/notifications/${id}`, + { method: 'DELETE' } + ); + return response.notification; + } + + // ==================== Templates ==================== + + /** + * List all templates + */ + async listTemplates(appId?: string): Promise { + const url = appId ? `/templates?appId=${encodeURIComponent(appId)}` : '/templates'; + const response = await this.request<{ templates: Template[] }>(url); + return response.templates; + } + + /** + * Get a template by slug + */ + async getTemplate(slug: string, locale = 'de-DE'): Promise