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,311 @@
|
|
|
1
|
+
# Notification Module
|
|
2
|
+
|
|
3
|
+
Multi-channel notification system with templates and persistent storage.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Channel** - Email, SMS, Push, Webhook, In-App
|
|
8
|
+
- **Multiple Providers** - SendGrid, Resend, Twilio, Firebase, etc.
|
|
9
|
+
- **Templates** - Reusable notification templates with variables
|
|
10
|
+
- **Persistence** - All notifications stored in database
|
|
11
|
+
- **In-App Notifications** - Read/unread tracking
|
|
12
|
+
|
|
13
|
+
## Supported Providers
|
|
14
|
+
|
|
15
|
+
| Channel | Providers |
|
|
16
|
+
|---------|-----------|
|
|
17
|
+
| Email | SendGrid, Resend, Mailgun, AWS SES, SMTP |
|
|
18
|
+
| SMS | Twilio, Nexmo (Vonage), Africa's Talking |
|
|
19
|
+
| Push | Firebase Cloud Messaging, OneSignal |
|
|
20
|
+
| Webhook | Any HTTP endpoint |
|
|
21
|
+
| In-App | Database storage |
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Configuration
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createNotificationService } from 'servcraft/modules/notification';
|
|
29
|
+
|
|
30
|
+
const notificationService = createNotificationService({
|
|
31
|
+
email: {
|
|
32
|
+
provider: 'sendgrid',
|
|
33
|
+
from: 'noreply@myapp.com',
|
|
34
|
+
sendgrid: {
|
|
35
|
+
apiKey: process.env.SENDGRID_API_KEY!,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
sms: {
|
|
39
|
+
provider: 'twilio',
|
|
40
|
+
from: '+1234567890',
|
|
41
|
+
twilio: {
|
|
42
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
43
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
push: {
|
|
47
|
+
provider: 'firebase',
|
|
48
|
+
firebase: {
|
|
49
|
+
projectId: process.env.FIREBASE_PROJECT_ID!,
|
|
50
|
+
privateKey: process.env.FIREBASE_PRIVATE_KEY!,
|
|
51
|
+
clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Sending Notifications
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Send single notification
|
|
61
|
+
const notification = await notificationService.send(
|
|
62
|
+
'user-123',
|
|
63
|
+
'email',
|
|
64
|
+
'Welcome to MyApp',
|
|
65
|
+
'Thank you for signing up!',
|
|
66
|
+
{ email: 'user@example.com' }
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Send to multiple channels
|
|
70
|
+
const notifications = await notificationService.sendToUser(
|
|
71
|
+
'user-123',
|
|
72
|
+
['email', 'push', 'in_app'],
|
|
73
|
+
'New Message',
|
|
74
|
+
'You have a new message from John',
|
|
75
|
+
{
|
|
76
|
+
email: 'user@example.com',
|
|
77
|
+
tokens: ['fcm-token-1', 'fcm-token-2'],
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Email
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Simple email
|
|
86
|
+
await notificationService.sendEmail({
|
|
87
|
+
to: 'user@example.com',
|
|
88
|
+
subject: 'Welcome!',
|
|
89
|
+
text: 'Welcome to our platform.',
|
|
90
|
+
html: '<h1>Welcome!</h1><p>Welcome to our platform.</p>',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// With template
|
|
94
|
+
await notificationService.sendEmail({
|
|
95
|
+
to: 'user@example.com',
|
|
96
|
+
subject: 'Order Confirmation',
|
|
97
|
+
template: 'order-confirmation',
|
|
98
|
+
templateData: {
|
|
99
|
+
orderId: 'ORD-123',
|
|
100
|
+
total: '$99.99',
|
|
101
|
+
items: '3 items',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Multiple recipients
|
|
106
|
+
await notificationService.sendEmail({
|
|
107
|
+
to: ['user1@example.com', 'user2@example.com'],
|
|
108
|
+
subject: 'Team Update',
|
|
109
|
+
text: 'Here is the weekly update...',
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### SMS
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
await notificationService.sendSMS({
|
|
117
|
+
to: '+1234567890',
|
|
118
|
+
body: 'Your verification code is: 123456',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Multiple recipients
|
|
122
|
+
await notificationService.sendSMS({
|
|
123
|
+
to: ['+1234567890', '+0987654321'],
|
|
124
|
+
body: 'Flash sale starts now!',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Push Notifications
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
await notificationService.sendPush({
|
|
132
|
+
tokens: ['fcm-token-1', 'fcm-token-2'],
|
|
133
|
+
title: 'New Message',
|
|
134
|
+
body: 'You have a new message',
|
|
135
|
+
data: {
|
|
136
|
+
type: 'message',
|
|
137
|
+
messageId: 'msg-123',
|
|
138
|
+
},
|
|
139
|
+
badge: 5,
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Webhooks
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
await notificationService.sendWebhook({
|
|
147
|
+
url: 'https://example.com/webhook',
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'X-Custom-Header': 'value',
|
|
151
|
+
},
|
|
152
|
+
body: {
|
|
153
|
+
event: 'user.created',
|
|
154
|
+
data: { userId: 'user-123' },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### In-App Notifications
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Send in-app notification
|
|
163
|
+
await notificationService.send(
|
|
164
|
+
'user-123',
|
|
165
|
+
'in_app',
|
|
166
|
+
'New Feature Available',
|
|
167
|
+
'Check out our new dashboard!'
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Get user's notifications
|
|
171
|
+
const notifications = await notificationService.getUserNotifications('user-123', {
|
|
172
|
+
limit: 20,
|
|
173
|
+
offset: 0,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Get unread count
|
|
177
|
+
const unreadCount = await notificationService.getUnreadCount('user-123');
|
|
178
|
+
|
|
179
|
+
// Mark as read
|
|
180
|
+
await notificationService.markAsRead(notificationId);
|
|
181
|
+
|
|
182
|
+
// Mark all as read
|
|
183
|
+
await notificationService.markAllAsRead('user-123');
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Templates
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Register template
|
|
190
|
+
await notificationService.registerTemplate({
|
|
191
|
+
name: 'welcome-email',
|
|
192
|
+
channel: 'email',
|
|
193
|
+
subject: 'Welcome to {{appName}}!',
|
|
194
|
+
body: `
|
|
195
|
+
<h1>Welcome, {{userName}}!</h1>
|
|
196
|
+
<p>Thank you for joining {{appName}}.</p>
|
|
197
|
+
<p>Get started by visiting your <a href="{{dashboardUrl}}">dashboard</a>.</p>
|
|
198
|
+
`,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Get template
|
|
202
|
+
const template = await notificationService.getTemplate('welcome-email');
|
|
203
|
+
|
|
204
|
+
// List all templates
|
|
205
|
+
const templates = await notificationService.getAllTemplates();
|
|
206
|
+
|
|
207
|
+
// Use template
|
|
208
|
+
await notificationService.sendEmail({
|
|
209
|
+
to: 'user@example.com',
|
|
210
|
+
subject: 'Welcome!',
|
|
211
|
+
template: 'welcome-email',
|
|
212
|
+
templateData: {
|
|
213
|
+
appName: 'MyApp',
|
|
214
|
+
userName: 'John',
|
|
215
|
+
dashboardUrl: 'https://myapp.com/dashboard',
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Cleanup
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Delete notifications older than 30 days
|
|
224
|
+
const deleted = await notificationService.cleanupOldNotifications(30);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Configuration Types
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
interface NotificationConfig {
|
|
231
|
+
email?: {
|
|
232
|
+
provider: 'sendgrid' | 'resend' | 'mailgun' | 'ses' | 'smtp';
|
|
233
|
+
from: string;
|
|
234
|
+
sendgrid?: { apiKey: string };
|
|
235
|
+
resend?: { apiKey: string };
|
|
236
|
+
mailgun?: { apiKey: string; domain: string };
|
|
237
|
+
ses?: { region: string; accessKeyId: string; secretAccessKey: string };
|
|
238
|
+
smtp?: { host: string; port: number; user: string; pass: string };
|
|
239
|
+
};
|
|
240
|
+
sms?: {
|
|
241
|
+
provider: 'twilio' | 'nexmo' | 'africas_talking';
|
|
242
|
+
from: string;
|
|
243
|
+
twilio?: { accountSid: string; authToken: string };
|
|
244
|
+
nexmo?: { apiKey: string; apiSecret: string };
|
|
245
|
+
africasTalking?: { apiKey: string; username: string };
|
|
246
|
+
};
|
|
247
|
+
push?: {
|
|
248
|
+
provider: 'firebase' | 'onesignal';
|
|
249
|
+
firebase?: { projectId: string; privateKey: string; clientEmail: string };
|
|
250
|
+
onesignal?: { appId: string; apiKey: string };
|
|
251
|
+
};
|
|
252
|
+
webhook?: {
|
|
253
|
+
defaultHeaders?: Record<string, string>;
|
|
254
|
+
timeout?: number;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Notification Status
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
type NotificationStatus = 'pending' | 'sent' | 'failed' | 'read';
|
|
263
|
+
|
|
264
|
+
interface Notification {
|
|
265
|
+
id: string;
|
|
266
|
+
userId: string;
|
|
267
|
+
channel: NotificationChannel;
|
|
268
|
+
status: NotificationStatus;
|
|
269
|
+
title: string;
|
|
270
|
+
body: string;
|
|
271
|
+
data?: Record<string, unknown>;
|
|
272
|
+
sentAt?: Date;
|
|
273
|
+
readAt?: Date;
|
|
274
|
+
createdAt: Date;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Database Schema
|
|
279
|
+
|
|
280
|
+
```sql
|
|
281
|
+
CREATE TABLE notifications (
|
|
282
|
+
id UUID PRIMARY KEY,
|
|
283
|
+
user_id TEXT NOT NULL,
|
|
284
|
+
channel TEXT NOT NULL,
|
|
285
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
286
|
+
title TEXT NOT NULL,
|
|
287
|
+
body TEXT NOT NULL,
|
|
288
|
+
data JSONB,
|
|
289
|
+
sent_at TIMESTAMP,
|
|
290
|
+
read_at TIMESTAMP,
|
|
291
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
CREATE TABLE notification_templates (
|
|
295
|
+
id UUID PRIMARY KEY,
|
|
296
|
+
name TEXT UNIQUE NOT NULL,
|
|
297
|
+
channel TEXT NOT NULL,
|
|
298
|
+
subject TEXT,
|
|
299
|
+
body TEXT NOT NULL,
|
|
300
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Best Practices
|
|
305
|
+
|
|
306
|
+
1. **Use Templates** - Create templates for consistent messaging
|
|
307
|
+
2. **Handle Failures** - Check notification status and implement retries
|
|
308
|
+
3. **Rate Limiting** - Implement rate limits to avoid spam
|
|
309
|
+
4. **User Preferences** - Let users choose notification channels
|
|
310
|
+
5. **Cleanup** - Regularly clean up old notifications
|
|
311
|
+
6. **Logging** - Log all notification attempts for debugging
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# OAuth Module
|
|
2
|
+
|
|
3
|
+
Social authentication with multiple providers (Google, Facebook, GitHub).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Providers** - Google, Facebook, GitHub out of the box
|
|
8
|
+
- **PKCE Support** - Secure authorization code flow
|
|
9
|
+
- **Account Linking** - Link multiple OAuth accounts to one user
|
|
10
|
+
- **Token Management** - Automatic token refresh
|
|
11
|
+
- **State Protection** - CSRF protection with Redis-stored states
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
17
|
+
│ OAuth Service │
|
|
18
|
+
├─────────────────────────────────────────────────────────────┤
|
|
19
|
+
│ Google Provider │ Facebook Provider │ GitHub Provider │
|
|
20
|
+
└────────┬──────────┴─────────┬───────────┴────────┬──────────┘
|
|
21
|
+
│ │ │
|
|
22
|
+
▼ ▼ ▼
|
|
23
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Repository │
|
|
25
|
+
├─────────────────────────────────────────────────────────────┤
|
|
26
|
+
│ Redis (States) │ Prisma (Linked Accounts) │
|
|
27
|
+
└───────────────────────────┴─────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Storage
|
|
31
|
+
|
|
32
|
+
| Data | Storage | TTL |
|
|
33
|
+
|------|---------|-----|
|
|
34
|
+
| OAuth States | Redis | 10 minutes |
|
|
35
|
+
| Linked Accounts | PostgreSQL | Permanent |
|
|
36
|
+
| Access Tokens | PostgreSQL | Per provider |
|
|
37
|
+
| Refresh Tokens | PostgreSQL | Permanent |
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Configuration
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createOAuthService } from 'servcraft/modules/oauth';
|
|
45
|
+
|
|
46
|
+
const oauthService = createOAuthService({
|
|
47
|
+
callbackBaseUrl: 'https://myapp.com/auth',
|
|
48
|
+
google: {
|
|
49
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
50
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
51
|
+
scopes: ['openid', 'email', 'profile'],
|
|
52
|
+
},
|
|
53
|
+
facebook: {
|
|
54
|
+
clientId: process.env.FACEBOOK_APP_ID!,
|
|
55
|
+
clientSecret: process.env.FACEBOOK_APP_SECRET!,
|
|
56
|
+
scopes: ['email', 'public_profile'],
|
|
57
|
+
},
|
|
58
|
+
github: {
|
|
59
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
60
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
61
|
+
scopes: ['user:email', 'read:user'],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### OAuth Flow
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// 1. Generate authorization URL
|
|
70
|
+
const { url, state } = await oauthService.getAuthorizationUrl('google');
|
|
71
|
+
// Redirect user to url
|
|
72
|
+
|
|
73
|
+
// 2. Handle callback (after user authorizes)
|
|
74
|
+
const oauthUser = await oauthService.handleCallback('google', code, state);
|
|
75
|
+
// oauthUser: { provider, providerAccountId, email, name, picture, accessToken }
|
|
76
|
+
|
|
77
|
+
// 3. Find or create user
|
|
78
|
+
let userId = await oauthService.findUserByOAuth('google', oauthUser.providerAccountId);
|
|
79
|
+
|
|
80
|
+
if (!userId) {
|
|
81
|
+
// Create new user
|
|
82
|
+
const user = await userService.create({ email: oauthUser.email, name: oauthUser.name });
|
|
83
|
+
userId = user.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Link OAuth account to user
|
|
87
|
+
await oauthService.linkAccount(userId, oauthUser);
|
|
88
|
+
|
|
89
|
+
// 5. Generate session/JWT for user
|
|
90
|
+
const tokens = await authService.generateTokens(userId);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Account Management
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Get user's linked accounts
|
|
97
|
+
const accounts = await oauthService.getUserLinkedAccounts(userId);
|
|
98
|
+
// [{ provider: 'google', email: '...', picture: '...' }, ...]
|
|
99
|
+
|
|
100
|
+
// Check if provider is linked
|
|
101
|
+
const googleAccount = accounts.find(a => a.provider === 'google');
|
|
102
|
+
|
|
103
|
+
// Unlink account
|
|
104
|
+
await oauthService.unlinkAccount(userId, 'facebook');
|
|
105
|
+
|
|
106
|
+
// Refresh tokens (for API access)
|
|
107
|
+
const refreshed = await oauthService.refreshTokens(linkedAccountId);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Provider Utilities
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Get supported providers
|
|
114
|
+
const providers = oauthService.getSupportedProviders();
|
|
115
|
+
// ['google', 'facebook', 'github']
|
|
116
|
+
|
|
117
|
+
// Check if provider is enabled
|
|
118
|
+
if (oauthService.isProviderEnabled('google')) {
|
|
119
|
+
// Show Google login button
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Configuration Types
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
interface OAuthConfig {
|
|
127
|
+
callbackBaseUrl: string;
|
|
128
|
+
google?: {
|
|
129
|
+
clientId: string;
|
|
130
|
+
clientSecret: string;
|
|
131
|
+
scopes?: string[];
|
|
132
|
+
};
|
|
133
|
+
facebook?: {
|
|
134
|
+
clientId: string;
|
|
135
|
+
clientSecret: string;
|
|
136
|
+
scopes?: string[];
|
|
137
|
+
};
|
|
138
|
+
github?: {
|
|
139
|
+
clientId: string;
|
|
140
|
+
clientSecret: string;
|
|
141
|
+
scopes?: string[];
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface OAuthUser {
|
|
146
|
+
provider: OAuthProvider;
|
|
147
|
+
providerAccountId: string;
|
|
148
|
+
email?: string;
|
|
149
|
+
name?: string;
|
|
150
|
+
picture?: string;
|
|
151
|
+
accessToken?: string;
|
|
152
|
+
refreshToken?: string;
|
|
153
|
+
expiresAt?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface LinkedAccount {
|
|
157
|
+
id: string;
|
|
158
|
+
userId: string;
|
|
159
|
+
provider: OAuthProvider;
|
|
160
|
+
providerAccountId: string;
|
|
161
|
+
email?: string;
|
|
162
|
+
name?: string;
|
|
163
|
+
picture?: string;
|
|
164
|
+
accessToken?: string;
|
|
165
|
+
refreshToken?: string;
|
|
166
|
+
expiresAt?: Date;
|
|
167
|
+
createdAt: Date;
|
|
168
|
+
updatedAt: Date;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Fastify Routes Example
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// GET /auth/google
|
|
176
|
+
fastify.get('/auth/google', async (request, reply) => {
|
|
177
|
+
const { url, state } = await oauthService.getAuthorizationUrl('google');
|
|
178
|
+
reply.redirect(url);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// GET /auth/google/callback
|
|
182
|
+
fastify.get('/auth/google/callback', async (request, reply) => {
|
|
183
|
+
const { code, state } = request.query as { code: string; state: string };
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const oauthUser = await oauthService.handleCallback('google', code, state);
|
|
187
|
+
|
|
188
|
+
// Find or create user
|
|
189
|
+
let userId = await oauthService.findUserByOAuth('google', oauthUser.providerAccountId);
|
|
190
|
+
|
|
191
|
+
if (!userId) {
|
|
192
|
+
const user = await userService.create({
|
|
193
|
+
email: oauthUser.email!,
|
|
194
|
+
name: oauthUser.name,
|
|
195
|
+
emailVerified: true, // OAuth emails are verified
|
|
196
|
+
});
|
|
197
|
+
userId = user.id;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await oauthService.linkAccount(userId, oauthUser);
|
|
201
|
+
|
|
202
|
+
const tokens = await authService.generateTokens(userId);
|
|
203
|
+
reply.redirect(`/dashboard?token=${tokens.accessToken}`);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
reply.redirect('/login?error=oauth_failed');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Redis Key Structure
|
|
211
|
+
|
|
212
|
+
| Key Pattern | Purpose | TTL |
|
|
213
|
+
|-------------|---------|-----|
|
|
214
|
+
| `oauth:state:{state}` | CSRF state with PKCE verifier | 10 min |
|
|
215
|
+
|
|
216
|
+
## Security Considerations
|
|
217
|
+
|
|
218
|
+
1. **State Validation** - Always validate state parameter to prevent CSRF
|
|
219
|
+
2. **PKCE** - Use PKCE for public clients (mobile, SPA)
|
|
220
|
+
3. **Token Storage** - Store tokens encrypted in database
|
|
221
|
+
4. **Scope Minimization** - Request only necessary scopes
|
|
222
|
+
5. **Token Refresh** - Implement automatic token refresh for long-lived access
|
|
223
|
+
|
|
224
|
+
## Error Handling
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
try {
|
|
228
|
+
await oauthService.handleCallback('google', code, state);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.message === 'Invalid or expired OAuth state') {
|
|
231
|
+
// User took too long or CSRF attack
|
|
232
|
+
}
|
|
233
|
+
if (error.message === 'This account is already linked to another user') {
|
|
234
|
+
// OAuth account already linked
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|