@rebasepro/server-postgres 0.9.1-canary.ff338b5 → 0.10.1-canary.18115ba
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 +7 -1
- package/dist/auth/services.d.ts +93 -54
- package/dist/index.es.js +929 -224
- 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 +194 -24
- 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 +24 -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/package.json +7 -31
- package/src/PostgresBackendDriver.ts +56 -5
- package/src/PostgresBootstrapper.ts +18 -1
- package/src/auth/ensure-tables.ts +187 -19
- package/src/auth/services.ts +309 -170
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +53 -15
- 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-runtime.ts +1 -1
- package/src/security/policy-drift.test.ts +46 -0
- package/src/security/policy-drift.ts +70 -4
- package/src/security/rls-enforcement.ts +11 -5
- package/src/services/channel-history.ts +343 -0
- package/src/services/realtimeService.ts +198 -10
- package/src/websocket.ts +30 -11
package/src/auth/services.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
RoleData,
|
|
15
15
|
CreateRoleData,
|
|
16
16
|
RefreshTokenInfo,
|
|
17
|
+
RefreshTokenSession,
|
|
17
18
|
PasswordResetTokenInfo,
|
|
18
19
|
MagicLinkTokenInfo,
|
|
19
20
|
UserIdentityData,
|
|
@@ -86,7 +87,7 @@ export class UserService implements UserRepository {
|
|
|
86
87
|
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
87
88
|
*
|
|
88
89
|
* The auth services run on the base/owner connection, which by design
|
|
89
|
-
* carries a NULL `app.
|
|
90
|
+
* carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
|
|
90
91
|
* in the default policies applies. That NULL is normally guaranteed by
|
|
91
92
|
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
92
93
|
* GUC that survives on a pooled connection (or a connection role that
|
|
@@ -100,7 +101,8 @@ export class UserService implements UserRepository {
|
|
|
100
101
|
private async withServerContext<T>(fn: (db: NodePgDatabase) => Promise<T>): Promise<T> {
|
|
101
102
|
return await this.db.transaction(async (tx) => {
|
|
102
103
|
await tx.execute(sql`
|
|
103
|
-
SELECT set_config('app.
|
|
104
|
+
SELECT set_config('app.uid', '', true),
|
|
105
|
+
set_config('app.user_id', '', true),
|
|
104
106
|
set_config('app.user_roles', '', true),
|
|
105
107
|
set_config('app.jwt', '', true)
|
|
106
108
|
`);
|
|
@@ -253,7 +255,7 @@ export class UserService implements UserRepository {
|
|
|
253
255
|
const result = await this.db
|
|
254
256
|
.select({ user: this.usersTable })
|
|
255
257
|
.from(this.usersTable)
|
|
256
|
-
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.
|
|
258
|
+
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid))
|
|
257
259
|
.where(
|
|
258
260
|
sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`
|
|
259
261
|
)
|
|
@@ -263,17 +265,17 @@ export class UserService implements UserRepository {
|
|
|
263
265
|
return this.mapRowToUser(result[0].user as Record<string, unknown>);
|
|
264
266
|
}
|
|
265
267
|
|
|
266
|
-
async getUserIdentities(
|
|
268
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
267
269
|
const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
|
|
268
270
|
const result = await this.db.execute(sql`
|
|
269
|
-
SELECT id,
|
|
271
|
+
SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at
|
|
270
272
|
FROM ${sql.raw(`"${schema}"."user_identities"`)}
|
|
271
|
-
WHERE
|
|
273
|
+
WHERE uid = ${uid}
|
|
272
274
|
`);
|
|
273
275
|
|
|
274
276
|
return result.rows.map((row: Record<string, unknown>) => ({
|
|
275
277
|
id: row.id as string,
|
|
276
|
-
|
|
278
|
+
uid: row.uid as string,
|
|
277
279
|
provider: row.provider as string,
|
|
278
280
|
providerId: row.provider_id as string,
|
|
279
281
|
profileData: (row.profile_data as Record<string, unknown> | null) ?? null,
|
|
@@ -282,9 +284,9 @@ export class UserService implements UserRepository {
|
|
|
282
284
|
}));
|
|
283
285
|
}
|
|
284
286
|
|
|
285
|
-
async linkUserIdentity(
|
|
287
|
+
async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
286
288
|
await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
|
|
287
|
-
|
|
289
|
+
uid,
|
|
288
290
|
provider,
|
|
289
291
|
providerId,
|
|
290
292
|
profileData: profileData || null
|
|
@@ -452,10 +454,10 @@ export class UserService implements UserRepository {
|
|
|
452
454
|
/**
|
|
453
455
|
* Get roles for a user from database (inline TEXT[] column)
|
|
454
456
|
*/
|
|
455
|
-
async getUserRoles(
|
|
457
|
+
async getUserRoles(uid: string): Promise<Role[]> {
|
|
456
458
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
457
459
|
const result = await this.db.execute(sql`
|
|
458
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
460
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
459
461
|
`);
|
|
460
462
|
|
|
461
463
|
if (result.rows.length === 0) return [];
|
|
@@ -475,10 +477,10 @@ export class UserService implements UserRepository {
|
|
|
475
477
|
/**
|
|
476
478
|
* Get role IDs for a user
|
|
477
479
|
*/
|
|
478
|
-
async getUserRoleIds(
|
|
480
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
479
481
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
480
482
|
const result = await this.db.execute(sql`
|
|
481
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
483
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
482
484
|
`);
|
|
483
485
|
|
|
484
486
|
if (result.rows.length === 0) return [];
|
|
@@ -490,36 +492,36 @@ export class UserService implements UserRepository {
|
|
|
490
492
|
/**
|
|
491
493
|
* Set roles for a user (replaces existing roles)
|
|
492
494
|
*/
|
|
493
|
-
async setUserRoles(
|
|
495
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
494
496
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
495
497
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
496
498
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
497
499
|
UPDATE ${sql.raw(usersTableName)}
|
|
498
500
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
499
|
-
WHERE id = ${
|
|
501
|
+
WHERE id = ${uid}
|
|
500
502
|
`));
|
|
501
503
|
}
|
|
502
504
|
|
|
503
505
|
/**
|
|
504
506
|
* Assign a specific role to new user (appends if not present)
|
|
505
507
|
*/
|
|
506
|
-
async assignDefaultRole(
|
|
508
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
507
509
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
508
510
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
509
511
|
UPDATE ${sql.raw(usersTableName)}
|
|
510
512
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
511
|
-
WHERE id = ${
|
|
513
|
+
WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))
|
|
512
514
|
`));
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
/**
|
|
516
518
|
* Get user with their roles
|
|
517
519
|
*/
|
|
518
|
-
async getUserWithRoles(
|
|
519
|
-
const user = await this.getUserById(
|
|
520
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[] } | null> {
|
|
521
|
+
const user = await this.getUserById(uid);
|
|
520
522
|
if (!user) return null;
|
|
521
523
|
|
|
522
|
-
const roles = await this.getUserRoles(
|
|
524
|
+
const roles = await this.getUserRoles(uid);
|
|
523
525
|
return { user,
|
|
524
526
|
roles };
|
|
525
527
|
}
|
|
@@ -528,6 +530,7 @@ export class UserService implements UserRepository {
|
|
|
528
530
|
|
|
529
531
|
export class RefreshTokenService {
|
|
530
532
|
private refreshTokensTable: RebasePgTable;
|
|
533
|
+
private usersTable: RebasePgTable | null;
|
|
531
534
|
|
|
532
535
|
constructor(
|
|
533
536
|
private db: NodePgDatabase,
|
|
@@ -535,86 +538,182 @@ export class RefreshTokenService {
|
|
|
535
538
|
) {
|
|
536
539
|
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
537
540
|
this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as RebasePgTable;
|
|
541
|
+
this.usersTable = ((tableOrTables as Partial<AuthSchemaTables>).users || users) as RebasePgTable;
|
|
538
542
|
} else {
|
|
539
543
|
this.refreshTokensTable = (tableOrTables as RebasePgTable) || (refreshTokens as unknown as RebasePgTable);
|
|
544
|
+
this.usersTable = users as unknown as RebasePgTable;
|
|
540
545
|
}
|
|
541
546
|
}
|
|
542
547
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
548
|
+
/**
|
|
549
|
+
* Whether the table actually carries a column, so a host application that
|
|
550
|
+
* supplied its own `refresh_tokens` table — one that predates session
|
|
551
|
+
* grouping — degrades instead of throwing on every sign-in.
|
|
552
|
+
*/
|
|
553
|
+
private has(column: string): boolean {
|
|
554
|
+
return Boolean((this.refreshTokensTable as unknown as Record<string, unknown>)[column]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private col(column: string) {
|
|
558
|
+
return (this.refreshTokensTable as unknown as Record<string, never>)[column];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** The columns to read back, narrowed to the ones this table has. */
|
|
562
|
+
private selection() {
|
|
563
|
+
const selection: Record<string, never> = {
|
|
564
|
+
id: this.refreshTokensTable.id,
|
|
565
|
+
uid: this.refreshTokensTable.uid,
|
|
566
|
+
tokenHash: this.refreshTokensTable.tokenHash,
|
|
567
|
+
expiresAt: this.refreshTokensTable.expiresAt,
|
|
568
|
+
createdAt: this.refreshTokensTable.createdAt,
|
|
569
|
+
userAgent: this.refreshTokensTable.userAgent,
|
|
570
|
+
ipAddress: this.refreshTokensTable.ipAddress
|
|
571
|
+
} as unknown as Record<string, never>;
|
|
572
|
+
for (const optional of ["sessionId", "rotatedAt", "revoked", "sessionStartedAt"]) {
|
|
573
|
+
if (this.has(optional)) selection[optional] = this.col(optional);
|
|
574
|
+
}
|
|
575
|
+
return selection;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async createToken(
|
|
579
|
+
uid: string,
|
|
580
|
+
tokenHash: string,
|
|
581
|
+
expiresAt: Date,
|
|
582
|
+
userAgent?: string,
|
|
583
|
+
ipAddress?: string,
|
|
584
|
+
session?: RefreshTokenSession
|
|
585
|
+
): Promise<void> {
|
|
586
|
+
// Empty strings rather than NULLs: the device-session UNIQUE constraint
|
|
587
|
+
// that needed them is gone, but sessions-list UIs already render "" as
|
|
588
|
+
// "unknown device" and would start showing blanks otherwise.
|
|
546
589
|
const safeUserAgent = userAgent || "";
|
|
547
590
|
const safeIpAddress = ipAddress || "";
|
|
548
591
|
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
this.refreshTokensTable.userId,
|
|
565
|
-
this.refreshTokensTable.userAgent,
|
|
566
|
-
this.refreshTokensTable.ipAddress
|
|
567
|
-
],
|
|
568
|
-
set: { tokenHash, expiresAt }
|
|
569
|
-
});
|
|
592
|
+
// A plain INSERT. Rotation ADDS a token; it does not replace a device's
|
|
593
|
+
// row. Two refreshes racing on the same session therefore both succeed
|
|
594
|
+
// and both end holding a usable token, where the previous upsert had
|
|
595
|
+
// them overwrite each other and logged one of the two tabs out.
|
|
596
|
+
const values: Record<string, unknown> = {
|
|
597
|
+
uid,
|
|
598
|
+
tokenHash,
|
|
599
|
+
expiresAt,
|
|
600
|
+
userAgent: safeUserAgent,
|
|
601
|
+
ipAddress: safeIpAddress
|
|
602
|
+
};
|
|
603
|
+
if (session && this.has("sessionId")) values.sessionId = session.id;
|
|
604
|
+
if (session && this.has("sessionStartedAt")) values.sessionStartedAt = session.startedAt;
|
|
605
|
+
|
|
606
|
+
await this.db.insert(this.refreshTokensTable).values(values);
|
|
570
607
|
}
|
|
571
608
|
|
|
572
609
|
async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
573
610
|
const [token] = await this.db
|
|
574
|
-
.select(
|
|
575
|
-
id: this.refreshTokensTable.id,
|
|
576
|
-
userId: this.refreshTokensTable.userId,
|
|
577
|
-
tokenHash: this.refreshTokensTable.tokenHash,
|
|
578
|
-
expiresAt: this.refreshTokensTable.expiresAt,
|
|
579
|
-
createdAt: this.refreshTokensTable.createdAt,
|
|
580
|
-
userAgent: this.refreshTokensTable.userAgent,
|
|
581
|
-
ipAddress: this.refreshTokensTable.ipAddress
|
|
582
|
-
})
|
|
611
|
+
.select(this.selection())
|
|
583
612
|
.from(this.refreshTokensTable)
|
|
584
613
|
.where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
585
614
|
|
|
586
|
-
return (token as RefreshTokenInfo) || null;
|
|
615
|
+
return (token as unknown as RefreshTokenInfo) || null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Record that a token was rotated away, keeping the row.
|
|
620
|
+
*
|
|
621
|
+
* The row is what lets `/auth/refresh` distinguish "you already used this,
|
|
622
|
+
* here is a fresh one" from "no idea what this is". Deleting it — which is
|
|
623
|
+
* what this used to do — collapsed both into a 401 and signed the user out
|
|
624
|
+
* for the crime of losing a response.
|
|
625
|
+
*/
|
|
626
|
+
async markRotated(tokenHash: string): Promise<void> {
|
|
627
|
+
if (!this.has("rotatedAt")) {
|
|
628
|
+
await this.deleteByHash(tokenHash);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
await this.db
|
|
632
|
+
.update(this.refreshTokensTable)
|
|
633
|
+
.set({ rotatedAt: new Date() })
|
|
634
|
+
.where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Final kill of one sign-in: logout, or revoking a device remotely. */
|
|
638
|
+
async revokeSession(sessionId: string): Promise<void> {
|
|
639
|
+
if (!this.has("sessionId")) return;
|
|
640
|
+
if (this.has("revoked")) {
|
|
641
|
+
await this.db
|
|
642
|
+
.update(this.refreshTokensTable)
|
|
643
|
+
.set({ revoked: true, ...(this.has("rotatedAt") ? { rotatedAt: new Date() } : {}) })
|
|
644
|
+
.where(eq(this.col("sessionId"), sessionId));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.col("sessionId"), sessionId));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Housekeeping: rotation would otherwise leave a row per refresh forever.
|
|
652
|
+
* Superseded rows are only needed for as long as a straggler might still
|
|
653
|
+
* present them, and expired ones are dead weight everywhere.
|
|
654
|
+
*/
|
|
655
|
+
async prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
|
|
656
|
+
const uidCol = this.refreshTokensTable.uid;
|
|
657
|
+
const expiresCol = this.refreshTokensTable.expiresAt;
|
|
658
|
+
if (!this.has("rotatedAt") || !this.has("sessionId")) {
|
|
659
|
+
await this.db.delete(this.refreshTokensTable)
|
|
660
|
+
.where(sql`${uidCol} = ${uid} AND ${expiresCol} < NOW()`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const rotatedCol = this.col("rotatedAt");
|
|
664
|
+
const sessionCol = this.col("sessionId");
|
|
665
|
+
await this.db.delete(this.refreshTokensTable).where(sql`
|
|
666
|
+
${uidCol} = ${uid}
|
|
667
|
+
AND (
|
|
668
|
+
${expiresCol} < NOW()
|
|
669
|
+
OR (
|
|
670
|
+
${sessionCol} = ${sessionId}
|
|
671
|
+
AND ${rotatedCol} IS NOT NULL
|
|
672
|
+
AND ${rotatedCol} < ${supersededBefore}
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async getTokensValidAfter(uid: string): Promise<Date | null> {
|
|
679
|
+
if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return null;
|
|
680
|
+
const [row] = await this.db
|
|
681
|
+
.select({ tokensValidAfter: (this.usersTable as unknown as Record<string, never>).tokensValidAfter })
|
|
682
|
+
.from(this.usersTable)
|
|
683
|
+
.where(eq(this.usersTable.id, uid));
|
|
684
|
+
const value = (row as { tokensValidAfter?: Date | string | null } | undefined)?.tokensValidAfter;
|
|
685
|
+
return value ? new Date(value) : null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async setTokensValidAfter(uid: string, at: Date): Promise<void> {
|
|
689
|
+
if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return;
|
|
690
|
+
await this.db
|
|
691
|
+
.update(this.usersTable)
|
|
692
|
+
.set({ tokensValidAfter: at })
|
|
693
|
+
.where(eq(this.usersTable.id, uid));
|
|
587
694
|
}
|
|
588
695
|
|
|
589
696
|
async deleteByHash(tokenHash: string): Promise<void> {
|
|
590
697
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
591
698
|
}
|
|
592
699
|
|
|
593
|
-
async deleteAllForUser(
|
|
594
|
-
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.
|
|
700
|
+
async deleteAllForUser(uid: string): Promise<void> {
|
|
701
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
|
|
595
702
|
}
|
|
596
703
|
|
|
597
|
-
async listForUser(
|
|
704
|
+
async listForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
598
705
|
const tokens = await this.db
|
|
599
|
-
.select(
|
|
600
|
-
id: this.refreshTokensTable.id,
|
|
601
|
-
userId: this.refreshTokensTable.userId,
|
|
602
|
-
tokenHash: this.refreshTokensTable.tokenHash,
|
|
603
|
-
expiresAt: this.refreshTokensTable.expiresAt,
|
|
604
|
-
createdAt: this.refreshTokensTable.createdAt,
|
|
605
|
-
userAgent: this.refreshTokensTable.userAgent,
|
|
606
|
-
ipAddress: this.refreshTokensTable.ipAddress
|
|
607
|
-
})
|
|
706
|
+
.select(this.selection())
|
|
608
707
|
.from(this.refreshTokensTable)
|
|
609
|
-
.where(eq(this.refreshTokensTable.
|
|
708
|
+
.where(eq(this.refreshTokensTable.uid, uid))
|
|
610
709
|
.orderBy(this.refreshTokensTable.createdAt);
|
|
611
710
|
|
|
612
|
-
return tokens as RefreshTokenInfo[];
|
|
711
|
+
return tokens as unknown as RefreshTokenInfo[];
|
|
613
712
|
}
|
|
614
713
|
|
|
615
|
-
async deleteById(id: string,
|
|
714
|
+
async deleteById(id: string, uid: string): Promise<void> {
|
|
616
715
|
await this.db.delete(this.refreshTokensTable)
|
|
617
|
-
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.
|
|
716
|
+
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
|
|
618
717
|
}
|
|
619
718
|
}
|
|
620
719
|
|
|
@@ -644,16 +743,16 @@ export class PasswordResetTokenService {
|
|
|
644
743
|
/**
|
|
645
744
|
* Create a password reset token
|
|
646
745
|
*/
|
|
647
|
-
async createToken(
|
|
746
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
648
747
|
// Delete any existing unused tokens for this user
|
|
649
748
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
650
749
|
await this.db.execute(sql`
|
|
651
750
|
DELETE FROM ${sql.raw(tableName)}
|
|
652
|
-
WHERE
|
|
751
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
653
752
|
`);
|
|
654
753
|
|
|
655
754
|
await this.db.insert(this.passwordResetTokensTable).values({
|
|
656
|
-
|
|
755
|
+
uid,
|
|
657
756
|
tokenHash,
|
|
658
757
|
expiresAt
|
|
659
758
|
});
|
|
@@ -662,21 +761,21 @@ export class PasswordResetTokenService {
|
|
|
662
761
|
/**
|
|
663
762
|
* Find a valid (not expired, not used) token by hash
|
|
664
763
|
*/
|
|
665
|
-
async findValidByHash(tokenHash: string): Promise<{
|
|
764
|
+
async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {
|
|
666
765
|
const [token] = await this.db
|
|
667
766
|
.select({
|
|
668
|
-
|
|
767
|
+
uid: this.passwordResetTokensTable.uid,
|
|
669
768
|
expiresAt: this.passwordResetTokensTable.expiresAt
|
|
670
769
|
})
|
|
671
770
|
.from(this.passwordResetTokensTable)
|
|
672
|
-
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{
|
|
771
|
+
.where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{ uid: string; expiresAt: Date }>;
|
|
673
772
|
|
|
674
773
|
if (!token) return null;
|
|
675
774
|
|
|
676
775
|
// Check if expired or used
|
|
677
776
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
678
777
|
const result = await this.db.execute(sql`
|
|
679
|
-
SELECT
|
|
778
|
+
SELECT uid, expires_at
|
|
680
779
|
FROM ${sql.raw(tableName)}
|
|
681
780
|
WHERE token_hash = ${tokenHash}
|
|
682
781
|
AND used_at IS NULL
|
|
@@ -685,9 +784,9 @@ export class PasswordResetTokenService {
|
|
|
685
784
|
|
|
686
785
|
if (result.rows.length === 0) return null;
|
|
687
786
|
|
|
688
|
-
const row = result.rows[0] as {
|
|
787
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
689
788
|
return {
|
|
690
|
-
|
|
789
|
+
uid: row.uid,
|
|
691
790
|
expiresAt: new Date(row.expires_at)
|
|
692
791
|
};
|
|
693
792
|
}
|
|
@@ -705,8 +804,8 @@ export class PasswordResetTokenService {
|
|
|
705
804
|
/**
|
|
706
805
|
* Delete all tokens for a user
|
|
707
806
|
*/
|
|
708
|
-
async deleteAllForUser(
|
|
709
|
-
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.
|
|
807
|
+
async deleteAllForUser(uid: string): Promise<void> {
|
|
808
|
+
await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.uid, uid));
|
|
710
809
|
}
|
|
711
810
|
|
|
712
811
|
/**
|
|
@@ -741,16 +840,16 @@ export class MagicLinkTokenService {
|
|
|
741
840
|
return `"${schema}"."${name}"`;
|
|
742
841
|
}
|
|
743
842
|
|
|
744
|
-
async createToken(
|
|
843
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
745
844
|
// Delete any existing unused tokens for this user
|
|
746
845
|
const tableName = this.getQualifiedTableName();
|
|
747
846
|
await this.db.execute(sql`
|
|
748
847
|
DELETE FROM ${sql.raw(tableName)}
|
|
749
|
-
WHERE
|
|
848
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
750
849
|
`);
|
|
751
850
|
|
|
752
851
|
await this.db.insert(this.magicLinkTokensTable).values({
|
|
753
|
-
|
|
852
|
+
uid,
|
|
754
853
|
tokenHash,
|
|
755
854
|
expiresAt
|
|
756
855
|
});
|
|
@@ -759,7 +858,7 @@ export class MagicLinkTokenService {
|
|
|
759
858
|
async findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
760
859
|
const tableName = this.getQualifiedTableName();
|
|
761
860
|
const result = await this.db.execute(sql`
|
|
762
|
-
SELECT
|
|
861
|
+
SELECT uid, expires_at
|
|
763
862
|
FROM ${sql.raw(tableName)}
|
|
764
863
|
WHERE token_hash = ${tokenHash}
|
|
765
864
|
AND used_at IS NULL
|
|
@@ -768,9 +867,9 @@ export class MagicLinkTokenService {
|
|
|
768
867
|
|
|
769
868
|
if (result.rows.length === 0) return null;
|
|
770
869
|
|
|
771
|
-
const row = result.rows[0] as {
|
|
870
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
772
871
|
return {
|
|
773
|
-
|
|
872
|
+
uid: row.uid,
|
|
774
873
|
expiresAt: new Date(row.expires_at)
|
|
775
874
|
};
|
|
776
875
|
}
|
|
@@ -803,8 +902,28 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
803
902
|
|
|
804
903
|
// Refresh token operations
|
|
805
904
|
|
|
806
|
-
async createRefreshToken(
|
|
807
|
-
await this.refreshTokenService.createToken(
|
|
905
|
+
async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
|
|
906
|
+
await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async markRefreshTokenRotated(tokenHash: string): Promise<void> {
|
|
910
|
+
await this.refreshTokenService.markRotated(tokenHash);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async revokeRefreshTokenSession(sessionId: string): Promise<void> {
|
|
914
|
+
await this.refreshTokenService.revokeSession(sessionId);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
|
|
918
|
+
await this.refreshTokenService.prune(uid, sessionId, supersededBefore);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async getTokensValidAfter(uid: string): Promise<Date | null> {
|
|
922
|
+
return this.refreshTokenService.getTokensValidAfter(uid);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async setTokensValidAfter(uid: string, at: Date): Promise<void> {
|
|
926
|
+
await this.refreshTokenService.setTokensValidAfter(uid, at);
|
|
808
927
|
}
|
|
809
928
|
|
|
810
929
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -815,22 +934,22 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
815
934
|
await this.refreshTokenService.deleteByHash(tokenHash);
|
|
816
935
|
}
|
|
817
936
|
|
|
818
|
-
async deleteAllRefreshTokensForUser(
|
|
819
|
-
await this.refreshTokenService.deleteAllForUser(
|
|
937
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
938
|
+
await this.refreshTokenService.deleteAllForUser(uid);
|
|
820
939
|
}
|
|
821
940
|
|
|
822
|
-
async listRefreshTokensForUser(
|
|
823
|
-
return this.refreshTokenService.listForUser(
|
|
941
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
942
|
+
return this.refreshTokenService.listForUser(uid);
|
|
824
943
|
}
|
|
825
944
|
|
|
826
|
-
async deleteRefreshTokenById(id: string,
|
|
827
|
-
await this.refreshTokenService.deleteById(id,
|
|
945
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
946
|
+
await this.refreshTokenService.deleteById(id, uid);
|
|
828
947
|
}
|
|
829
948
|
|
|
830
949
|
// Password reset token operations
|
|
831
950
|
|
|
832
|
-
async createPasswordResetToken(
|
|
833
|
-
await this.passwordResetTokenService.createToken(
|
|
951
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
952
|
+
await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);
|
|
834
953
|
}
|
|
835
954
|
|
|
836
955
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -841,8 +960,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
841
960
|
await this.passwordResetTokenService.markAsUsed(tokenHash);
|
|
842
961
|
}
|
|
843
962
|
|
|
844
|
-
async deleteAllPasswordResetTokensForUser(
|
|
845
|
-
await this.passwordResetTokenService.deleteAllForUser(
|
|
963
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
964
|
+
await this.passwordResetTokenService.deleteAllForUser(uid);
|
|
846
965
|
}
|
|
847
966
|
|
|
848
967
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -851,8 +970,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
851
970
|
|
|
852
971
|
// Magic link token operations
|
|
853
972
|
|
|
854
|
-
async createMagicLinkToken(
|
|
855
|
-
await this.magicLinkTokenService.createToken(
|
|
973
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
974
|
+
await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);
|
|
856
975
|
}
|
|
857
976
|
|
|
858
977
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -899,12 +1018,12 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
899
1018
|
return this.userService.getUserByIdentity(provider, providerId);
|
|
900
1019
|
}
|
|
901
1020
|
|
|
902
|
-
async getUserIdentities(
|
|
903
|
-
return this.userService.getUserIdentities(
|
|
1021
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
1022
|
+
return this.userService.getUserIdentities(uid);
|
|
904
1023
|
}
|
|
905
1024
|
|
|
906
|
-
async linkUserIdentity(
|
|
907
|
-
return this.userService.linkUserIdentity(
|
|
1025
|
+
async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
1026
|
+
return this.userService.linkUserIdentity(uid, provider, providerId, profileData);
|
|
908
1027
|
}
|
|
909
1028
|
|
|
910
1029
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
@@ -939,24 +1058,24 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
939
1058
|
return this.userService.getUserByVerificationToken(token);
|
|
940
1059
|
}
|
|
941
1060
|
|
|
942
|
-
async getUserRoles(
|
|
943
|
-
return this.userService.getUserRoles(
|
|
1061
|
+
async getUserRoles(uid: string): Promise<RoleData[]> {
|
|
1062
|
+
return this.userService.getUserRoles(uid);
|
|
944
1063
|
}
|
|
945
1064
|
|
|
946
|
-
async getUserRoleIds(
|
|
947
|
-
return this.userService.getUserRoleIds(
|
|
1065
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
1066
|
+
return this.userService.getUserRoleIds(uid);
|
|
948
1067
|
}
|
|
949
1068
|
|
|
950
|
-
async setUserRoles(
|
|
951
|
-
await this.userService.setUserRoles(
|
|
1069
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
1070
|
+
await this.userService.setUserRoles(uid, roleIds);
|
|
952
1071
|
}
|
|
953
1072
|
|
|
954
|
-
async assignDefaultRole(
|
|
955
|
-
await this.userService.assignDefaultRole(
|
|
1073
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
1074
|
+
await this.userService.assignDefaultRole(uid, roleId);
|
|
956
1075
|
}
|
|
957
1076
|
|
|
958
|
-
async getUserWithRoles(
|
|
959
|
-
return this.userService.getUserWithRoles(
|
|
1077
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
|
|
1078
|
+
return this.userService.getUserWithRoles(uid);
|
|
960
1079
|
}
|
|
961
1080
|
|
|
962
1081
|
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
@@ -1017,8 +1136,28 @@ collectionPermissions: null }
|
|
|
1017
1136
|
|
|
1018
1137
|
// Token operations (delegate to PostgresTokenRepository)
|
|
1019
1138
|
|
|
1020
|
-
async createRefreshToken(
|
|
1021
|
-
await this.tokenRepository.createRefreshToken(
|
|
1139
|
+
async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
|
|
1140
|
+
await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async markRefreshTokenRotated(tokenHash: string): Promise<void> {
|
|
1144
|
+
await this.tokenRepository.markRefreshTokenRotated(tokenHash);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async revokeRefreshTokenSession(sessionId: string): Promise<void> {
|
|
1148
|
+
await this.tokenRepository.revokeRefreshTokenSession(sessionId);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
|
|
1152
|
+
await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async getTokensValidAfter(uid: string): Promise<Date | null> {
|
|
1156
|
+
return this.tokenRepository.getTokensValidAfter(uid);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async setTokensValidAfter(uid: string, at: Date): Promise<void> {
|
|
1160
|
+
await this.tokenRepository.setTokensValidAfter(uid, at);
|
|
1022
1161
|
}
|
|
1023
1162
|
|
|
1024
1163
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -1029,20 +1168,20 @@ collectionPermissions: null }
|
|
|
1029
1168
|
await this.tokenRepository.deleteRefreshToken(tokenHash);
|
|
1030
1169
|
}
|
|
1031
1170
|
|
|
1032
|
-
async deleteAllRefreshTokensForUser(
|
|
1033
|
-
await this.tokenRepository.deleteAllRefreshTokensForUser(
|
|
1171
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
1172
|
+
await this.tokenRepository.deleteAllRefreshTokensForUser(uid);
|
|
1034
1173
|
}
|
|
1035
1174
|
|
|
1036
|
-
async listRefreshTokensForUser(
|
|
1037
|
-
return this.tokenRepository.listRefreshTokensForUser(
|
|
1175
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
1176
|
+
return this.tokenRepository.listRefreshTokensForUser(uid);
|
|
1038
1177
|
}
|
|
1039
1178
|
|
|
1040
|
-
async deleteRefreshTokenById(id: string,
|
|
1041
|
-
await this.tokenRepository.deleteRefreshTokenById(id,
|
|
1179
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
1180
|
+
await this.tokenRepository.deleteRefreshTokenById(id, uid);
|
|
1042
1181
|
}
|
|
1043
1182
|
|
|
1044
|
-
async createPasswordResetToken(
|
|
1045
|
-
await this.tokenRepository.createPasswordResetToken(
|
|
1183
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1184
|
+
await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);
|
|
1046
1185
|
}
|
|
1047
1186
|
|
|
1048
1187
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -1053,8 +1192,8 @@ collectionPermissions: null }
|
|
|
1053
1192
|
await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
|
|
1054
1193
|
}
|
|
1055
1194
|
|
|
1056
|
-
async deleteAllPasswordResetTokensForUser(
|
|
1057
|
-
await this.tokenRepository.deleteAllPasswordResetTokensForUser(
|
|
1195
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
1196
|
+
await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);
|
|
1058
1197
|
}
|
|
1059
1198
|
|
|
1060
1199
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -1063,8 +1202,8 @@ collectionPermissions: null }
|
|
|
1063
1202
|
|
|
1064
1203
|
// Magic link token operations
|
|
1065
1204
|
|
|
1066
|
-
async createMagicLinkToken(
|
|
1067
|
-
await this.tokenRepository.createMagicLinkToken(
|
|
1205
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1206
|
+
await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);
|
|
1068
1207
|
}
|
|
1069
1208
|
|
|
1070
1209
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -1085,12 +1224,12 @@ collectionPermissions: null }
|
|
|
1085
1224
|
return this._mfaService;
|
|
1086
1225
|
}
|
|
1087
1226
|
|
|
1088
|
-
async createMfaFactor(
|
|
1089
|
-
return this.getMfaService().createMfaFactor(
|
|
1227
|
+
async createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
|
|
1228
|
+
return this.getMfaService().createMfaFactor(uid, factorType, secretEncrypted, friendlyName);
|
|
1090
1229
|
}
|
|
1091
1230
|
|
|
1092
|
-
async getMfaFactors(
|
|
1093
|
-
return this.getMfaService().getMfaFactors(
|
|
1231
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1232
|
+
return this.getMfaService().getMfaFactors(uid);
|
|
1094
1233
|
}
|
|
1095
1234
|
|
|
1096
1235
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
@@ -1101,8 +1240,8 @@ collectionPermissions: null }
|
|
|
1101
1240
|
return this.getMfaService().verifyMfaFactor(factorId);
|
|
1102
1241
|
}
|
|
1103
1242
|
|
|
1104
|
-
async deleteMfaFactor(factorId: string,
|
|
1105
|
-
return this.getMfaService().deleteMfaFactor(factorId,
|
|
1243
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1244
|
+
return this.getMfaService().deleteMfaFactor(factorId, uid);
|
|
1106
1245
|
}
|
|
1107
1246
|
|
|
1108
1247
|
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
@@ -1117,24 +1256,24 @@ collectionPermissions: null }
|
|
|
1117
1256
|
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
1118
1257
|
}
|
|
1119
1258
|
|
|
1120
|
-
async createRecoveryCodes(
|
|
1121
|
-
return this.getMfaService().createRecoveryCodes(
|
|
1259
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1260
|
+
return this.getMfaService().createRecoveryCodes(uid, codeHashes);
|
|
1122
1261
|
}
|
|
1123
1262
|
|
|
1124
|
-
async useRecoveryCode(
|
|
1125
|
-
return this.getMfaService().useRecoveryCode(
|
|
1263
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1264
|
+
return this.getMfaService().useRecoveryCode(uid, codeHash);
|
|
1126
1265
|
}
|
|
1127
1266
|
|
|
1128
|
-
async getUnusedRecoveryCodeCount(
|
|
1129
|
-
return this.getMfaService().getUnusedRecoveryCodeCount(
|
|
1267
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1268
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(uid);
|
|
1130
1269
|
}
|
|
1131
1270
|
|
|
1132
|
-
async deleteAllRecoveryCodes(
|
|
1133
|
-
return this.getMfaService().deleteAllRecoveryCodes(
|
|
1271
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1272
|
+
return this.getMfaService().deleteAllRecoveryCodes(uid);
|
|
1134
1273
|
}
|
|
1135
1274
|
|
|
1136
|
-
async hasVerifiedMfaFactors(
|
|
1137
|
-
return this.getMfaService().hasVerifiedMfaFactors(
|
|
1275
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1276
|
+
return this.getMfaService().hasVerifiedMfaFactors(uid);
|
|
1138
1277
|
}
|
|
1139
1278
|
}
|
|
1140
1279
|
|
|
@@ -1154,22 +1293,22 @@ export class MfaService implements MfaRepository {
|
|
|
1154
1293
|
}
|
|
1155
1294
|
|
|
1156
1295
|
async createMfaFactor(
|
|
1157
|
-
|
|
1296
|
+
uid: string,
|
|
1158
1297
|
factorType: "totp",
|
|
1159
1298
|
secretEncrypted: string,
|
|
1160
1299
|
friendlyName?: string
|
|
1161
1300
|
): Promise<MfaFactor> {
|
|
1162
1301
|
const tableName = this.qualify("mfa_factors");
|
|
1163
1302
|
const result = await this.db.execute(sql`
|
|
1164
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1165
|
-
VALUES (${
|
|
1166
|
-
RETURNING id,
|
|
1303
|
+
INSERT INTO ${sql.raw(tableName)} (uid, factor_type, secret_encrypted, friendly_name)
|
|
1304
|
+
VALUES (${uid}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
1305
|
+
RETURNING id, uid, factor_type, friendly_name, verified, created_at, updated_at
|
|
1167
1306
|
`);
|
|
1168
1307
|
|
|
1169
1308
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1170
1309
|
return {
|
|
1171
1310
|
id: row.id as string,
|
|
1172
|
-
|
|
1311
|
+
uid: row.uid as string,
|
|
1173
1312
|
factorType: row.factor_type as "totp",
|
|
1174
1313
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1175
1314
|
verified: row.verified as boolean,
|
|
@@ -1178,18 +1317,18 @@ export class MfaService implements MfaRepository {
|
|
|
1178
1317
|
};
|
|
1179
1318
|
}
|
|
1180
1319
|
|
|
1181
|
-
async getMfaFactors(
|
|
1320
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1182
1321
|
const tableName = this.qualify("mfa_factors");
|
|
1183
1322
|
const result = await this.db.execute(sql`
|
|
1184
|
-
SELECT id,
|
|
1323
|
+
SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at
|
|
1185
1324
|
FROM ${sql.raw(tableName)}
|
|
1186
|
-
WHERE
|
|
1325
|
+
WHERE uid = ${uid}
|
|
1187
1326
|
ORDER BY created_at
|
|
1188
1327
|
`);
|
|
1189
1328
|
|
|
1190
1329
|
return (result.rows as Array<Record<string, unknown>>).map(row => ({
|
|
1191
1330
|
id: row.id as string,
|
|
1192
|
-
|
|
1331
|
+
uid: row.uid as string,
|
|
1193
1332
|
factorType: row.factor_type as "totp",
|
|
1194
1333
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1195
1334
|
verified: row.verified as boolean,
|
|
@@ -1201,7 +1340,7 @@ export class MfaService implements MfaRepository {
|
|
|
1201
1340
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1202
1341
|
const tableName = this.qualify("mfa_factors");
|
|
1203
1342
|
const result = await this.db.execute(sql`
|
|
1204
|
-
SELECT id,
|
|
1343
|
+
SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
1205
1344
|
FROM ${sql.raw(tableName)}
|
|
1206
1345
|
WHERE id = ${factorId}
|
|
1207
1346
|
`);
|
|
@@ -1211,7 +1350,7 @@ export class MfaService implements MfaRepository {
|
|
|
1211
1350
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1212
1351
|
return {
|
|
1213
1352
|
id: row.id as string,
|
|
1214
|
-
|
|
1353
|
+
uid: row.uid as string,
|
|
1215
1354
|
factorType: row.factor_type as "totp",
|
|
1216
1355
|
secretEncrypted: row.secret_encrypted as string,
|
|
1217
1356
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
@@ -1230,11 +1369,11 @@ export class MfaService implements MfaRepository {
|
|
|
1230
1369
|
`);
|
|
1231
1370
|
}
|
|
1232
1371
|
|
|
1233
|
-
async deleteMfaFactor(factorId: string,
|
|
1372
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1234
1373
|
const tableName = this.qualify("mfa_factors");
|
|
1235
1374
|
await this.db.execute(sql`
|
|
1236
1375
|
DELETE FROM ${sql.raw(tableName)}
|
|
1237
|
-
WHERE id = ${factorId} AND
|
|
1376
|
+
WHERE id = ${factorId} AND uid = ${uid}
|
|
1238
1377
|
`);
|
|
1239
1378
|
}
|
|
1240
1379
|
|
|
@@ -1287,56 +1426,56 @@ export class MfaService implements MfaRepository {
|
|
|
1287
1426
|
`);
|
|
1288
1427
|
}
|
|
1289
1428
|
|
|
1290
|
-
async createRecoveryCodes(
|
|
1429
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1291
1430
|
const tableName = this.qualify("recovery_codes");
|
|
1292
1431
|
// Delete existing codes first
|
|
1293
1432
|
await this.db.execute(sql`
|
|
1294
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1433
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1295
1434
|
`);
|
|
1296
1435
|
|
|
1297
1436
|
// Insert new codes
|
|
1298
1437
|
for (const hash of codeHashes) {
|
|
1299
1438
|
await this.db.execute(sql`
|
|
1300
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1301
|
-
VALUES (${
|
|
1439
|
+
INSERT INTO ${sql.raw(tableName)} (uid, code_hash)
|
|
1440
|
+
VALUES (${uid}, ${hash})
|
|
1302
1441
|
`);
|
|
1303
1442
|
}
|
|
1304
1443
|
}
|
|
1305
1444
|
|
|
1306
|
-
async useRecoveryCode(
|
|
1445
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1307
1446
|
const tableName = this.qualify("recovery_codes");
|
|
1308
1447
|
const result = await this.db.execute(sql`
|
|
1309
1448
|
UPDATE ${sql.raw(tableName)}
|
|
1310
1449
|
SET used_at = NOW()
|
|
1311
|
-
WHERE
|
|
1450
|
+
WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
1312
1451
|
RETURNING id
|
|
1313
1452
|
`);
|
|
1314
1453
|
|
|
1315
1454
|
return result.rows.length > 0;
|
|
1316
1455
|
}
|
|
1317
1456
|
|
|
1318
|
-
async getUnusedRecoveryCodeCount(
|
|
1457
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1319
1458
|
const tableName = this.qualify("recovery_codes");
|
|
1320
1459
|
const result = await this.db.execute(sql`
|
|
1321
1460
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1322
|
-
WHERE
|
|
1461
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
1323
1462
|
`);
|
|
1324
1463
|
|
|
1325
1464
|
return (result.rows[0] as { count: number }).count;
|
|
1326
1465
|
}
|
|
1327
1466
|
|
|
1328
|
-
async deleteAllRecoveryCodes(
|
|
1467
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1329
1468
|
const tableName = this.qualify("recovery_codes");
|
|
1330
1469
|
await this.db.execute(sql`
|
|
1331
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1470
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1332
1471
|
`);
|
|
1333
1472
|
}
|
|
1334
1473
|
|
|
1335
|
-
async hasVerifiedMfaFactors(
|
|
1474
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1336
1475
|
const tableName = this.qualify("mfa_factors");
|
|
1337
1476
|
const result = await this.db.execute(sql`
|
|
1338
1477
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1339
|
-
WHERE
|
|
1478
|
+
WHERE uid = ${uid} AND verified = TRUE
|
|
1340
1479
|
`);
|
|
1341
1480
|
|
|
1342
1481
|
return (result.rows[0] as { count: number }).count > 0;
|