@rebasepro/server-mongo 0.17.3 → 0.18.0

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.
Files changed (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +4 -0
  3. package/dist/MongoBootstrapper.d.ts +0 -1
  4. package/dist/auth/ensure-collections.d.ts +0 -1
  5. package/dist/auth/services.d.ts +0 -1
  6. package/dist/connection.d.ts +0 -1
  7. package/dist/db/MongoConditionBuilder.d.ts +0 -1
  8. package/dist/db/MongoDataService.d.ts +0 -1
  9. package/dist/db/securityRuleFilter.d.ts +0 -1
  10. package/dist/factory.d.ts +0 -1
  11. package/dist/history/ensure-history-collection.d.ts +0 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.es.js +83 -79
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/plan-schema-change.d.ts +0 -1
  16. package/dist/services/MongoDriver.d.ts +0 -1
  17. package/dist/services/MongoHistoryService.d.ts +0 -1
  18. package/dist/services/MongoRealtimeService.d.ts +0 -1
  19. package/dist/websocket.d.ts +0 -1
  20. package/package.json +28 -24
  21. package/dist/MongoBootstrapper.d.ts.map +0 -1
  22. package/dist/auth/ensure-collections.d.ts.map +0 -1
  23. package/dist/auth/services.d.ts.map +0 -1
  24. package/dist/connection.d.ts.map +0 -1
  25. package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
  26. package/dist/db/MongoDataService.d.ts.map +0 -1
  27. package/dist/db/securityRuleFilter.d.ts.map +0 -1
  28. package/dist/factory.d.ts.map +0 -1
  29. package/dist/history/ensure-history-collection.d.ts.map +0 -1
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/schema/plan-schema-change.d.ts.map +0 -1
  32. package/dist/services/MongoDriver.d.ts.map +0 -1
  33. package/dist/services/MongoHistoryService.d.ts.map +0 -1
  34. package/dist/services/MongoRealtimeService.d.ts.map +0 -1
  35. package/dist/websocket.d.ts.map +0 -1
  36. package/src/MongoBootstrapper.ts +0 -204
  37. package/src/auth/ensure-collections.ts +0 -153
  38. package/src/auth/services.ts +0 -866
  39. package/src/connection.ts +0 -60
  40. package/src/db/MongoConditionBuilder.ts +0 -348
  41. package/src/db/MongoDataService.ts +0 -412
  42. package/src/db/securityRuleFilter.ts +0 -398
  43. package/src/factory.ts +0 -331
  44. package/src/history/ensure-history-collection.ts +0 -22
  45. package/src/index.ts +0 -25
  46. package/src/schema/plan-schema-change.ts +0 -159
  47. package/src/services/MongoDriver.ts +0 -950
  48. package/src/services/MongoHistoryService.ts +0 -186
  49. package/src/services/MongoRealtimeService.ts +0 -592
  50. package/src/websocket.ts +0 -387
@@ -1,866 +0,0 @@
1
- import { Db, ObjectId } from "mongodb";
2
- import { normalizeEmail } from "@rebasepro/common";
3
-
4
- /** Loose document type that allows string _id values (Rebase convention). */
5
- export interface MongoDoc { _id?: string; [key: string]: any; }
6
- import {
7
- UserRepository,
8
- RoleRepository,
9
- TokenRepository,
10
- AuthRepository,
11
- UserData,
12
- CreateUserData,
13
- RoleData,
14
- CreateRoleData,
15
- RefreshTokenInfo,
16
- RefreshTokenSession,
17
- PasswordResetTokenInfo,
18
- MagicLinkTokenInfo,
19
- UserIdentityData,
20
- ListUsersOptions,
21
- PaginatedUsersResult,
22
- MfaFactor,
23
- MfaChallengeInfo,
24
- ApiError
25
- } from "@rebasepro/server";
26
-
27
- export type Role = RoleData;
28
-
29
- function escapeRegExp(str: string): string {
30
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
- }
32
-
33
- function toUser(doc: any): UserData {
34
- return {
35
- id: doc._id || doc.id,
36
- email: doc.email,
37
- passwordHash: doc.passwordHash ?? null,
38
- displayName: doc.displayName ?? null,
39
- photoUrl: doc.photoUrl ?? null,
40
- emailVerified: doc.emailVerified ?? false,
41
- emailVerificationToken: doc.emailVerificationToken ?? null,
42
- emailVerificationSentAt: doc.emailVerificationSentAt ? new Date(doc.emailVerificationSentAt) : null,
43
- createdAt: new Date(doc.createdAt),
44
- updatedAt: new Date(doc.updatedAt)
45
- };
46
- }
47
-
48
- export class MongoUserService implements UserRepository {
49
- constructor(private db: Db) {}
50
-
51
- private get collection() {
52
- return this.db.collection<MongoDoc>("rebase_users");
53
- }
54
-
55
- private get identitiesCollection() {
56
- return this.db.collection<MongoDoc>("rebase_user_identities");
57
- }
58
-
59
- private get userRolesCollection() {
60
- return this.db.collection<MongoDoc>("rebase_user_roles");
61
- }
62
-
63
- private get rolesCollection() {
64
- return this.db.collection<MongoDoc>("rebase_roles");
65
- }
66
-
67
- async createUser(data: CreateUserData): Promise<UserData> {
68
- const id = new ObjectId().toString();
69
- const now = new Date();
70
- const doc = {
71
- _id: id,
72
- id,
73
- email: normalizeEmail(data.email),
74
- passwordHash: data.passwordHash ?? null,
75
- displayName: data.displayName ?? null,
76
- photoUrl: data.photoUrl ?? null,
77
- emailVerified: data.emailVerified ?? false,
78
- createdAt: now,
79
- updatedAt: now
80
- };
81
- try {
82
- await this.collection.insertOne(doc);
83
- } catch (error) {
84
- // 11000 is Mongo's duplicate key, and the unique index on `email`
85
- // in `ensure-collections.ts` is what raises it. Same answer as
86
- // Postgres gives for its 23505, and the same answer the route
87
- // gives when its pre-check sees the row — see
88
- // `UserRepository.createUser`.
89
- if ((error as { code?: number })?.code === 11000) {
90
- throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
91
- }
92
- throw error;
93
- }
94
- return toUser(doc);
95
- }
96
-
97
- async getUserById(id: string): Promise<UserData | null> {
98
- const doc = await this.collection.findOne({ id });
99
- return doc ? toUser(doc) : null;
100
- }
101
-
102
- async getUserByEmail(email: string): Promise<UserData | null> {
103
- const doc = await this.collection.findOne({ email: normalizeEmail(email) });
104
- return doc ? toUser(doc) : null;
105
- }
106
-
107
- async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {
108
- const identity = await this.identitiesCollection.findOne({ provider,
109
- providerId });
110
- if (!identity) return null;
111
- return this.getUserById(identity.uid);
112
- }
113
-
114
- async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
115
- const docs = await this.identitiesCollection.find({ uid }).toArray();
116
- return docs.map(doc => ({
117
- id: doc.id,
118
- uid: doc.uid,
119
- provider: doc.provider,
120
- providerId: doc.providerId,
121
- profileData: doc.profileData ?? null,
122
- createdAt: new Date(doc.createdAt),
123
- updatedAt: new Date(doc.updatedAt)
124
- }));
125
- }
126
-
127
- async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
128
- const now = new Date();
129
- await this.identitiesCollection.updateOne(
130
- { provider,
131
- providerId },
132
- {
133
- $setOnInsert: {
134
- _id: new ObjectId().toString(),
135
- id: new ObjectId().toString(),
136
- uid,
137
- provider,
138
- providerId,
139
- createdAt: now
140
- },
141
- $set: {
142
- profileData: profileData ?? null,
143
- updatedAt: now
144
- }
145
- },
146
- { upsert: true }
147
- );
148
- }
149
-
150
- async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
151
- const updateData: Record<string, unknown> = { ...data,
152
- updatedAt: new Date() };
153
- if (typeof updateData.email === "string") updateData.email = normalizeEmail(updateData.email);
154
-
155
- await this.collection.updateOne({ id }, { $set: updateData });
156
- return this.getUserById(id);
157
- }
158
-
159
- async deleteUser(id: string): Promise<void> {
160
- await this.collection.deleteOne({ id });
161
- await this.identitiesCollection.deleteMany({ uid: id });
162
- await this.userRolesCollection.deleteMany({ uid: id });
163
- }
164
-
165
- async listUsers(): Promise<UserData[]> {
166
- const docs = await this.collection.find().toArray();
167
- return docs.map(toUser);
168
- }
169
-
170
- async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
171
- const limit = options?.limit ?? 25;
172
- const offset = options?.offset ?? 0;
173
- const search = options?.search?.trim() || "";
174
- const orderBy = options?.orderBy || "createdAt";
175
- const orderDir = options?.orderDir || "desc";
176
- const roleId = options?.roleId;
177
-
178
- const query: Record<string, unknown> = {};
179
-
180
- if (search) {
181
- const escapedSearch = escapeRegExp(search);
182
- query.$or = [
183
- { email: { $regex: escapedSearch,
184
- $options: "i" } },
185
- { displayName: { $regex: escapedSearch,
186
- $options: "i" } }
187
- ];
188
- }
189
-
190
- if (roleId) {
191
- const userRoles = await this.userRolesCollection.find({ roleId }).toArray();
192
- const userIds = userRoles.map(ur => ur.uid);
193
- query.id = { $in: userIds };
194
- }
195
-
196
- const sort: Record<string, 1 | -1> = {};
197
- sort[orderBy] = orderDir === "asc" ? 1 : -1;
198
-
199
- const total = await this.collection.countDocuments(query);
200
- const docs = await this.collection.find(query).sort(sort).skip(offset).limit(limit).toArray();
201
-
202
- return {
203
- users: docs.map(toUser),
204
- total,
205
- limit,
206
- offset
207
- };
208
- }
209
-
210
- async updatePassword(id: string, passwordHash: string): Promise<void> {
211
- await this.collection.updateOne(
212
- { id },
213
- { $set: { passwordHash,
214
- updatedAt: new Date() } }
215
- );
216
- }
217
-
218
- async setEmailVerified(id: string, verified: boolean): Promise<void> {
219
- await this.collection.updateOne(
220
- { id },
221
- { $set: { emailVerified: verified,
222
- emailVerificationToken: null,
223
- updatedAt: new Date() } }
224
- );
225
- }
226
-
227
- async setVerificationToken(id: string, token: string | null): Promise<void> {
228
- await this.collection.updateOne(
229
- { id },
230
- { $set: { emailVerificationToken: token,
231
- emailVerificationSentAt: token ? new Date() : null,
232
- updatedAt: new Date() } }
233
- );
234
- }
235
-
236
- async getUserByVerificationToken(token: string): Promise<UserData | null> {
237
- const doc = await this.collection.findOne({ emailVerificationToken: token });
238
- return doc ? toUser(doc) : null;
239
- }
240
-
241
- async getUserRoles(uid: string): Promise<RoleData[]> {
242
- const userRoles = await this.userRolesCollection.find({ uid }).toArray();
243
- const roleIds = userRoles.map(ur => ur.roleId);
244
- if (roleIds.length === 0) return [];
245
-
246
- const roles = await this.rolesCollection.find({ id: { $in: roleIds } }).toArray();
247
- return roles.map(r => ({
248
- id: r.id,
249
- name: r.name,
250
- isAdmin: r.isAdmin ?? false,
251
- defaultPermissions: r.defaultPermissions ?? null,
252
- collectionPermissions: r.collectionPermissions ?? null
253
- }));
254
- }
255
-
256
- async getUserRoleIds(uid: string): Promise<string[]> {
257
- const userRoles = await this.userRolesCollection.find({ uid }).toArray();
258
- return userRoles.map(ur => ur.roleId);
259
- }
260
-
261
- async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
262
- await this.userRolesCollection.deleteMany({ uid });
263
- if (roleIds.length > 0) {
264
- const docs = roleIds.map(roleId => ({
265
- _id: new ObjectId().toString(),
266
- uid,
267
- roleId
268
- }));
269
- await this.userRolesCollection.insertMany(docs);
270
- }
271
- }
272
-
273
- async assignDefaultRole(uid: string, roleId: string): Promise<void> {
274
- await this.userRolesCollection.updateOne(
275
- { uid,
276
- roleId },
277
- { $setOnInsert: { _id: new ObjectId().toString(),
278
- uid,
279
- roleId } },
280
- { upsert: true }
281
- );
282
- }
283
-
284
- async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
285
- const user = await this.getUserById(uid);
286
- if (!user) return null;
287
- const roles = await this.getUserRoles(uid);
288
- return { user,
289
- roles };
290
- }
291
- }
292
-
293
- export class MongoRoleService implements RoleRepository {
294
- constructor(private db: Db) {}
295
-
296
- private get collection() {
297
- return this.db.collection<MongoDoc>("rebase_roles");
298
- }
299
-
300
- async getRoleById(id: string): Promise<RoleData | null> {
301
- const doc = await this.collection.findOne({ id });
302
- if (!doc) return null;
303
- return {
304
- id: doc.id,
305
- name: doc.name,
306
- isAdmin: doc.isAdmin ?? false,
307
- defaultPermissions: doc.defaultPermissions ?? null,
308
- collectionPermissions: doc.collectionPermissions ?? null
309
- };
310
- }
311
-
312
- async listRoles(): Promise<RoleData[]> {
313
- const docs = await this.collection.find().sort({ name: 1 }).toArray();
314
- return docs.map(doc => ({
315
- id: doc.id,
316
- name: doc.name,
317
- isAdmin: doc.isAdmin ?? false,
318
- defaultPermissions: doc.defaultPermissions ?? null,
319
- collectionPermissions: doc.collectionPermissions ?? null
320
- }));
321
- }
322
-
323
- async createRole(data: CreateRoleData): Promise<RoleData> {
324
- const doc = {
325
- _id: data.id,
326
- id: data.id,
327
- name: data.name,
328
- isAdmin: data.isAdmin ?? false,
329
- defaultPermissions: data.defaultPermissions ?? null,
330
- collectionPermissions: data.collectionPermissions ?? null
331
- };
332
- await this.collection.insertOne(doc);
333
- return { ...doc } as RoleData;
334
- }
335
-
336
- async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
337
- await this.collection.updateOne({ id }, { $set: data });
338
- return this.getRoleById(id);
339
- }
340
-
341
- async deleteRole(id: string): Promise<void> {
342
- await this.collection.deleteOne({ id });
343
- await this.db.collection("rebase_user_roles").deleteMany({ roleId: id });
344
- }
345
- }
346
-
347
- export class MongoRefreshTokenService {
348
- constructor(private db: Db) {}
349
-
350
- private get collection() {
351
- return this.db.collection<MongoDoc>("rebase_refresh_tokens");
352
- }
353
-
354
- private toInfo(doc: MongoDoc): RefreshTokenInfo {
355
- return {
356
- id: doc.id,
357
- uid: doc.uid,
358
- tokenHash: doc.tokenHash,
359
- expiresAt: new Date(doc.expiresAt),
360
- createdAt: new Date(doc.createdAt),
361
- userAgent: doc.userAgent,
362
- ipAddress: doc.ipAddress,
363
- sessionId: doc.sessionId,
364
- rotatedAt: doc.rotatedAt ? new Date(doc.rotatedAt) : null,
365
- revoked: Boolean(doc.revoked),
366
- sessionStartedAt: new Date(doc.sessionStartedAt || doc.createdAt)
367
- };
368
- }
369
-
370
- async createToken(
371
- uid: string,
372
- tokenHash: string,
373
- expiresAt: Date,
374
- userAgent?: string,
375
- ipAddress?: string,
376
- session?: RefreshTokenSession
377
- ): Promise<void> {
378
- const safeUserAgent = userAgent || "";
379
- const safeIpAddress = ipAddress || "";
380
-
381
- // No deleteMany first. Tokens of one sign-in accumulate under a shared
382
- // sessionId and are pruned once nobody can still be holding them —
383
- // evicting by (uid, userAgent, ipAddress) is what used to sign out a
384
- // second browser profile behind the same address.
385
- const now = new Date();
386
- await this.collection.insertOne({
387
- _id: new ObjectId().toString(),
388
- id: new ObjectId().toString(),
389
- uid,
390
- tokenHash,
391
- expiresAt,
392
- createdAt: now,
393
- userAgent: safeUserAgent,
394
- ipAddress: safeIpAddress,
395
- sessionId: session?.id ?? new ObjectId().toString(),
396
- sessionStartedAt: session?.startedAt ?? now,
397
- rotatedAt: null,
398
- revoked: false
399
- });
400
- }
401
-
402
- async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
403
- const doc = await this.collection.findOne({ tokenHash });
404
- return doc ? this.toInfo(doc) : null;
405
- }
406
-
407
- /** Superseded, not gone — see the Postgres service for why that matters. */
408
- async markRotated(tokenHash: string): Promise<void> {
409
- await this.collection.updateOne({ tokenHash }, { $set: { rotatedAt: new Date() } });
410
- }
411
-
412
- async revokeSession(sessionId: string): Promise<void> {
413
- await this.collection.updateMany(
414
- { sessionId },
415
- { $set: { revoked: true, rotatedAt: new Date() } }
416
- );
417
- }
418
-
419
- async prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
420
- await this.collection.deleteMany({
421
- uid,
422
- $or: [
423
- { expiresAt: { $lt: new Date() } },
424
- { sessionId, rotatedAt: { $ne: null, $lt: supersededBefore } }
425
- ]
426
- });
427
- }
428
-
429
- async getTokensValidAfter(uid: string): Promise<Date | null> {
430
- const user = await this.db.collection<MongoDoc>("rebase_users").findOne({ id: uid });
431
- return user?.tokensValidAfter ? new Date(user.tokensValidAfter) : null;
432
- }
433
-
434
- async setTokensValidAfter(uid: string, at: Date): Promise<void> {
435
- await this.db.collection<MongoDoc>("rebase_users")
436
- .updateOne({ id: uid }, { $set: { tokensValidAfter: at } });
437
- }
438
-
439
- async deleteByHash(tokenHash: string): Promise<void> {
440
- await this.collection.deleteOne({ tokenHash });
441
- }
442
-
443
- async deleteAllForUser(uid: string): Promise<void> {
444
- await this.collection.deleteMany({ uid });
445
- }
446
-
447
- async listForUser(uid: string): Promise<RefreshTokenInfo[]> {
448
- const docs = await this.collection.find({ uid }).sort({ createdAt: 1 }).toArray();
449
- return docs.map(doc => this.toInfo(doc));
450
- }
451
-
452
- async deleteById(id: string, uid: string): Promise<void> {
453
- await this.collection.deleteOne({ id,
454
- uid });
455
- }
456
- }
457
-
458
- export class MongoPasswordResetTokenService {
459
- constructor(private db: Db) {}
460
-
461
- private get collection() {
462
- return this.db.collection<MongoDoc>("rebase_password_reset_tokens");
463
- }
464
-
465
- async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
466
- await this.collection.deleteMany({ uid,
467
- usedAt: null });
468
-
469
- await this.collection.insertOne({
470
- _id: new ObjectId().toString(),
471
- uid,
472
- tokenHash,
473
- expiresAt,
474
- usedAt: null
475
- });
476
- }
477
-
478
- async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {
479
- const doc = await this.collection.findOne({
480
- tokenHash,
481
- usedAt: null,
482
- expiresAt: { $gt: new Date() }
483
- });
484
-
485
- if (!doc) return null;
486
-
487
- return {
488
- uid: doc.uid,
489
- expiresAt: new Date(doc.expiresAt)
490
- };
491
- }
492
-
493
- async markAsUsed(tokenHash: string): Promise<void> {
494
- await this.collection.updateOne(
495
- { tokenHash },
496
- { $set: { usedAt: new Date() } }
497
- );
498
- }
499
-
500
- async deleteAllForUser(uid: string): Promise<void> {
501
- await this.collection.deleteMany({ uid });
502
- }
503
-
504
- async deleteExpired(): Promise<void> {
505
- await this.collection.deleteMany({ expiresAt: { $lt: new Date() } });
506
- }
507
- }
508
-
509
- export class MongoTokenRepository implements TokenRepository {
510
- private refreshTokenService: MongoRefreshTokenService;
511
- private passwordResetTokenService: MongoPasswordResetTokenService;
512
-
513
- constructor(private db: Db) {
514
- this.refreshTokenService = new MongoRefreshTokenService(db);
515
- this.passwordResetTokenService = new MongoPasswordResetTokenService(db);
516
- }
517
-
518
- async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
519
- await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
520
- }
521
-
522
- async markRefreshTokenRotated(tokenHash: string): Promise<void> {
523
- await this.refreshTokenService.markRotated(tokenHash);
524
- }
525
-
526
- async revokeRefreshTokenSession(sessionId: string): Promise<void> {
527
- await this.refreshTokenService.revokeSession(sessionId);
528
- }
529
-
530
- async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
531
- await this.refreshTokenService.prune(uid, sessionId, supersededBefore);
532
- }
533
-
534
- async getTokensValidAfter(uid: string): Promise<Date | null> {
535
- return this.refreshTokenService.getTokensValidAfter(uid);
536
- }
537
-
538
- async setTokensValidAfter(uid: string, at: Date): Promise<void> {
539
- await this.refreshTokenService.setTokensValidAfter(uid, at);
540
- }
541
-
542
- async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
543
- return this.refreshTokenService.findByHash(tokenHash);
544
- }
545
-
546
- async deleteRefreshToken(tokenHash: string): Promise<void> {
547
- await this.refreshTokenService.deleteByHash(tokenHash);
548
- }
549
-
550
- async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
551
- await this.refreshTokenService.deleteAllForUser(uid);
552
- }
553
-
554
- async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
555
- return this.refreshTokenService.listForUser(uid);
556
- }
557
-
558
- async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
559
- await this.refreshTokenService.deleteById(id, uid);
560
- }
561
-
562
- async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
563
- await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);
564
- }
565
-
566
- async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
567
- return this.passwordResetTokenService.findValidByHash(tokenHash);
568
- }
569
-
570
- async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {
571
- await this.passwordResetTokenService.markAsUsed(tokenHash);
572
- }
573
-
574
- async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
575
- await this.passwordResetTokenService.deleteAllForUser(uid);
576
- }
577
-
578
- async deleteExpiredTokens(): Promise<void> {
579
- await this.passwordResetTokenService.deleteExpired();
580
- }
581
-
582
- // Magic link token operations
583
-
584
- async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
585
- const col = this.db.collection("magic_link_tokens");
586
- await col.deleteMany({ uid, usedAt: null });
587
- await col.insertOne({ uid, tokenHash, expiresAt, usedAt: null, createdAt: new Date() });
588
- }
589
-
590
- async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
591
- const col = this.db.collection("magic_link_tokens");
592
- const doc = await col.findOne({ tokenHash, usedAt: null, expiresAt: { $gt: new Date() } });
593
- if (!doc) return null;
594
- return { uid: doc.uid as string, expiresAt: doc.expiresAt as Date };
595
- }
596
-
597
- async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {
598
- const col = this.db.collection("magic_link_tokens");
599
- await col.updateOne({ tokenHash }, { $set: { usedAt: new Date() } });
600
- }
601
- }
602
-
603
- export class MongoAuthRepository implements AuthRepository {
604
- private userService: MongoUserService;
605
- private roleService: MongoRoleService;
606
- private tokenRepository: MongoTokenRepository;
607
-
608
- constructor(private db: Db) {
609
- this.userService = new MongoUserService(db);
610
- this.roleService = new MongoRoleService(db);
611
- this.tokenRepository = new MongoTokenRepository(db);
612
- }
613
-
614
- async createUser(data: CreateUserData): Promise<UserData> {
615
- return this.userService.createUser(data);
616
- }
617
-
618
- async getUserById(id: string): Promise<UserData | null> {
619
- return this.userService.getUserById(id);
620
- }
621
-
622
- async getUserByEmail(email: string): Promise<UserData | null> {
623
- return this.userService.getUserByEmail(email);
624
- }
625
-
626
- async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {
627
- return this.userService.getUserByIdentity(provider, providerId);
628
- }
629
-
630
- async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
631
- return this.userService.getUserIdentities(uid);
632
- }
633
-
634
- async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
635
- return this.userService.linkUserIdentity(uid, provider, providerId, profileData);
636
- }
637
-
638
- async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
639
- return this.userService.updateUser(id, data);
640
- }
641
-
642
- async deleteUser(id: string): Promise<void> {
643
- await this.userService.deleteUser(id);
644
- }
645
-
646
- async listUsers(): Promise<UserData[]> {
647
- return this.userService.listUsers();
648
- }
649
-
650
- async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
651
- return this.userService.listUsersPaginated(options);
652
- }
653
-
654
- async updatePassword(id: string, passwordHash: string): Promise<void> {
655
- await this.userService.updatePassword(id, passwordHash);
656
- }
657
-
658
- async setEmailVerified(id: string, verified: boolean): Promise<void> {
659
- await this.userService.setEmailVerified(id, verified);
660
- }
661
-
662
- async setVerificationToken(id: string, token: string | null): Promise<void> {
663
- await this.userService.setVerificationToken(id, token);
664
- }
665
-
666
- async getUserByVerificationToken(token: string): Promise<UserData | null> {
667
- return this.userService.getUserByVerificationToken(token);
668
- }
669
-
670
- async getUserRoles(uid: string): Promise<RoleData[]> {
671
- return this.userService.getUserRoles(uid);
672
- }
673
-
674
- async getUserRoleIds(uid: string): Promise<string[]> {
675
- return this.userService.getUserRoleIds(uid);
676
- }
677
-
678
- async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
679
- await this.userService.setUserRoles(uid, roleIds);
680
- }
681
-
682
- async assignDefaultRole(uid: string, roleId: string): Promise<void> {
683
- await this.userService.assignDefaultRole(uid, roleId);
684
- }
685
-
686
- async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
687
- return this.userService.getUserWithRoles(uid);
688
- }
689
-
690
- async getRoleById(id: string): Promise<RoleData | null> {
691
- return this.roleService.getRoleById(id);
692
- }
693
-
694
- async listRoles(): Promise<RoleData[]> {
695
- return this.roleService.listRoles();
696
- }
697
-
698
- async createRole(data: CreateRoleData): Promise<RoleData> {
699
- return this.roleService.createRole(data);
700
- }
701
-
702
- async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
703
- return this.roleService.updateRole(id, data);
704
- }
705
-
706
- async deleteRole(id: string): Promise<void> {
707
- await this.roleService.deleteRole(id);
708
- }
709
-
710
- async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
711
- await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
712
- }
713
-
714
- async markRefreshTokenRotated(tokenHash: string): Promise<void> {
715
- await this.tokenRepository.markRefreshTokenRotated(tokenHash);
716
- }
717
-
718
- async revokeRefreshTokenSession(sessionId: string): Promise<void> {
719
- await this.tokenRepository.revokeRefreshTokenSession(sessionId);
720
- }
721
-
722
- async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
723
- await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);
724
- }
725
-
726
- async getTokensValidAfter(uid: string): Promise<Date | null> {
727
- return this.tokenRepository.getTokensValidAfter(uid);
728
- }
729
-
730
- async setTokensValidAfter(uid: string, at: Date): Promise<void> {
731
- await this.tokenRepository.setTokensValidAfter(uid, at);
732
- }
733
-
734
- async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
735
- return this.tokenRepository.findRefreshTokenByHash(tokenHash);
736
- }
737
-
738
- async deleteRefreshToken(tokenHash: string): Promise<void> {
739
- await this.tokenRepository.deleteRefreshToken(tokenHash);
740
- }
741
-
742
- async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
743
- await this.tokenRepository.deleteAllRefreshTokensForUser(uid);
744
- }
745
-
746
- async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
747
- return this.tokenRepository.listRefreshTokensForUser(uid);
748
- }
749
-
750
- async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
751
- await this.tokenRepository.deleteRefreshTokenById(id, uid);
752
- }
753
-
754
- async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
755
- await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);
756
- }
757
-
758
- async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
759
- return this.tokenRepository.findValidPasswordResetToken(tokenHash);
760
- }
761
-
762
- async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {
763
- await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
764
- }
765
-
766
- async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
767
- await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);
768
- }
769
-
770
- async deleteExpiredTokens(): Promise<void> {
771
- await this.tokenRepository.deleteExpiredTokens();
772
- }
773
-
774
- // Magic link token operations
775
-
776
- async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
777
- await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);
778
- }
779
-
780
- async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
781
- return this.tokenRepository.findValidMagicLinkToken(tokenHash);
782
- }
783
-
784
- async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {
785
- await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);
786
- }
787
-
788
- // ── MFA: not implemented on this engine ──────────────────────────────
789
- //
790
- // The routes are mounted for every backend, so `POST /auth/mfa/enroll`
791
- // is live here and reaches these. Throwing a bare `Error` made every one
792
- // of them a 500 — and a 500's message is sanitized on the way out, so the
793
- // caller was told "Internal Server Error" while the reason stayed in the
794
- // server log. Someone turning on two-factor auth got a server fault
795
- // instead of an answer, every time, on this engine.
796
- //
797
- // 501 with the reason, which is what `init.ts` already does for an admin
798
- // surface it mounts and cannot serve: "they answer 501 instead, and stay
799
- // mounted to say why". The reads below stay as they are — no factor can
800
- // exist here, so `[]`, `null` and `false` are true rather than merely
801
- // convenient, and `assertMfaSatisfied` reading `false` correctly leaves
802
- // the login gate inert.
803
- async createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
804
- throw new ApiError(
805
- 501,
806
- "MFA_NOT_SUPPORTED",
807
- "Multi-factor authentication is not implemented for the MongoDB backend, so a factor cannot be stored. Use a Postgres data source for accounts that need a second factor."
808
- );
809
- }
810
- async getMfaFactors(uid: string): Promise<MfaFactor[]> {
811
- return [];
812
- }
813
- async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
814
- return null;
815
- }
816
- async verifyMfaFactor(factorId: string): Promise<void> {
817
- throw new ApiError(
818
- 501,
819
- "MFA_NOT_SUPPORTED",
820
- "Multi-factor authentication is not implemented for the MongoDB backend, so a factor's verification cannot be stored. Use a Postgres data source for accounts that need a second factor."
821
- );
822
- }
823
- async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
824
- throw new ApiError(
825
- 501,
826
- "MFA_NOT_SUPPORTED",
827
- "Multi-factor authentication is not implemented for the MongoDB backend, so a factor cannot be stored. Use a Postgres data source for accounts that need a second factor."
828
- );
829
- }
830
- async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
831
- throw new ApiError(
832
- 501,
833
- "MFA_NOT_SUPPORTED",
834
- "Multi-factor authentication is not implemented for the MongoDB backend, so a challenge cannot be stored. Use a Postgres data source for accounts that need a second factor."
835
- );
836
- }
837
- async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
838
- return null;
839
- }
840
- async verifyMfaChallenge(challengeId: string): Promise<void> {
841
- throw new ApiError(
842
- 501,
843
- "MFA_NOT_SUPPORTED",
844
- "Multi-factor authentication is not implemented for the MongoDB backend, so a challenge's verification cannot be stored. Use a Postgres data source for accounts that need a second factor."
845
- );
846
- }
847
- async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
848
- throw new ApiError(
849
- 501,
850
- "MFA_NOT_SUPPORTED",
851
- "Multi-factor authentication is not implemented for the MongoDB backend, so recovery codes cannot be stored. Use a Postgres data source for accounts that need a second factor."
852
- );
853
- }
854
- async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
855
- return false;
856
- }
857
- async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
858
- return 0;
859
- }
860
- async deleteAllRecoveryCodes(uid: string): Promise<void> {
861
- // No-op
862
- }
863
- async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
864
- return false;
865
- }
866
- }