servcraft 0.1.0 → 0.1.1
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 +29 -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/README.md +1070 -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,504 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { logger } from '../../core/logger.js';
|
|
3
|
+
import type {
|
|
4
|
+
ChatMessage,
|
|
5
|
+
PresenceStatus,
|
|
6
|
+
Notification,
|
|
7
|
+
TypingIndicator,
|
|
8
|
+
LiveEvent,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
import type { WebSocketService } from './websocket.service.js';
|
|
11
|
+
|
|
12
|
+
// Storage
|
|
13
|
+
const presenceStatus = new Map<string, PresenceStatus>();
|
|
14
|
+
const notifications = new Map<string, Notification[]>();
|
|
15
|
+
const typingIndicators = new Map<string, Map<string, TypingIndicator>>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Chat Feature
|
|
19
|
+
* Real-time chat functionality
|
|
20
|
+
*/
|
|
21
|
+
export class ChatFeature {
|
|
22
|
+
constructor(private ws: WebSocketService) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send chat message
|
|
26
|
+
*/
|
|
27
|
+
async sendMessage(
|
|
28
|
+
roomId: string,
|
|
29
|
+
userId: string,
|
|
30
|
+
content: string,
|
|
31
|
+
options?: {
|
|
32
|
+
replyTo?: string;
|
|
33
|
+
mentions?: string[];
|
|
34
|
+
attachments?: ChatMessage['attachments'];
|
|
35
|
+
}
|
|
36
|
+
): Promise<ChatMessage> {
|
|
37
|
+
const message = (await this.ws.sendMessage(
|
|
38
|
+
roomId,
|
|
39
|
+
userId,
|
|
40
|
+
content,
|
|
41
|
+
'text',
|
|
42
|
+
options
|
|
43
|
+
)) as ChatMessage;
|
|
44
|
+
|
|
45
|
+
message.replyTo = options?.replyTo;
|
|
46
|
+
message.mentions = options?.mentions;
|
|
47
|
+
message.attachments = options?.attachments;
|
|
48
|
+
|
|
49
|
+
// Broadcast to room
|
|
50
|
+
await this.ws.broadcastToRoom(roomId, 'chat:message', message);
|
|
51
|
+
|
|
52
|
+
// Notify mentioned users
|
|
53
|
+
if (options?.mentions) {
|
|
54
|
+
await this.notifyMentions(options.mentions, message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
logger.debug({ messageId: message.id, roomId, userId }, 'Chat message sent');
|
|
58
|
+
|
|
59
|
+
return message;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Edit message
|
|
64
|
+
*/
|
|
65
|
+
async editMessage(
|
|
66
|
+
messageId: string,
|
|
67
|
+
roomId: string,
|
|
68
|
+
userId: string,
|
|
69
|
+
newContent: string
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const messages = this.ws.getRoomMessages(roomId, 1000);
|
|
72
|
+
const message = messages.find((m) => m.id === messageId);
|
|
73
|
+
|
|
74
|
+
if (!message) {
|
|
75
|
+
throw new Error('Message not found');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (message.userId !== userId) {
|
|
79
|
+
throw new Error('Unauthorized');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
message.content = newContent;
|
|
83
|
+
message.edited = true;
|
|
84
|
+
message.editedAt = new Date();
|
|
85
|
+
|
|
86
|
+
await this.ws.broadcastToRoom(roomId, 'chat:message:edited', {
|
|
87
|
+
messageId,
|
|
88
|
+
content: newContent,
|
|
89
|
+
editedAt: message.editedAt,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
logger.debug({ messageId, roomId, userId }, 'Message edited');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete message
|
|
97
|
+
*/
|
|
98
|
+
async deleteMessage(messageId: string, roomId: string, userId: string): Promise<void> {
|
|
99
|
+
const messages = this.ws.getRoomMessages(roomId, 1000);
|
|
100
|
+
const messageIndex = messages.findIndex((m) => m.id === messageId);
|
|
101
|
+
|
|
102
|
+
if (messageIndex === -1) {
|
|
103
|
+
throw new Error('Message not found');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const message = messages[messageIndex];
|
|
107
|
+
|
|
108
|
+
if (!message || message.userId !== userId) {
|
|
109
|
+
throw new Error('Unauthorized');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
messages.splice(messageIndex, 1);
|
|
113
|
+
|
|
114
|
+
await this.ws.broadcastToRoom(roomId, 'chat:message:deleted', {
|
|
115
|
+
messageId,
|
|
116
|
+
roomId,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
logger.debug({ messageId, roomId, userId }, 'Message deleted');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start typing indicator
|
|
124
|
+
*/
|
|
125
|
+
async startTyping(roomId: string, userId: string, username?: string): Promise<void> {
|
|
126
|
+
if (!typingIndicators.has(roomId)) {
|
|
127
|
+
typingIndicators.set(roomId, new Map());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const roomTyping = typingIndicators.get(roomId)!;
|
|
131
|
+
roomTyping.set(userId, {
|
|
132
|
+
userId,
|
|
133
|
+
username,
|
|
134
|
+
roomId,
|
|
135
|
+
timestamp: new Date(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await this.ws.broadcastToRoom(
|
|
139
|
+
roomId,
|
|
140
|
+
'chat:typing:start',
|
|
141
|
+
{ userId, username },
|
|
142
|
+
{ except: this.ws.getUserSockets(userId) }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
logger.debug({ roomId, userId }, 'User started typing');
|
|
146
|
+
|
|
147
|
+
// Auto-stop after 5 seconds
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
this.stopTyping(roomId, userId);
|
|
150
|
+
}, 5000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Stop typing indicator
|
|
155
|
+
*/
|
|
156
|
+
async stopTyping(roomId: string, userId: string): Promise<void> {
|
|
157
|
+
const roomTyping = typingIndicators.get(roomId);
|
|
158
|
+
|
|
159
|
+
if (roomTyping) {
|
|
160
|
+
roomTyping.delete(userId);
|
|
161
|
+
|
|
162
|
+
await this.ws.broadcastToRoom(
|
|
163
|
+
roomId,
|
|
164
|
+
'chat:typing:stop',
|
|
165
|
+
{ userId },
|
|
166
|
+
{ except: this.ws.getUserSockets(userId) }
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
logger.debug({ roomId, userId }, 'User stopped typing');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get typing users in room
|
|
175
|
+
*/
|
|
176
|
+
getTypingUsers(roomId: string): TypingIndicator[] {
|
|
177
|
+
const roomTyping = typingIndicators.get(roomId);
|
|
178
|
+
|
|
179
|
+
if (!roomTyping) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Array.from(roomTyping.values());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Notify mentioned users
|
|
188
|
+
*/
|
|
189
|
+
private async notifyMentions(userIds: string[], message: ChatMessage): Promise<void> {
|
|
190
|
+
for (const userId of userIds) {
|
|
191
|
+
if (this.ws.isUserOnline(userId)) {
|
|
192
|
+
await this.ws.broadcastToUsers([userId], 'chat:mention', {
|
|
193
|
+
messageId: message.id,
|
|
194
|
+
roomId: message.roomId,
|
|
195
|
+
from: message.username,
|
|
196
|
+
content: message.content,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Presence Feature
|
|
205
|
+
* User online/offline status tracking
|
|
206
|
+
*/
|
|
207
|
+
export class PresenceFeature {
|
|
208
|
+
constructor(private ws: WebSocketService) {
|
|
209
|
+
// Listen to connection events
|
|
210
|
+
ws.on('connection', (user) => {
|
|
211
|
+
this.updateStatus(user.id, 'online');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
ws.on('disconnect', (user) => {
|
|
215
|
+
this.updateStatus(user.id, 'offline');
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update user status
|
|
221
|
+
*/
|
|
222
|
+
async updateStatus(
|
|
223
|
+
userId: string,
|
|
224
|
+
status: PresenceStatus['status'],
|
|
225
|
+
metadata?: Record<string, unknown>
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const presence: PresenceStatus = {
|
|
228
|
+
userId,
|
|
229
|
+
status,
|
|
230
|
+
lastSeen: new Date(),
|
|
231
|
+
metadata,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
presenceStatus.set(userId, presence);
|
|
235
|
+
|
|
236
|
+
// Broadcast status change
|
|
237
|
+
await this.ws.broadcastToAll('presence:status', presence);
|
|
238
|
+
|
|
239
|
+
logger.debug({ userId, status }, 'Presence status updated');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get user status
|
|
244
|
+
*/
|
|
245
|
+
getStatus(userId: string): PresenceStatus | null {
|
|
246
|
+
return presenceStatus.get(userId) || null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get multiple user statuses
|
|
251
|
+
*/
|
|
252
|
+
getStatuses(userIds: string[]): PresenceStatus[] {
|
|
253
|
+
return userIds
|
|
254
|
+
.map((userId) => presenceStatus.get(userId))
|
|
255
|
+
.filter((status): status is PresenceStatus => status !== undefined);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get all online users
|
|
260
|
+
*/
|
|
261
|
+
getOnlineUsers(): PresenceStatus[] {
|
|
262
|
+
return Array.from(presenceStatus.values()).filter((status) => status.status === 'online');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Subscribe to user status changes
|
|
267
|
+
*/
|
|
268
|
+
async subscribeToUsers(socketId: string, userIds: string[]): Promise<void> {
|
|
269
|
+
// Send current statuses
|
|
270
|
+
const statuses = this.getStatuses(userIds);
|
|
271
|
+
|
|
272
|
+
await this.ws.emitToSocket(socketId, 'presence:statuses', statuses);
|
|
273
|
+
|
|
274
|
+
logger.debug({ socketId, userCount: userIds.length }, 'Subscribed to user statuses');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Notification Feature
|
|
280
|
+
* Real-time notifications
|
|
281
|
+
*/
|
|
282
|
+
export class NotificationFeature {
|
|
283
|
+
constructor(private ws: WebSocketService) {}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Send notification to user
|
|
287
|
+
*/
|
|
288
|
+
async send(
|
|
289
|
+
userId: string,
|
|
290
|
+
type: string,
|
|
291
|
+
title: string,
|
|
292
|
+
message: string,
|
|
293
|
+
data?: Record<string, unknown>
|
|
294
|
+
): Promise<Notification> {
|
|
295
|
+
const notification: Notification = {
|
|
296
|
+
id: randomUUID(),
|
|
297
|
+
userId,
|
|
298
|
+
type,
|
|
299
|
+
title,
|
|
300
|
+
message,
|
|
301
|
+
data,
|
|
302
|
+
read: false,
|
|
303
|
+
createdAt: new Date(),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Store notification
|
|
307
|
+
if (!notifications.has(userId)) {
|
|
308
|
+
notifications.set(userId, []);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
notifications.get(userId)!.push(notification);
|
|
312
|
+
|
|
313
|
+
// Send to user if online
|
|
314
|
+
if (this.ws.isUserOnline(userId)) {
|
|
315
|
+
await this.ws.broadcastToUsers([userId], 'notification:new', notification);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
logger.debug({ notificationId: notification.id, userId, type }, 'Notification sent');
|
|
319
|
+
|
|
320
|
+
return notification;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Send bulk notifications
|
|
325
|
+
*/
|
|
326
|
+
async sendBulk(
|
|
327
|
+
userIds: string[],
|
|
328
|
+
type: string,
|
|
329
|
+
title: string,
|
|
330
|
+
message: string,
|
|
331
|
+
data?: Record<string, unknown>
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
const promises = userIds.map((userId) => this.send(userId, type, title, message, data));
|
|
334
|
+
|
|
335
|
+
await Promise.all(promises);
|
|
336
|
+
|
|
337
|
+
logger.info({ userCount: userIds.length, type }, 'Bulk notifications sent');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Mark notification as read
|
|
342
|
+
*/
|
|
343
|
+
async markAsRead(userId: string, notificationId: string): Promise<void> {
|
|
344
|
+
const userNotifications = notifications.get(userId);
|
|
345
|
+
|
|
346
|
+
if (!userNotifications) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const notification = userNotifications.find((n) => n.id === notificationId);
|
|
351
|
+
|
|
352
|
+
if (notification) {
|
|
353
|
+
notification.read = true;
|
|
354
|
+
|
|
355
|
+
await this.ws.broadcastToUsers([userId], 'notification:read', {
|
|
356
|
+
notificationId,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
logger.debug({ notificationId, userId }, 'Notification marked as read');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Mark all as read
|
|
365
|
+
*/
|
|
366
|
+
async markAllAsRead(userId: string): Promise<void> {
|
|
367
|
+
const userNotifications = notifications.get(userId);
|
|
368
|
+
|
|
369
|
+
if (!userNotifications) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
userNotifications.forEach((notification) => {
|
|
374
|
+
notification.read = true;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await this.ws.broadcastToUsers([userId], 'notification:all_read', {});
|
|
378
|
+
|
|
379
|
+
logger.debug({ userId, count: userNotifications.length }, 'All notifications marked as read');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get user notifications
|
|
384
|
+
*/
|
|
385
|
+
getNotifications(userId: string, unreadOnly = false): Notification[] {
|
|
386
|
+
const userNotifications = notifications.get(userId) || [];
|
|
387
|
+
|
|
388
|
+
if (unreadOnly) {
|
|
389
|
+
return userNotifications.filter((n) => !n.read);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return userNotifications;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get unread count
|
|
397
|
+
*/
|
|
398
|
+
getUnreadCount(userId: string): number {
|
|
399
|
+
const userNotifications = notifications.get(userId) || [];
|
|
400
|
+
|
|
401
|
+
return userNotifications.filter((n) => !n.read).length;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Delete notification
|
|
406
|
+
*/
|
|
407
|
+
async deleteNotification(userId: string, notificationId: string): Promise<void> {
|
|
408
|
+
const userNotifications = notifications.get(userId);
|
|
409
|
+
|
|
410
|
+
if (!userNotifications) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const index = userNotifications.findIndex((n) => n.id === notificationId);
|
|
415
|
+
|
|
416
|
+
if (index !== -1) {
|
|
417
|
+
userNotifications.splice(index, 1);
|
|
418
|
+
|
|
419
|
+
await this.ws.broadcastToUsers([userId], 'notification:deleted', {
|
|
420
|
+
notificationId,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
logger.debug({ notificationId, userId }, 'Notification deleted');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Clear all notifications
|
|
429
|
+
*/
|
|
430
|
+
async clearAll(userId: string): Promise<void> {
|
|
431
|
+
notifications.set(userId, []);
|
|
432
|
+
|
|
433
|
+
await this.ws.broadcastToUsers([userId], 'notification:cleared', {});
|
|
434
|
+
|
|
435
|
+
logger.debug({ userId }, 'All notifications cleared');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Live Events Feature
|
|
441
|
+
* Broadcast live events (analytics, updates, etc.)
|
|
442
|
+
*/
|
|
443
|
+
export class LiveEventsFeature {
|
|
444
|
+
constructor(private ws: WebSocketService) {}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Broadcast live event
|
|
448
|
+
*/
|
|
449
|
+
async broadcast(
|
|
450
|
+
type: string,
|
|
451
|
+
data: Record<string, unknown>,
|
|
452
|
+
source?: string,
|
|
453
|
+
targetUsers?: string[]
|
|
454
|
+
): Promise<void> {
|
|
455
|
+
const event: LiveEvent = {
|
|
456
|
+
id: randomUUID(),
|
|
457
|
+
type,
|
|
458
|
+
data,
|
|
459
|
+
timestamp: new Date(),
|
|
460
|
+
source,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (targetUsers && targetUsers.length > 0) {
|
|
464
|
+
await this.ws.broadcastToUsers(targetUsers, 'live:event', event);
|
|
465
|
+
} else {
|
|
466
|
+
await this.ws.broadcastToAll('live:event', event);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
logger.debug(
|
|
470
|
+
{ eventId: event.id, type, targetCount: targetUsers?.length },
|
|
471
|
+
'Live event broadcasted'
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Broadcast analytics event
|
|
477
|
+
*/
|
|
478
|
+
async broadcastAnalytics(
|
|
479
|
+
metric: string,
|
|
480
|
+
value: number,
|
|
481
|
+
metadata?: Record<string, unknown>
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
await this.broadcast('analytics', {
|
|
484
|
+
metric,
|
|
485
|
+
value,
|
|
486
|
+
...metadata,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Broadcast system update
|
|
492
|
+
*/
|
|
493
|
+
async broadcastSystemUpdate(
|
|
494
|
+
message: string,
|
|
495
|
+
severity: 'info' | 'warning' | 'error',
|
|
496
|
+
metadata?: Record<string, unknown>
|
|
497
|
+
): Promise<void> {
|
|
498
|
+
await this.broadcast('system_update', {
|
|
499
|
+
message,
|
|
500
|
+
severity,
|
|
501
|
+
...metadata,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket/Real-time Module
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time communication with Socket.io:
|
|
5
|
+
* - Real-time chat with typing indicators
|
|
6
|
+
* - User presence tracking (online/offline)
|
|
7
|
+
* - Live notifications
|
|
8
|
+
* - Room management
|
|
9
|
+
* - Live events broadcasting
|
|
10
|
+
* - Authentication & authorization middlewares
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import {
|
|
15
|
+
* WebSocketService,
|
|
16
|
+
* ChatFeature,
|
|
17
|
+
* PresenceFeature,
|
|
18
|
+
* NotificationFeature,
|
|
19
|
+
* authMiddleware
|
|
20
|
+
* } from './modules/websocket';
|
|
21
|
+
*
|
|
22
|
+
* // Create WebSocket service
|
|
23
|
+
* const wsService = new WebSocketService({
|
|
24
|
+
* cors: { origin: 'http://localhost:3000' },
|
|
25
|
+
* redis: { host: 'localhost', port: 6379 }
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Initialize with HTTP server
|
|
29
|
+
* wsService.initialize(httpServer);
|
|
30
|
+
*
|
|
31
|
+
* // Create features
|
|
32
|
+
* const chat = new ChatFeature(wsService);
|
|
33
|
+
* const presence = new PresenceFeature(wsService);
|
|
34
|
+
* const notifications = new NotificationFeature(wsService);
|
|
35
|
+
*
|
|
36
|
+
* // Send chat message
|
|
37
|
+
* await chat.sendMessage('room-123', 'user-456', 'Hello!', {
|
|
38
|
+
* mentions: ['user-789']
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Send notification
|
|
42
|
+
* await notifications.send(
|
|
43
|
+
* 'user-123',
|
|
44
|
+
* 'message',
|
|
45
|
+
* 'New Message',
|
|
46
|
+
* 'You have a new message from John'
|
|
47
|
+
* );
|
|
48
|
+
*
|
|
49
|
+
* // Broadcast live event
|
|
50
|
+
* await wsService.broadcastToAll('analytics:update', {
|
|
51
|
+
* metric: 'active_users',
|
|
52
|
+
* value: 1250
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* ## Client-side Integration
|
|
57
|
+
*
|
|
58
|
+
* ```typescript
|
|
59
|
+
* import { io } from 'socket.io-client';
|
|
60
|
+
*
|
|
61
|
+
* const socket = io('http://localhost:3000', {
|
|
62
|
+
* auth: { token: 'your-jwt-token' },
|
|
63
|
+
* query: { username: 'john', namespace: 'chat' }
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Listen for events
|
|
67
|
+
* socket.on('chat:message', (message) => {
|
|
68
|
+
* console.log('New message:', message);
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* socket.on('presence:status', (status) => {
|
|
72
|
+
* console.log('User status changed:', status);
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* socket.on('notification:new', (notification) => {
|
|
76
|
+
* console.log('New notification:', notification);
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
// Types
|
|
82
|
+
export * from './types.js';
|
|
83
|
+
|
|
84
|
+
// Service
|
|
85
|
+
export { WebSocketService } from './websocket.service.js';
|
|
86
|
+
|
|
87
|
+
// Features
|
|
88
|
+
export {
|
|
89
|
+
ChatFeature,
|
|
90
|
+
PresenceFeature,
|
|
91
|
+
NotificationFeature,
|
|
92
|
+
LiveEventsFeature,
|
|
93
|
+
} from './features.js';
|
|
94
|
+
|
|
95
|
+
// Middlewares
|
|
96
|
+
export {
|
|
97
|
+
authMiddleware,
|
|
98
|
+
rateLimitMiddleware,
|
|
99
|
+
corsMiddleware,
|
|
100
|
+
loggingMiddleware,
|
|
101
|
+
validationMiddleware,
|
|
102
|
+
roleMiddleware,
|
|
103
|
+
namespaceMiddleware,
|
|
104
|
+
throttleMiddleware,
|
|
105
|
+
errorMiddleware,
|
|
106
|
+
} from './middlewares.js';
|