@rebasepro/server-postgres 0.9.1-canary.a57c262 → 0.9.1-canary.a8dbf1c
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/README.md +21 -0
- package/dist/PostgresBackendDriver.d.ts +18 -0
- package/dist/PostgresBootstrapper.d.ts +17 -1
- package/dist/auth/services.d.ts +68 -52
- package/dist/connection.d.ts +21 -0
- package/dist/index.es.js +914 -226
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
- package/dist/schema/auth-schema.d.ts +24 -24
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/schema/introspect-db-logic.d.ts +0 -5
- package/dist/schema/introspect-db-naming.d.ts +10 -0
- package/dist/security/policy-drift.d.ts +30 -0
- package/dist/security/rls-enforcement.d.ts +2 -2
- package/dist/services/channel-history.d.ts +118 -0
- package/dist/services/realtimeService.d.ts +69 -2
- package/dist/services/row-pipeline.d.ts +5 -2
- package/package.json +8 -31
- package/src/PostgresBackendDriver.ts +74 -10
- package/src/PostgresBootstrapper.ts +69 -3
- package/src/auth/ensure-tables.ts +170 -28
- package/src/auth/services.ts +181 -150
- package/src/cli.ts +60 -0
- package/src/connection.ts +61 -1
- package/src/databasePoolManager.ts +2 -0
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +13 -13
- package/src/schema/doctor.ts +45 -20
- package/src/schema/introspect-db-inference.ts +1 -1
- package/src/schema/introspect-db-logic.ts +1 -10
- package/src/schema/introspect-db-naming.ts +15 -0
- package/src/schema/introspect-db.ts +11 -1
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/security/policy-drift.test.ts +106 -1
- package/src/security/policy-drift.ts +56 -0
- package/src/security/rls-enforcement.ts +11 -5
- package/src/services/PersistService.ts +9 -2
- package/src/services/channel-history.ts +343 -0
- package/src/services/realtimeService.ts +198 -10
- package/src/services/row-pipeline.ts +70 -7
- package/src/utils/drizzle-conditions.ts +13 -0
- package/src/websocket.ts +30 -11
package/src/auth/services.ts
CHANGED
|
@@ -82,6 +82,33 @@ export class UserService implements UserRepository {
|
|
|
82
82
|
return `"${schema}"."${name}"`;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
87
|
+
*
|
|
88
|
+
* The auth services run on the base/owner connection, which by design
|
|
89
|
+
* carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
|
|
90
|
+
* in the default policies applies. That NULL is normally guaranteed by
|
|
91
|
+
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
92
|
+
* GUC that survives on a pooled connection (or a connection role that
|
|
93
|
+
* doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
|
|
94
|
+
* turns the trusted write into an RLS-scoped one and denies it with
|
|
95
|
+
* SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
|
|
96
|
+
* single chokepoint, makes the server context deterministic instead of
|
|
97
|
+
* trusting whatever state the pool hands us. `auth.uid()` reads '' as
|
|
98
|
+
* NULL via NULLIF, so '' is the server context.
|
|
99
|
+
*/
|
|
100
|
+
private async withServerContext<T>(fn: (db: NodePgDatabase) => Promise<T>): Promise<T> {
|
|
101
|
+
return await this.db.transaction(async (tx) => {
|
|
102
|
+
await tx.execute(sql`
|
|
103
|
+
SELECT set_config('app.uid', '', true),
|
|
104
|
+
set_config('app.user_id', '', true),
|
|
105
|
+
set_config('app.user_roles', '', true),
|
|
106
|
+
set_config('app.jwt', '', true)
|
|
107
|
+
`);
|
|
108
|
+
return await fn(tx as unknown as NodePgDatabase);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
private mapRowToUser(row: Record<string, unknown>): UserData {
|
|
86
113
|
if (!row) return row as UserData;
|
|
87
114
|
|
|
@@ -200,7 +227,9 @@ export class UserService implements UserRepository {
|
|
|
200
227
|
|
|
201
228
|
async createUser(data: CreateUserData): Promise<UserData> {
|
|
202
229
|
const payload = this.mapPayload(data);
|
|
203
|
-
const [row] =
|
|
230
|
+
const [row] = await this.withServerContext(async (db) =>
|
|
231
|
+
(await db.insert(this.usersTable).values(payload).returning()) as Record<string, unknown>[]
|
|
232
|
+
);
|
|
204
233
|
return this.mapRowToUser(row);
|
|
205
234
|
}
|
|
206
235
|
|
|
@@ -225,7 +254,7 @@ export class UserService implements UserRepository {
|
|
|
225
254
|
const result = await this.db
|
|
226
255
|
.select({ user: this.usersTable })
|
|
227
256
|
.from(this.usersTable)
|
|
228
|
-
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.
|
|
257
|
+
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid))
|
|
229
258
|
.where(
|
|
230
259
|
sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`
|
|
231
260
|
)
|
|
@@ -235,17 +264,17 @@ export class UserService implements UserRepository {
|
|
|
235
264
|
return this.mapRowToUser(result[0].user as Record<string, unknown>);
|
|
236
265
|
}
|
|
237
266
|
|
|
238
|
-
async getUserIdentities(
|
|
267
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
239
268
|
const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
|
|
240
269
|
const result = await this.db.execute(sql`
|
|
241
|
-
SELECT id,
|
|
270
|
+
SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at
|
|
242
271
|
FROM ${sql.raw(`"${schema}"."user_identities"`)}
|
|
243
|
-
WHERE
|
|
272
|
+
WHERE uid = ${uid}
|
|
244
273
|
`);
|
|
245
274
|
|
|
246
275
|
return result.rows.map((row: Record<string, unknown>) => ({
|
|
247
276
|
id: row.id as string,
|
|
248
|
-
|
|
277
|
+
uid: row.uid as string,
|
|
249
278
|
provider: row.provider as string,
|
|
250
279
|
providerId: row.provider_id as string,
|
|
251
280
|
profileData: (row.profile_data as Record<string, unknown> | null) ?? null,
|
|
@@ -254,13 +283,13 @@ export class UserService implements UserRepository {
|
|
|
254
283
|
}));
|
|
255
284
|
}
|
|
256
285
|
|
|
257
|
-
async linkUserIdentity(
|
|
258
|
-
await this.db.insert(this.userIdentitiesTable).values({
|
|
259
|
-
|
|
286
|
+
async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
287
|
+
await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
|
|
288
|
+
uid,
|
|
260
289
|
provider,
|
|
261
290
|
providerId,
|
|
262
291
|
profileData: profileData || null
|
|
263
|
-
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
292
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));
|
|
264
293
|
}
|
|
265
294
|
|
|
266
295
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
@@ -270,18 +299,20 @@ export class UserService implements UserRepository {
|
|
|
270
299
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
271
300
|
payload[updatedAtKey] = new Date();
|
|
272
301
|
|
|
273
|
-
const [row] =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
302
|
+
const [row] = await this.withServerContext(async (db) =>
|
|
303
|
+
(await db
|
|
304
|
+
.update(this.usersTable)
|
|
305
|
+
.set(payload)
|
|
306
|
+
.where(eq(idCol, id))
|
|
307
|
+
.returning()) as Record<string, unknown>[]
|
|
308
|
+
);
|
|
278
309
|
return row ? this.mapRowToUser(row) : null;
|
|
279
310
|
}
|
|
280
311
|
|
|
281
312
|
async deleteUser(id: string): Promise<void> {
|
|
282
313
|
const idCol = getColumn(this.usersTable, "id");
|
|
283
314
|
if (!idCol) return;
|
|
284
|
-
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
315
|
+
await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));
|
|
285
316
|
}
|
|
286
317
|
|
|
287
318
|
async listUsers(): Promise<UserData[]> {
|
|
@@ -357,13 +388,13 @@ export class UserService implements UserRepository {
|
|
|
357
388
|
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
358
389
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
359
390
|
|
|
360
|
-
await this.db
|
|
391
|
+
await this.withServerContext(async (db) => db
|
|
361
392
|
.update(this.usersTable)
|
|
362
393
|
.set({
|
|
363
394
|
[passwordHashColKey]: passwordHash,
|
|
364
395
|
[updatedAtColKey]: new Date()
|
|
365
396
|
})
|
|
366
|
-
.where(eq(idCol, id));
|
|
397
|
+
.where(eq(idCol, id)));
|
|
367
398
|
}
|
|
368
399
|
|
|
369
400
|
/**
|
|
@@ -376,14 +407,14 @@ export class UserService implements UserRepository {
|
|
|
376
407
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
377
408
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
378
409
|
|
|
379
|
-
await this.db
|
|
410
|
+
await this.withServerContext(async (db) => db
|
|
380
411
|
.update(this.usersTable)
|
|
381
412
|
.set({
|
|
382
413
|
[emailVerifiedColKey]: verified,
|
|
383
414
|
[emailVerificationTokenColKey]: null,
|
|
384
415
|
[updatedAtColKey]: new Date()
|
|
385
416
|
})
|
|
386
|
-
.where(eq(idCol, id));
|
|
417
|
+
.where(eq(idCol, id)));
|
|
387
418
|
}
|
|
388
419
|
|
|
389
420
|
/**
|
|
@@ -396,14 +427,14 @@ export class UserService implements UserRepository {
|
|
|
396
427
|
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
397
428
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
398
429
|
|
|
399
|
-
await this.db
|
|
430
|
+
await this.withServerContext(async (db) => db
|
|
400
431
|
.update(this.usersTable)
|
|
401
432
|
.set({
|
|
402
433
|
[emailVerificationTokenColKey]: token,
|
|
403
434
|
[emailVerificationSentAtColKey]: token ? new Date() : null,
|
|
404
435
|
[updatedAtColKey]: new Date()
|
|
405
436
|
})
|
|
406
|
-
.where(eq(idCol, id));
|
|
437
|
+
.where(eq(idCol, id)));
|
|
407
438
|
}
|
|
408
439
|
|
|
409
440
|
/**
|
|
@@ -422,10 +453,10 @@ export class UserService implements UserRepository {
|
|
|
422
453
|
/**
|
|
423
454
|
* Get roles for a user from database (inline TEXT[] column)
|
|
424
455
|
*/
|
|
425
|
-
async getUserRoles(
|
|
456
|
+
async getUserRoles(uid: string): Promise<Role[]> {
|
|
426
457
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
427
458
|
const result = await this.db.execute(sql`
|
|
428
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
459
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
429
460
|
`);
|
|
430
461
|
|
|
431
462
|
if (result.rows.length === 0) return [];
|
|
@@ -445,10 +476,10 @@ export class UserService implements UserRepository {
|
|
|
445
476
|
/**
|
|
446
477
|
* Get role IDs for a user
|
|
447
478
|
*/
|
|
448
|
-
async getUserRoleIds(
|
|
479
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
449
480
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
450
481
|
const result = await this.db.execute(sql`
|
|
451
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
482
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
452
483
|
`);
|
|
453
484
|
|
|
454
485
|
if (result.rows.length === 0) return [];
|
|
@@ -460,36 +491,36 @@ export class UserService implements UserRepository {
|
|
|
460
491
|
/**
|
|
461
492
|
* Set roles for a user (replaces existing roles)
|
|
462
493
|
*/
|
|
463
|
-
async setUserRoles(
|
|
494
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
464
495
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
465
496
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
466
|
-
await this.db.execute(sql`
|
|
497
|
+
await this.withServerContext(async (db) => db.execute(sql`
|
|
467
498
|
UPDATE ${sql.raw(usersTableName)}
|
|
468
499
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
469
|
-
WHERE id = ${
|
|
470
|
-
`);
|
|
500
|
+
WHERE id = ${uid}
|
|
501
|
+
`));
|
|
471
502
|
}
|
|
472
503
|
|
|
473
504
|
/**
|
|
474
505
|
* Assign a specific role to new user (appends if not present)
|
|
475
506
|
*/
|
|
476
|
-
async assignDefaultRole(
|
|
507
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
477
508
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
478
|
-
await this.db.execute(sql`
|
|
509
|
+
await this.withServerContext(async (db) => db.execute(sql`
|
|
479
510
|
UPDATE ${sql.raw(usersTableName)}
|
|
480
511
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
481
|
-
WHERE id = ${
|
|
482
|
-
`);
|
|
512
|
+
WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))
|
|
513
|
+
`));
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
/**
|
|
486
517
|
* Get user with their roles
|
|
487
518
|
*/
|
|
488
|
-
async getUserWithRoles(
|
|
489
|
-
const user = await this.getUserById(
|
|
519
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[] } | null> {
|
|
520
|
+
const user = await this.getUserById(uid);
|
|
490
521
|
if (!user) return null;
|
|
491
522
|
|
|
492
|
-
const roles = await this.getUserRoles(
|
|
523
|
+
const roles = await this.getUserRoles(uid);
|
|
493
524
|
return { user,
|
|
494
525
|
roles };
|
|
495
526
|
}
|
|
@@ -510,20 +541,20 @@ export class RefreshTokenService {
|
|
|
510
541
|
}
|
|
511
542
|
}
|
|
512
543
|
|
|
513
|
-
async createToken(
|
|
544
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
|
|
514
545
|
// Fallback to empty string because UNIQUE constraints treat NULLs as strictly distinct in standard Postgres.
|
|
515
|
-
// We want (
|
|
546
|
+
// We want (uid, NULL, NULL) to collide and overwrite, so we map undefined/null to empty strings.
|
|
516
547
|
const safeUserAgent = userAgent || "";
|
|
517
548
|
const safeIpAddress = ipAddress || "";
|
|
518
549
|
|
|
519
|
-
// Atomic upsert keyed on the device session (
|
|
550
|
+
// Atomic upsert keyed on the device session (uid, user_agent, ip_address).
|
|
520
551
|
// A DELETE-then-INSERT races under concurrent refreshes — cookie-mode boot can
|
|
521
552
|
// fire two /refresh calls at once (both carrying the same refresh cookie): both
|
|
522
553
|
// DELETE, then the second INSERT violates `unique_device_session` and 500s.
|
|
523
554
|
// A single INSERT ... ON CONFLICT DO UPDATE rotates the token atomically.
|
|
524
555
|
await this.db.insert(this.refreshTokensTable)
|
|
525
556
|
.values({
|
|
526
|
-
|
|
557
|
+
uid,
|
|
527
558
|
tokenHash,
|
|
528
559
|
expiresAt,
|
|
529
560
|
userAgent: safeUserAgent,
|
|
@@ -531,7 +562,7 @@ export class RefreshTokenService {
|
|
|
531
562
|
})
|
|
532
563
|
.onConflictDoUpdate({
|
|
533
564
|
target: [
|
|
534
|
-
this.refreshTokensTable.
|
|
565
|
+
this.refreshTokensTable.uid,
|
|
535
566
|
this.refreshTokensTable.userAgent,
|
|
536
567
|
this.refreshTokensTable.ipAddress
|
|
537
568
|
],
|
|
@@ -543,7 +574,7 @@ export class RefreshTokenService {
|
|
|
543
574
|
const [token] = await this.db
|
|
544
575
|
.select({
|
|
545
576
|
id: this.refreshTokensTable.id,
|
|
546
|
-
|
|
577
|
+
uid: this.refreshTokensTable.uid,
|
|
547
578
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
548
579
|
expiresAt: this.refreshTokensTable.expiresAt,
|
|
549
580
|
createdAt: this.refreshTokensTable.createdAt,
|
|
@@ -560,15 +591,15 @@ export class RefreshTokenService {
|
|
|
560
591
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
561
592
|
}
|
|
562
593
|
|
|
563
|
-
async deleteAllForUser(
|
|
564
|
-
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.
|
|
594
|
+
async deleteAllForUser(uid: string): Promise<void> {
|
|
595
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
|
|
565
596
|
}
|
|
566
597
|
|
|
567
|
-
async listForUser(
|
|
598
|
+
async listForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
568
599
|
const tokens = await this.db
|
|
569
600
|
.select({
|
|
570
601
|
id: this.refreshTokensTable.id,
|
|
571
|
-
|
|
602
|
+
uid: this.refreshTokensTable.uid,
|
|
572
603
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
573
604
|
expiresAt: this.refreshTokensTable.expiresAt,
|
|
574
605
|
createdAt: this.refreshTokensTable.createdAt,
|
|
@@ -576,15 +607,15 @@ export class RefreshTokenService {
|
|
|
576
607
|
ipAddress: this.refreshTokensTable.ipAddress
|
|
577
608
|
})
|
|
578
609
|
.from(this.refreshTokensTable)
|
|
579
|
-
.where(eq(this.refreshTokensTable.
|
|
610
|
+
.where(eq(this.refreshTokensTable.uid, uid))
|
|
580
611
|
.orderBy(this.refreshTokensTable.createdAt);
|
|
581
612
|
|
|
582
613
|
return tokens as RefreshTokenInfo[];
|
|
583
614
|
}
|
|
584
615
|
|
|
585
|
-
async deleteById(id: string,
|
|
616
|
+
async deleteById(id: string, uid: string): Promise<void> {
|
|
586
617
|
await this.db.delete(this.refreshTokensTable)
|
|
587
|
-
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.
|
|
618
|
+
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
|
|
588
619
|
}
|
|
589
620
|
}
|
|
590
621
|
|
|
@@ -614,16 +645,16 @@ export class PasswordResetTokenService {
|
|
|
614
645
|
/**
|
|
615
646
|
* Create a password reset token
|
|
616
647
|
*/
|
|
617
|
-
async createToken(
|
|
648
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
618
649
|
// Delete any existing unused tokens for this user
|
|
619
650
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
620
651
|
await this.db.execute(sql`
|
|
621
652
|
DELETE FROM ${sql.raw(tableName)}
|
|
622
|
-
WHERE
|
|
653
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
623
654
|
`);
|
|
624
655
|
|
|
625
656
|
await this.db.insert(this.passwordResetTokensTable).values({
|
|
626
|
-
|
|
657
|
+
uid,
|
|
627
658
|
tokenHash,
|
|
628
659
|
expiresAt
|
|
629
660
|
});
|
|
@@ -632,21 +663,21 @@ export class PasswordResetTokenService {
|
|
|
632
663
|
/**
|
|
633
664
|
* Find a valid (not expired, not used) token by hash
|
|
634
665
|
*/
|
|
635
|
-
async findValidByHash(tokenHash: string): Promise<{
|
|
666
|
+
async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {
|
|
636
667
|
const [token] = await this.db
|
|
637
668
|
.select({
|
|
638
|
-
|
|
669
|
+
uid: this.passwordResetTokensTable.uid,
|
|
639
670
|
expiresAt: this.passwordResetTokensTable.expiresAt
|
|
640
671
|
})
|
|
641
672
|
.from(this.passwordResetTokensTable)
|
|
642
|
-
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{
|
|
673
|
+
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{ uid: string; expiresAt: Date }>;
|
|
643
674
|
|
|
644
675
|
if (!token) return null;
|
|
645
676
|
|
|
646
677
|
// Check if expired or used
|
|
647
678
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
648
679
|
const result = await this.db.execute(sql`
|
|
649
|
-
SELECT
|
|
680
|
+
SELECT uid, expires_at
|
|
650
681
|
FROM ${sql.raw(tableName)}
|
|
651
682
|
WHERE token_hash = ${tokenHash}
|
|
652
683
|
AND used_at IS NULL
|
|
@@ -655,9 +686,9 @@ export class PasswordResetTokenService {
|
|
|
655
686
|
|
|
656
687
|
if (result.rows.length === 0) return null;
|
|
657
688
|
|
|
658
|
-
const row = result.rows[0] as {
|
|
689
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
659
690
|
return {
|
|
660
|
-
|
|
691
|
+
uid: row.uid,
|
|
661
692
|
expiresAt: new Date(row.expires_at)
|
|
662
693
|
};
|
|
663
694
|
}
|
|
@@ -675,8 +706,8 @@ export class PasswordResetTokenService {
|
|
|
675
706
|
/**
|
|
676
707
|
* Delete all tokens for a user
|
|
677
708
|
*/
|
|
678
|
-
async deleteAllForUser(
|
|
679
|
-
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.
|
|
709
|
+
async deleteAllForUser(uid: string): Promise<void> {
|
|
710
|
+
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.uid, uid));
|
|
680
711
|
}
|
|
681
712
|
|
|
682
713
|
/**
|
|
@@ -711,16 +742,16 @@ export class MagicLinkTokenService {
|
|
|
711
742
|
return `"${schema}"."${name}"`;
|
|
712
743
|
}
|
|
713
744
|
|
|
714
|
-
async createToken(
|
|
745
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
715
746
|
// Delete any existing unused tokens for this user
|
|
716
747
|
const tableName = this.getQualifiedTableName();
|
|
717
748
|
await this.db.execute(sql`
|
|
718
749
|
DELETE FROM ${sql.raw(tableName)}
|
|
719
|
-
WHERE
|
|
750
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
720
751
|
`);
|
|
721
752
|
|
|
722
753
|
await this.db.insert(this.magicLinkTokensTable).values({
|
|
723
|
-
|
|
754
|
+
uid,
|
|
724
755
|
tokenHash,
|
|
725
756
|
expiresAt
|
|
726
757
|
});
|
|
@@ -729,7 +760,7 @@ export class MagicLinkTokenService {
|
|
|
729
760
|
async findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
730
761
|
const tableName = this.getQualifiedTableName();
|
|
731
762
|
const result = await this.db.execute(sql`
|
|
732
|
-
SELECT
|
|
763
|
+
SELECT uid, expires_at
|
|
733
764
|
FROM ${sql.raw(tableName)}
|
|
734
765
|
WHERE token_hash = ${tokenHash}
|
|
735
766
|
AND used_at IS NULL
|
|
@@ -738,9 +769,9 @@ export class MagicLinkTokenService {
|
|
|
738
769
|
|
|
739
770
|
if (result.rows.length === 0) return null;
|
|
740
771
|
|
|
741
|
-
const row = result.rows[0] as {
|
|
772
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
742
773
|
return {
|
|
743
|
-
|
|
774
|
+
uid: row.uid,
|
|
744
775
|
expiresAt: new Date(row.expires_at)
|
|
745
776
|
};
|
|
746
777
|
}
|
|
@@ -773,8 +804,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
773
804
|
|
|
774
805
|
// Refresh token operations
|
|
775
806
|
|
|
776
|
-
async createRefreshToken(
|
|
777
|
-
await this.refreshTokenService.createToken(
|
|
807
|
+
async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
|
|
808
|
+
await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
|
|
778
809
|
}
|
|
779
810
|
|
|
780
811
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -785,22 +816,22 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
785
816
|
await this.refreshTokenService.deleteByHash(tokenHash);
|
|
786
817
|
}
|
|
787
818
|
|
|
788
|
-
async deleteAllRefreshTokensForUser(
|
|
789
|
-
await this.refreshTokenService.deleteAllForUser(
|
|
819
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
820
|
+
await this.refreshTokenService.deleteAllForUser(uid);
|
|
790
821
|
}
|
|
791
822
|
|
|
792
|
-
async listRefreshTokensForUser(
|
|
793
|
-
return this.refreshTokenService.listForUser(
|
|
823
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
824
|
+
return this.refreshTokenService.listForUser(uid);
|
|
794
825
|
}
|
|
795
826
|
|
|
796
|
-
async deleteRefreshTokenById(id: string,
|
|
797
|
-
await this.refreshTokenService.deleteById(id,
|
|
827
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
828
|
+
await this.refreshTokenService.deleteById(id, uid);
|
|
798
829
|
}
|
|
799
830
|
|
|
800
831
|
// Password reset token operations
|
|
801
832
|
|
|
802
|
-
async createPasswordResetToken(
|
|
803
|
-
await this.passwordResetTokenService.createToken(
|
|
833
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
834
|
+
await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);
|
|
804
835
|
}
|
|
805
836
|
|
|
806
837
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -811,8 +842,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
811
842
|
await this.passwordResetTokenService.markAsUsed(tokenHash);
|
|
812
843
|
}
|
|
813
844
|
|
|
814
|
-
async deleteAllPasswordResetTokensForUser(
|
|
815
|
-
await this.passwordResetTokenService.deleteAllForUser(
|
|
845
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
846
|
+
await this.passwordResetTokenService.deleteAllForUser(uid);
|
|
816
847
|
}
|
|
817
848
|
|
|
818
849
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -821,8 +852,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
821
852
|
|
|
822
853
|
// Magic link token operations
|
|
823
854
|
|
|
824
|
-
async createMagicLinkToken(
|
|
825
|
-
await this.magicLinkTokenService.createToken(
|
|
855
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
856
|
+
await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);
|
|
826
857
|
}
|
|
827
858
|
|
|
828
859
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -869,12 +900,12 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
869
900
|
return this.userService.getUserByIdentity(provider, providerId);
|
|
870
901
|
}
|
|
871
902
|
|
|
872
|
-
async getUserIdentities(
|
|
873
|
-
return this.userService.getUserIdentities(
|
|
903
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
904
|
+
return this.userService.getUserIdentities(uid);
|
|
874
905
|
}
|
|
875
906
|
|
|
876
|
-
async linkUserIdentity(
|
|
877
|
-
return this.userService.linkUserIdentity(
|
|
907
|
+
async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
908
|
+
return this.userService.linkUserIdentity(uid, provider, providerId, profileData);
|
|
878
909
|
}
|
|
879
910
|
|
|
880
911
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
@@ -909,24 +940,24 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
909
940
|
return this.userService.getUserByVerificationToken(token);
|
|
910
941
|
}
|
|
911
942
|
|
|
912
|
-
async getUserRoles(
|
|
913
|
-
return this.userService.getUserRoles(
|
|
943
|
+
async getUserRoles(uid: string): Promise<RoleData[]> {
|
|
944
|
+
return this.userService.getUserRoles(uid);
|
|
914
945
|
}
|
|
915
946
|
|
|
916
|
-
async getUserRoleIds(
|
|
917
|
-
return this.userService.getUserRoleIds(
|
|
947
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
948
|
+
return this.userService.getUserRoleIds(uid);
|
|
918
949
|
}
|
|
919
950
|
|
|
920
|
-
async setUserRoles(
|
|
921
|
-
await this.userService.setUserRoles(
|
|
951
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
952
|
+
await this.userService.setUserRoles(uid, roleIds);
|
|
922
953
|
}
|
|
923
954
|
|
|
924
|
-
async assignDefaultRole(
|
|
925
|
-
await this.userService.assignDefaultRole(
|
|
955
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
956
|
+
await this.userService.assignDefaultRole(uid, roleId);
|
|
926
957
|
}
|
|
927
958
|
|
|
928
|
-
async getUserWithRoles(
|
|
929
|
-
return this.userService.getUserWithRoles(
|
|
959
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
|
|
960
|
+
return this.userService.getUserWithRoles(uid);
|
|
930
961
|
}
|
|
931
962
|
|
|
932
963
|
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
@@ -987,8 +1018,8 @@ collectionPermissions: null }
|
|
|
987
1018
|
|
|
988
1019
|
// Token operations (delegate to PostgresTokenRepository)
|
|
989
1020
|
|
|
990
|
-
async createRefreshToken(
|
|
991
|
-
await this.tokenRepository.createRefreshToken(
|
|
1021
|
+
async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
|
|
1022
|
+
await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
|
|
992
1023
|
}
|
|
993
1024
|
|
|
994
1025
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -999,20 +1030,20 @@ collectionPermissions: null }
|
|
|
999
1030
|
await this.tokenRepository.deleteRefreshToken(tokenHash);
|
|
1000
1031
|
}
|
|
1001
1032
|
|
|
1002
|
-
async deleteAllRefreshTokensForUser(
|
|
1003
|
-
await this.tokenRepository.deleteAllRefreshTokensForUser(
|
|
1033
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
1034
|
+
await this.tokenRepository.deleteAllRefreshTokensForUser(uid);
|
|
1004
1035
|
}
|
|
1005
1036
|
|
|
1006
|
-
async listRefreshTokensForUser(
|
|
1007
|
-
return this.tokenRepository.listRefreshTokensForUser(
|
|
1037
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
1038
|
+
return this.tokenRepository.listRefreshTokensForUser(uid);
|
|
1008
1039
|
}
|
|
1009
1040
|
|
|
1010
|
-
async deleteRefreshTokenById(id: string,
|
|
1011
|
-
await this.tokenRepository.deleteRefreshTokenById(id,
|
|
1041
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
1042
|
+
await this.tokenRepository.deleteRefreshTokenById(id, uid);
|
|
1012
1043
|
}
|
|
1013
1044
|
|
|
1014
|
-
async createPasswordResetToken(
|
|
1015
|
-
await this.tokenRepository.createPasswordResetToken(
|
|
1045
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1046
|
+
await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);
|
|
1016
1047
|
}
|
|
1017
1048
|
|
|
1018
1049
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -1023,8 +1054,8 @@ collectionPermissions: null }
|
|
|
1023
1054
|
await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
|
|
1024
1055
|
}
|
|
1025
1056
|
|
|
1026
|
-
async deleteAllPasswordResetTokensForUser(
|
|
1027
|
-
await this.tokenRepository.deleteAllPasswordResetTokensForUser(
|
|
1057
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
1058
|
+
await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);
|
|
1028
1059
|
}
|
|
1029
1060
|
|
|
1030
1061
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -1033,8 +1064,8 @@ collectionPermissions: null }
|
|
|
1033
1064
|
|
|
1034
1065
|
// Magic link token operations
|
|
1035
1066
|
|
|
1036
|
-
async createMagicLinkToken(
|
|
1037
|
-
await this.tokenRepository.createMagicLinkToken(
|
|
1067
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1068
|
+
await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);
|
|
1038
1069
|
}
|
|
1039
1070
|
|
|
1040
1071
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -1055,12 +1086,12 @@ collectionPermissions: null }
|
|
|
1055
1086
|
return this._mfaService;
|
|
1056
1087
|
}
|
|
1057
1088
|
|
|
1058
|
-
async createMfaFactor(
|
|
1059
|
-
return this.getMfaService().createMfaFactor(
|
|
1089
|
+
async createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
|
|
1090
|
+
return this.getMfaService().createMfaFactor(uid, factorType, secretEncrypted, friendlyName);
|
|
1060
1091
|
}
|
|
1061
1092
|
|
|
1062
|
-
async getMfaFactors(
|
|
1063
|
-
return this.getMfaService().getMfaFactors(
|
|
1093
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1094
|
+
return this.getMfaService().getMfaFactors(uid);
|
|
1064
1095
|
}
|
|
1065
1096
|
|
|
1066
1097
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
@@ -1071,8 +1102,8 @@ collectionPermissions: null }
|
|
|
1071
1102
|
return this.getMfaService().verifyMfaFactor(factorId);
|
|
1072
1103
|
}
|
|
1073
1104
|
|
|
1074
|
-
async deleteMfaFactor(factorId: string,
|
|
1075
|
-
return this.getMfaService().deleteMfaFactor(factorId,
|
|
1105
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1106
|
+
return this.getMfaService().deleteMfaFactor(factorId, uid);
|
|
1076
1107
|
}
|
|
1077
1108
|
|
|
1078
1109
|
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
@@ -1087,24 +1118,24 @@ collectionPermissions: null }
|
|
|
1087
1118
|
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
1088
1119
|
}
|
|
1089
1120
|
|
|
1090
|
-
async createRecoveryCodes(
|
|
1091
|
-
return this.getMfaService().createRecoveryCodes(
|
|
1121
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1122
|
+
return this.getMfaService().createRecoveryCodes(uid, codeHashes);
|
|
1092
1123
|
}
|
|
1093
1124
|
|
|
1094
|
-
async useRecoveryCode(
|
|
1095
|
-
return this.getMfaService().useRecoveryCode(
|
|
1125
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1126
|
+
return this.getMfaService().useRecoveryCode(uid, codeHash);
|
|
1096
1127
|
}
|
|
1097
1128
|
|
|
1098
|
-
async getUnusedRecoveryCodeCount(
|
|
1099
|
-
return this.getMfaService().getUnusedRecoveryCodeCount(
|
|
1129
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1130
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(uid);
|
|
1100
1131
|
}
|
|
1101
1132
|
|
|
1102
|
-
async deleteAllRecoveryCodes(
|
|
1103
|
-
return this.getMfaService().deleteAllRecoveryCodes(
|
|
1133
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1134
|
+
return this.getMfaService().deleteAllRecoveryCodes(uid);
|
|
1104
1135
|
}
|
|
1105
1136
|
|
|
1106
|
-
async hasVerifiedMfaFactors(
|
|
1107
|
-
return this.getMfaService().hasVerifiedMfaFactors(
|
|
1137
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1138
|
+
return this.getMfaService().hasVerifiedMfaFactors(uid);
|
|
1108
1139
|
}
|
|
1109
1140
|
}
|
|
1110
1141
|
|
|
@@ -1124,22 +1155,22 @@ export class MfaService implements MfaRepository {
|
|
|
1124
1155
|
}
|
|
1125
1156
|
|
|
1126
1157
|
async createMfaFactor(
|
|
1127
|
-
|
|
1158
|
+
uid: string,
|
|
1128
1159
|
factorType: "totp",
|
|
1129
1160
|
secretEncrypted: string,
|
|
1130
1161
|
friendlyName?: string
|
|
1131
1162
|
): Promise<MfaFactor> {
|
|
1132
1163
|
const tableName = this.qualify("mfa_factors");
|
|
1133
1164
|
const result = await this.db.execute(sql`
|
|
1134
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1135
|
-
VALUES (${
|
|
1136
|
-
RETURNING id,
|
|
1165
|
+
INSERT INTO ${sql.raw(tableName)} (uid, factor_type, secret_encrypted, friendly_name)
|
|
1166
|
+
VALUES (${uid}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
1167
|
+
RETURNING id, uid, factor_type, friendly_name, verified, created_at, updated_at
|
|
1137
1168
|
`);
|
|
1138
1169
|
|
|
1139
1170
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1140
1171
|
return {
|
|
1141
1172
|
id: row.id as string,
|
|
1142
|
-
|
|
1173
|
+
uid: row.uid as string,
|
|
1143
1174
|
factorType: row.factor_type as "totp",
|
|
1144
1175
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1145
1176
|
verified: row.verified as boolean,
|
|
@@ -1148,18 +1179,18 @@ export class MfaService implements MfaRepository {
|
|
|
1148
1179
|
};
|
|
1149
1180
|
}
|
|
1150
1181
|
|
|
1151
|
-
async getMfaFactors(
|
|
1182
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1152
1183
|
const tableName = this.qualify("mfa_factors");
|
|
1153
1184
|
const result = await this.db.execute(sql`
|
|
1154
|
-
SELECT id,
|
|
1185
|
+
SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at
|
|
1155
1186
|
FROM ${sql.raw(tableName)}
|
|
1156
|
-
WHERE
|
|
1187
|
+
WHERE uid = ${uid}
|
|
1157
1188
|
ORDER BY created_at
|
|
1158
1189
|
`);
|
|
1159
1190
|
|
|
1160
1191
|
return (result.rows as Array<Record<string, unknown>>).map(row => ({
|
|
1161
1192
|
id: row.id as string,
|
|
1162
|
-
|
|
1193
|
+
uid: row.uid as string,
|
|
1163
1194
|
factorType: row.factor_type as "totp",
|
|
1164
1195
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1165
1196
|
verified: row.verified as boolean,
|
|
@@ -1171,7 +1202,7 @@ export class MfaService implements MfaRepository {
|
|
|
1171
1202
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1172
1203
|
const tableName = this.qualify("mfa_factors");
|
|
1173
1204
|
const result = await this.db.execute(sql`
|
|
1174
|
-
SELECT id,
|
|
1205
|
+
SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
1175
1206
|
FROM ${sql.raw(tableName)}
|
|
1176
1207
|
WHERE id = ${factorId}
|
|
1177
1208
|
`);
|
|
@@ -1181,7 +1212,7 @@ export class MfaService implements MfaRepository {
|
|
|
1181
1212
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1182
1213
|
return {
|
|
1183
1214
|
id: row.id as string,
|
|
1184
|
-
|
|
1215
|
+
uid: row.uid as string,
|
|
1185
1216
|
factorType: row.factor_type as "totp",
|
|
1186
1217
|
secretEncrypted: row.secret_encrypted as string,
|
|
1187
1218
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
@@ -1200,11 +1231,11 @@ export class MfaService implements MfaRepository {
|
|
|
1200
1231
|
`);
|
|
1201
1232
|
}
|
|
1202
1233
|
|
|
1203
|
-
async deleteMfaFactor(factorId: string,
|
|
1234
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1204
1235
|
const tableName = this.qualify("mfa_factors");
|
|
1205
1236
|
await this.db.execute(sql`
|
|
1206
1237
|
DELETE FROM ${sql.raw(tableName)}
|
|
1207
|
-
WHERE id = ${factorId} AND
|
|
1238
|
+
WHERE id = ${factorId} AND uid = ${uid}
|
|
1208
1239
|
`);
|
|
1209
1240
|
}
|
|
1210
1241
|
|
|
@@ -1257,56 +1288,56 @@ export class MfaService implements MfaRepository {
|
|
|
1257
1288
|
`);
|
|
1258
1289
|
}
|
|
1259
1290
|
|
|
1260
|
-
async createRecoveryCodes(
|
|
1291
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1261
1292
|
const tableName = this.qualify("recovery_codes");
|
|
1262
1293
|
// Delete existing codes first
|
|
1263
1294
|
await this.db.execute(sql`
|
|
1264
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1295
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1265
1296
|
`);
|
|
1266
1297
|
|
|
1267
1298
|
// Insert new codes
|
|
1268
1299
|
for (const hash of codeHashes) {
|
|
1269
1300
|
await this.db.execute(sql`
|
|
1270
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1271
|
-
VALUES (${
|
|
1301
|
+
INSERT INTO ${sql.raw(tableName)} (uid, code_hash)
|
|
1302
|
+
VALUES (${uid}, ${hash})
|
|
1272
1303
|
`);
|
|
1273
1304
|
}
|
|
1274
1305
|
}
|
|
1275
1306
|
|
|
1276
|
-
async useRecoveryCode(
|
|
1307
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1277
1308
|
const tableName = this.qualify("recovery_codes");
|
|
1278
1309
|
const result = await this.db.execute(sql`
|
|
1279
1310
|
UPDATE ${sql.raw(tableName)}
|
|
1280
1311
|
SET used_at = NOW()
|
|
1281
|
-
WHERE
|
|
1312
|
+
WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
1282
1313
|
RETURNING id
|
|
1283
1314
|
`);
|
|
1284
1315
|
|
|
1285
1316
|
return result.rows.length > 0;
|
|
1286
1317
|
}
|
|
1287
1318
|
|
|
1288
|
-
async getUnusedRecoveryCodeCount(
|
|
1319
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1289
1320
|
const tableName = this.qualify("recovery_codes");
|
|
1290
1321
|
const result = await this.db.execute(sql`
|
|
1291
1322
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1292
|
-
WHERE
|
|
1323
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
1293
1324
|
`);
|
|
1294
1325
|
|
|
1295
1326
|
return (result.rows[0] as { count: number }).count;
|
|
1296
1327
|
}
|
|
1297
1328
|
|
|
1298
|
-
async deleteAllRecoveryCodes(
|
|
1329
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1299
1330
|
const tableName = this.qualify("recovery_codes");
|
|
1300
1331
|
await this.db.execute(sql`
|
|
1301
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1332
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1302
1333
|
`);
|
|
1303
1334
|
}
|
|
1304
1335
|
|
|
1305
|
-
async hasVerifiedMfaFactors(
|
|
1336
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1306
1337
|
const tableName = this.qualify("mfa_factors");
|
|
1307
1338
|
const result = await this.db.execute(sql`
|
|
1308
1339
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1309
|
-
WHERE
|
|
1340
|
+
WHERE uid = ${uid} AND verified = TRUE
|
|
1310
1341
|
`);
|
|
1311
1342
|
|
|
1312
1343
|
return (result.rows[0] as { count: number }).count > 0;
|