servcraft 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { logger } from '../../core/logger.js';
|
|
2
|
+
import { BadRequestError } from '../../utils/errors.js';
|
|
3
|
+
import { prisma } from '../../database/prisma.js';
|
|
4
|
+
import { NotificationRepository } from './notification.repository.js';
|
|
5
|
+
import type {
|
|
6
|
+
Notification,
|
|
7
|
+
NotificationConfig,
|
|
8
|
+
NotificationChannel,
|
|
9
|
+
EmailMessage,
|
|
10
|
+
SMSMessage,
|
|
11
|
+
PushMessage,
|
|
12
|
+
WebhookMessage,
|
|
13
|
+
NotificationTemplate,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Notification Service
|
|
18
|
+
* Manages multi-channel notifications with Prisma persistence
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Multi-channel support (email, SMS, push, webhook, in-app)
|
|
22
|
+
* - Persistent notification storage in database
|
|
23
|
+
* - Template system with variable substitution
|
|
24
|
+
* - Multiple provider support per channel
|
|
25
|
+
*/
|
|
26
|
+
export class NotificationService {
|
|
27
|
+
private config: NotificationConfig;
|
|
28
|
+
private repository: NotificationRepository;
|
|
29
|
+
|
|
30
|
+
constructor(config: NotificationConfig = {}) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.repository = new NotificationRepository(prisma);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ==========================================
|
|
36
|
+
// SEND NOTIFICATIONS
|
|
37
|
+
// ==========================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send a notification through a specific channel
|
|
41
|
+
*/
|
|
42
|
+
async send(
|
|
43
|
+
userId: string,
|
|
44
|
+
channel: NotificationChannel,
|
|
45
|
+
title: string,
|
|
46
|
+
body: string,
|
|
47
|
+
data?: Record<string, unknown>
|
|
48
|
+
): Promise<Notification> {
|
|
49
|
+
// Create notification record
|
|
50
|
+
const notification = await this.repository.createNotification({
|
|
51
|
+
userId,
|
|
52
|
+
channel,
|
|
53
|
+
status: 'pending',
|
|
54
|
+
title,
|
|
55
|
+
body,
|
|
56
|
+
data,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
switch (channel) {
|
|
61
|
+
case 'email':
|
|
62
|
+
await this.sendEmail({ to: data?.email as string, subject: title, text: body });
|
|
63
|
+
break;
|
|
64
|
+
case 'sms':
|
|
65
|
+
await this.sendSMS({ to: data?.phone as string, body });
|
|
66
|
+
break;
|
|
67
|
+
case 'push':
|
|
68
|
+
await this.sendPush({ tokens: data?.tokens as string[], title, body });
|
|
69
|
+
break;
|
|
70
|
+
case 'webhook':
|
|
71
|
+
await this.sendWebhook({ url: data?.url as string, body: { title, body, data } });
|
|
72
|
+
break;
|
|
73
|
+
case 'in_app':
|
|
74
|
+
// Just stored in database
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update status to sent
|
|
79
|
+
const updated = await this.repository.updateNotification(notification.id, {
|
|
80
|
+
status: 'sent',
|
|
81
|
+
sentAt: new Date(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return updated || notification;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Update status to failed
|
|
87
|
+
await this.repository.updateNotification(notification.id, {
|
|
88
|
+
status: 'failed',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
logger.error({ error, notificationId: notification.id }, 'Failed to send notification');
|
|
92
|
+
return { ...notification, status: 'failed' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Send notification to user through multiple channels
|
|
98
|
+
*/
|
|
99
|
+
async sendToUser(
|
|
100
|
+
userId: string,
|
|
101
|
+
channels: NotificationChannel[],
|
|
102
|
+
title: string,
|
|
103
|
+
body: string,
|
|
104
|
+
data?: Record<string, unknown>
|
|
105
|
+
): Promise<Notification[]> {
|
|
106
|
+
const results: Notification[] = [];
|
|
107
|
+
for (const channel of channels) {
|
|
108
|
+
const notification = await this.send(userId, channel, title, body, data);
|
|
109
|
+
results.push(notification);
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ==========================================
|
|
115
|
+
// EMAIL METHODS
|
|
116
|
+
// ==========================================
|
|
117
|
+
|
|
118
|
+
async sendEmail(message: EmailMessage): Promise<void> {
|
|
119
|
+
if (!this.config.email) {
|
|
120
|
+
throw new BadRequestError('Email not configured');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { provider } = this.config.email;
|
|
124
|
+
|
|
125
|
+
switch (provider) {
|
|
126
|
+
case 'sendgrid':
|
|
127
|
+
await this.sendEmailViaSendGrid(message);
|
|
128
|
+
break;
|
|
129
|
+
case 'resend':
|
|
130
|
+
await this.sendEmailViaResend(message);
|
|
131
|
+
break;
|
|
132
|
+
case 'mailgun':
|
|
133
|
+
await this.sendEmailViaMailgun(message);
|
|
134
|
+
break;
|
|
135
|
+
case 'ses':
|
|
136
|
+
await this.sendEmailViaSES(message);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
await this.sendEmailViaSMTP(message);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logger.info({ to: message.to, subject: message.subject }, 'Email sent');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async sendEmailViaSendGrid(message: EmailMessage): Promise<void> {
|
|
146
|
+
const config = this.config.email!.sendgrid!;
|
|
147
|
+
const body = await this.renderTemplate(message);
|
|
148
|
+
|
|
149
|
+
await fetch('https://api.sendgrid.com/v3/mail/send', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
personalizations: [
|
|
157
|
+
{
|
|
158
|
+
to: Array.isArray(message.to)
|
|
159
|
+
? message.to.map((e) => ({ email: e }))
|
|
160
|
+
: [{ email: message.to }],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
from: { email: this.config.email!.from },
|
|
164
|
+
subject: message.subject,
|
|
165
|
+
content: [{ type: body.html ? 'text/html' : 'text/plain', value: body.html || body.text }],
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async sendEmailViaResend(message: EmailMessage): Promise<void> {
|
|
171
|
+
const config = this.config.email!.resend!;
|
|
172
|
+
const body = await this.renderTemplate(message);
|
|
173
|
+
|
|
174
|
+
await fetch('https://api.resend.com/emails', {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
from: this.config.email!.from,
|
|
182
|
+
to: Array.isArray(message.to) ? message.to : [message.to],
|
|
183
|
+
subject: message.subject,
|
|
184
|
+
html: body.html,
|
|
185
|
+
text: body.text,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async sendEmailViaMailgun(message: EmailMessage): Promise<void> {
|
|
191
|
+
const config = this.config.email!.mailgun!;
|
|
192
|
+
const body = await this.renderTemplate(message);
|
|
193
|
+
|
|
194
|
+
const formData = new FormData();
|
|
195
|
+
formData.append('from', this.config.email!.from);
|
|
196
|
+
formData.append('to', Array.isArray(message.to) ? message.to.join(',') : message.to);
|
|
197
|
+
formData.append('subject', message.subject);
|
|
198
|
+
if (body.html) formData.append('html', body.html);
|
|
199
|
+
if (body.text) formData.append('text', body.text);
|
|
200
|
+
|
|
201
|
+
await fetch(`https://api.mailgun.net/v3/${config.domain}/messages`, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
Authorization: `Basic ${Buffer.from(`api:${config.apiKey}`).toString('base64')}`,
|
|
205
|
+
},
|
|
206
|
+
body: formData,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async sendEmailViaSES(_message: EmailMessage): Promise<void> {
|
|
211
|
+
// AWS SES implementation - use @aws-sdk/client-ses in production
|
|
212
|
+
logger.debug('SES email would be sent');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async sendEmailViaSMTP(_message: EmailMessage): Promise<void> {
|
|
216
|
+
// SMTP implementation - use nodemailer in production
|
|
217
|
+
logger.debug('SMTP email would be sent');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ==========================================
|
|
221
|
+
// SMS METHODS
|
|
222
|
+
// ==========================================
|
|
223
|
+
|
|
224
|
+
async sendSMS(message: SMSMessage): Promise<void> {
|
|
225
|
+
if (!this.config.sms) {
|
|
226
|
+
throw new BadRequestError('SMS not configured');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { provider } = this.config.sms;
|
|
230
|
+
|
|
231
|
+
switch (provider) {
|
|
232
|
+
case 'twilio':
|
|
233
|
+
await this.sendSMSViaTwilio(message);
|
|
234
|
+
break;
|
|
235
|
+
case 'nexmo':
|
|
236
|
+
await this.sendSMSViaNexmo(message);
|
|
237
|
+
break;
|
|
238
|
+
case 'africas_talking':
|
|
239
|
+
await this.sendSMSViaAfricasTalking(message);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
logger.info({ to: message.to }, 'SMS sent');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async sendSMSViaTwilio(message: SMSMessage): Promise<void> {
|
|
247
|
+
const config = this.config.sms!.twilio!;
|
|
248
|
+
const recipients = Array.isArray(message.to) ? message.to : [message.to];
|
|
249
|
+
|
|
250
|
+
for (const to of recipients) {
|
|
251
|
+
await fetch(`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: `Basic ${Buffer.from(`${config.accountSid}:${config.authToken}`).toString('base64')}`,
|
|
255
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
256
|
+
},
|
|
257
|
+
body: new URLSearchParams({
|
|
258
|
+
From: this.config.sms!.from,
|
|
259
|
+
To: to,
|
|
260
|
+
Body: message.body,
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async sendSMSViaNexmo(message: SMSMessage): Promise<void> {
|
|
267
|
+
const config = this.config.sms!.nexmo!;
|
|
268
|
+
const recipients = Array.isArray(message.to) ? message.to : [message.to];
|
|
269
|
+
|
|
270
|
+
for (const to of recipients) {
|
|
271
|
+
await fetch('https://rest.nexmo.com/sms/json', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
api_key: config.apiKey,
|
|
276
|
+
api_secret: config.apiSecret,
|
|
277
|
+
from: this.config.sms!.from,
|
|
278
|
+
to,
|
|
279
|
+
text: message.body,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async sendSMSViaAfricasTalking(message: SMSMessage): Promise<void> {
|
|
286
|
+
const config = this.config.sms!.africasTalking!;
|
|
287
|
+
const recipients = Array.isArray(message.to) ? message.to.join(',') : message.to;
|
|
288
|
+
|
|
289
|
+
await fetch('https://api.africastalking.com/version1/messaging', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: {
|
|
292
|
+
Accept: 'application/json',
|
|
293
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
294
|
+
apiKey: config.apiKey,
|
|
295
|
+
},
|
|
296
|
+
body: new URLSearchParams({
|
|
297
|
+
username: config.username,
|
|
298
|
+
to: recipients,
|
|
299
|
+
message: message.body,
|
|
300
|
+
from: this.config.sms!.from,
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ==========================================
|
|
306
|
+
// PUSH NOTIFICATION METHODS
|
|
307
|
+
// ==========================================
|
|
308
|
+
|
|
309
|
+
async sendPush(message: PushMessage): Promise<void> {
|
|
310
|
+
if (!this.config.push) {
|
|
311
|
+
throw new BadRequestError('Push notifications not configured');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { provider } = this.config.push;
|
|
315
|
+
|
|
316
|
+
switch (provider) {
|
|
317
|
+
case 'firebase':
|
|
318
|
+
await this.sendPushViaFirebase(message);
|
|
319
|
+
break;
|
|
320
|
+
case 'onesignal':
|
|
321
|
+
await this.sendPushViaOneSignal(message);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
logger.info({ tokens: message.tokens.length }, 'Push notification sent');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async sendPushViaFirebase(message: PushMessage): Promise<void> {
|
|
329
|
+
// Firebase Cloud Messaging - use firebase-admin in production
|
|
330
|
+
logger.debug({ message }, 'Firebase push would be sent');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async sendPushViaOneSignal(message: PushMessage): Promise<void> {
|
|
334
|
+
const config = this.config.push!.onesignal!;
|
|
335
|
+
|
|
336
|
+
await fetch('https://onesignal.com/api/v1/notifications', {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: {
|
|
339
|
+
Authorization: `Basic ${config.apiKey}`,
|
|
340
|
+
'Content-Type': 'application/json',
|
|
341
|
+
},
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
app_id: config.appId,
|
|
344
|
+
include_player_ids: message.tokens,
|
|
345
|
+
headings: { en: message.title },
|
|
346
|
+
contents: { en: message.body },
|
|
347
|
+
data: message.data,
|
|
348
|
+
ios_badgeType: 'SetTo',
|
|
349
|
+
ios_badgeCount: message.badge,
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ==========================================
|
|
355
|
+
// WEBHOOK METHODS
|
|
356
|
+
// ==========================================
|
|
357
|
+
|
|
358
|
+
async sendWebhook(message: WebhookMessage): Promise<void> {
|
|
359
|
+
const config = this.config.webhook || {};
|
|
360
|
+
const method = message.method || 'POST';
|
|
361
|
+
|
|
362
|
+
const response = await fetch(message.url, {
|
|
363
|
+
method,
|
|
364
|
+
headers: {
|
|
365
|
+
'Content-Type': 'application/json',
|
|
366
|
+
...config.defaultHeaders,
|
|
367
|
+
...message.headers,
|
|
368
|
+
},
|
|
369
|
+
body: JSON.stringify(message.body),
|
|
370
|
+
signal: AbortSignal.timeout(config.timeout || 30000),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
throw new Error(`Webhook failed: ${response.status}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
logger.info({ url: message.url, method }, 'Webhook sent');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ==========================================
|
|
381
|
+
// TEMPLATE METHODS
|
|
382
|
+
// ==========================================
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Register a notification template
|
|
386
|
+
*/
|
|
387
|
+
async registerTemplate(
|
|
388
|
+
template: Omit<NotificationTemplate, 'id'>
|
|
389
|
+
): Promise<NotificationTemplate> {
|
|
390
|
+
return this.repository.createTemplate(template);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get template by name
|
|
395
|
+
*/
|
|
396
|
+
async getTemplate(name: string): Promise<NotificationTemplate | null> {
|
|
397
|
+
return this.repository.getTemplateByName(name);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get all templates
|
|
402
|
+
*/
|
|
403
|
+
async getAllTemplates(): Promise<NotificationTemplate[]> {
|
|
404
|
+
return this.repository.getAllTemplates();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async renderTemplate(message: EmailMessage): Promise<{ text?: string; html?: string }> {
|
|
408
|
+
if (!message.template) {
|
|
409
|
+
return { text: message.text, html: message.html };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const template = await this.repository.getTemplateByName(message.template);
|
|
413
|
+
if (!template) {
|
|
414
|
+
return { text: message.text, html: message.html };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let rendered = template.body;
|
|
418
|
+
for (const [key, value] of Object.entries(message.templateData || {})) {
|
|
419
|
+
rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { html: rendered, text: rendered.replace(/<[^>]*>/g, '') };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ==========================================
|
|
426
|
+
// IN-APP NOTIFICATION METHODS
|
|
427
|
+
// ==========================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get user's notifications
|
|
431
|
+
*/
|
|
432
|
+
async getUserNotifications(
|
|
433
|
+
userId: string,
|
|
434
|
+
options?: { limit?: number; offset?: number }
|
|
435
|
+
): Promise<Notification[]> {
|
|
436
|
+
return this.repository.getNotificationsByUserId(userId, options);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get unread count for user
|
|
441
|
+
*/
|
|
442
|
+
async getUnreadCount(userId: string): Promise<number> {
|
|
443
|
+
return this.repository.getUnreadCount(userId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Mark notification as read
|
|
448
|
+
*/
|
|
449
|
+
async markAsRead(notificationId: string): Promise<Notification | null> {
|
|
450
|
+
return this.repository.markAsRead(notificationId);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Mark all notifications as read for user
|
|
455
|
+
*/
|
|
456
|
+
async markAllAsRead(userId: string): Promise<number> {
|
|
457
|
+
return this.repository.markAllAsRead(userId);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Delete old notifications (cleanup)
|
|
462
|
+
*/
|
|
463
|
+
async cleanupOldNotifications(olderThanDays: number = 30): Promise<number> {
|
|
464
|
+
const cutoffDate = new Date();
|
|
465
|
+
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
466
|
+
return this.repository.deleteOldNotifications(cutoffDate);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Singleton instance
|
|
471
|
+
let notificationService: NotificationService | null = null;
|
|
472
|
+
|
|
473
|
+
export function getNotificationService(): NotificationService {
|
|
474
|
+
if (!notificationService) {
|
|
475
|
+
notificationService = new NotificationService();
|
|
476
|
+
}
|
|
477
|
+
return notificationService;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function createNotificationService(config: NotificationConfig): NotificationService {
|
|
481
|
+
notificationService = new NotificationService(config);
|
|
482
|
+
return notificationService;
|
|
483
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export type NotificationChannel = 'email' | 'sms' | 'push' | 'in_app' | 'webhook';
|
|
2
|
+
export type NotificationStatus = 'pending' | 'sent' | 'delivered' | 'failed' | 'read';
|
|
3
|
+
|
|
4
|
+
export interface Notification {
|
|
5
|
+
id: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
channel: NotificationChannel;
|
|
8
|
+
status: NotificationStatus;
|
|
9
|
+
title: string;
|
|
10
|
+
body: string;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
sentAt?: Date;
|
|
13
|
+
readAt?: Date;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NotificationConfig {
|
|
18
|
+
email?: EmailConfig;
|
|
19
|
+
sms?: SMSConfig;
|
|
20
|
+
push?: PushConfig;
|
|
21
|
+
webhook?: WebhookConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Email configuration
|
|
25
|
+
export interface EmailConfig {
|
|
26
|
+
provider: 'smtp' | 'sendgrid' | 'ses' | 'mailgun' | 'resend';
|
|
27
|
+
from: string;
|
|
28
|
+
replyTo?: string;
|
|
29
|
+
smtp?: SMTPConfig;
|
|
30
|
+
sendgrid?: { apiKey: string };
|
|
31
|
+
ses?: { region: string; accessKeyId: string; secretAccessKey: string };
|
|
32
|
+
mailgun?: { apiKey: string; domain: string };
|
|
33
|
+
resend?: { apiKey: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SMTPConfig {
|
|
37
|
+
host: string;
|
|
38
|
+
port: number;
|
|
39
|
+
secure: boolean;
|
|
40
|
+
auth: {
|
|
41
|
+
user: string;
|
|
42
|
+
pass: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EmailMessage {
|
|
47
|
+
to: string | string[];
|
|
48
|
+
subject: string;
|
|
49
|
+
text?: string;
|
|
50
|
+
html?: string;
|
|
51
|
+
template?: string;
|
|
52
|
+
templateData?: Record<string, unknown>;
|
|
53
|
+
attachments?: EmailAttachment[];
|
|
54
|
+
cc?: string[];
|
|
55
|
+
bcc?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface EmailAttachment {
|
|
59
|
+
filename: string;
|
|
60
|
+
content?: Buffer | string;
|
|
61
|
+
path?: string;
|
|
62
|
+
contentType?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// SMS configuration
|
|
66
|
+
export interface SMSConfig {
|
|
67
|
+
provider: 'twilio' | 'nexmo' | 'africas_talking';
|
|
68
|
+
from: string;
|
|
69
|
+
twilio?: { accountSid: string; authToken: string };
|
|
70
|
+
nexmo?: { apiKey: string; apiSecret: string };
|
|
71
|
+
africasTalking?: { username: string; apiKey: string };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SMSMessage {
|
|
75
|
+
to: string | string[];
|
|
76
|
+
body: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Push notification configuration
|
|
80
|
+
export interface PushConfig {
|
|
81
|
+
provider: 'firebase' | 'onesignal' | 'pusher';
|
|
82
|
+
firebase?: { serviceAccount: string | object };
|
|
83
|
+
onesignal?: { appId: string; apiKey: string };
|
|
84
|
+
pusher?: { appId: string; key: string; secret: string; cluster: string };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PushMessage {
|
|
88
|
+
tokens: string[];
|
|
89
|
+
title: string;
|
|
90
|
+
body: string;
|
|
91
|
+
data?: Record<string, string>;
|
|
92
|
+
badge?: number;
|
|
93
|
+
sound?: string;
|
|
94
|
+
imageUrl?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Webhook configuration
|
|
98
|
+
export interface WebhookConfig {
|
|
99
|
+
defaultHeaders?: Record<string, string>;
|
|
100
|
+
timeout?: number;
|
|
101
|
+
retries?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface WebhookMessage {
|
|
105
|
+
url: string;
|
|
106
|
+
method?: 'POST' | 'PUT' | 'PATCH';
|
|
107
|
+
headers?: Record<string, string>;
|
|
108
|
+
body: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Template support
|
|
112
|
+
export interface NotificationTemplate {
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
channel: NotificationChannel;
|
|
116
|
+
subject?: string;
|
|
117
|
+
body: string;
|
|
118
|
+
variables: string[];
|
|
119
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { OAuthService, getOAuthService, createOAuthService } from './oauth.service.js';
|
|
2
|
+
export { registerOAuthRoutes } from './oauth.routes.js';
|
|
3
|
+
export { GoogleOAuthProvider } from './providers/google.provider.js';
|
|
4
|
+
export { FacebookOAuthProvider } from './providers/facebook.provider.js';
|
|
5
|
+
export { GitHubOAuthProvider } from './providers/github.provider.js';
|
|
6
|
+
export { TwitterOAuthProvider } from './providers/twitter.provider.js';
|
|
7
|
+
export { AppleOAuthProvider } from './providers/apple.provider.js';
|
|
8
|
+
export type {
|
|
9
|
+
OAuthConfig,
|
|
10
|
+
OAuthProvider,
|
|
11
|
+
OAuthUser,
|
|
12
|
+
OAuthTokens,
|
|
13
|
+
OAuthState,
|
|
14
|
+
LinkedAccount,
|
|
15
|
+
GoogleOAuthConfig,
|
|
16
|
+
FacebookOAuthConfig,
|
|
17
|
+
GitHubOAuthConfig,
|
|
18
|
+
TwitterOAuthConfig,
|
|
19
|
+
AppleOAuthConfig,
|
|
20
|
+
} from './types.js';
|