@rebasepro/server-postgres 0.9.1-canary.a639738 → 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 +7 -1
- package/dist/auth/services.d.ts +53 -53
- package/dist/index.es.js +708 -191
- 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/introspect-db-logic.d.ts +0 -5
- package/dist/schema/introspect-db-naming.d.ts +10 -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 +97 -17
- package/src/auth/services.ts +134 -133
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +13 -13
- 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/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
|
@@ -86,7 +86,7 @@ export class UserService implements UserRepository {
|
|
|
86
86
|
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
87
87
|
*
|
|
88
88
|
* The auth services run on the base/owner connection, which by design
|
|
89
|
-
* carries a NULL `app.
|
|
89
|
+
* carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
|
|
90
90
|
* in the default policies applies. That NULL is normally guaranteed by
|
|
91
91
|
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
92
92
|
* GUC that survives on a pooled connection (or a connection role that
|
|
@@ -100,7 +100,8 @@ export class UserService implements UserRepository {
|
|
|
100
100
|
private async withServerContext<T>(fn: (db: NodePgDatabase) => Promise<T>): Promise<T> {
|
|
101
101
|
return await this.db.transaction(async (tx) => {
|
|
102
102
|
await tx.execute(sql`
|
|
103
|
-
SELECT set_config('app.
|
|
103
|
+
SELECT set_config('app.uid', '', true),
|
|
104
|
+
set_config('app.user_id', '', true),
|
|
104
105
|
set_config('app.user_roles', '', true),
|
|
105
106
|
set_config('app.jwt', '', true)
|
|
106
107
|
`);
|
|
@@ -253,7 +254,7 @@ export class UserService implements UserRepository {
|
|
|
253
254
|
const result = await this.db
|
|
254
255
|
.select({ user: this.usersTable })
|
|
255
256
|
.from(this.usersTable)
|
|
256
|
-
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.
|
|
257
|
+
.innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid))
|
|
257
258
|
.where(
|
|
258
259
|
sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`
|
|
259
260
|
)
|
|
@@ -263,17 +264,17 @@ export class UserService implements UserRepository {
|
|
|
263
264
|
return this.mapRowToUser(result[0].user as Record<string, unknown>);
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
async getUserIdentities(
|
|
267
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
267
268
|
const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
|
|
268
269
|
const result = await this.db.execute(sql`
|
|
269
|
-
SELECT id,
|
|
270
|
+
SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at
|
|
270
271
|
FROM ${sql.raw(`"${schema}"."user_identities"`)}
|
|
271
|
-
WHERE
|
|
272
|
+
WHERE uid = ${uid}
|
|
272
273
|
`);
|
|
273
274
|
|
|
274
275
|
return result.rows.map((row: Record<string, unknown>) => ({
|
|
275
276
|
id: row.id as string,
|
|
276
|
-
|
|
277
|
+
uid: row.uid as string,
|
|
277
278
|
provider: row.provider as string,
|
|
278
279
|
providerId: row.provider_id as string,
|
|
279
280
|
profileData: (row.profile_data as Record<string, unknown> | null) ?? null,
|
|
@@ -282,9 +283,9 @@ export class UserService implements UserRepository {
|
|
|
282
283
|
}));
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
async linkUserIdentity(
|
|
286
|
+
async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
286
287
|
await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
|
|
287
|
-
|
|
288
|
+
uid,
|
|
288
289
|
provider,
|
|
289
290
|
providerId,
|
|
290
291
|
profileData: profileData || null
|
|
@@ -452,10 +453,10 @@ export class UserService implements UserRepository {
|
|
|
452
453
|
/**
|
|
453
454
|
* Get roles for a user from database (inline TEXT[] column)
|
|
454
455
|
*/
|
|
455
|
-
async getUserRoles(
|
|
456
|
+
async getUserRoles(uid: string): Promise<Role[]> {
|
|
456
457
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
457
458
|
const result = await this.db.execute(sql`
|
|
458
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
459
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
459
460
|
`);
|
|
460
461
|
|
|
461
462
|
if (result.rows.length === 0) return [];
|
|
@@ -475,10 +476,10 @@ export class UserService implements UserRepository {
|
|
|
475
476
|
/**
|
|
476
477
|
* Get role IDs for a user
|
|
477
478
|
*/
|
|
478
|
-
async getUserRoleIds(
|
|
479
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
479
480
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
480
481
|
const result = await this.db.execute(sql`
|
|
481
|
-
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${
|
|
482
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
|
|
482
483
|
`);
|
|
483
484
|
|
|
484
485
|
if (result.rows.length === 0) return [];
|
|
@@ -490,36 +491,36 @@ export class UserService implements UserRepository {
|
|
|
490
491
|
/**
|
|
491
492
|
* Set roles for a user (replaces existing roles)
|
|
492
493
|
*/
|
|
493
|
-
async setUserRoles(
|
|
494
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
494
495
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
495
496
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
496
497
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
497
498
|
UPDATE ${sql.raw(usersTableName)}
|
|
498
499
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
499
|
-
WHERE id = ${
|
|
500
|
+
WHERE id = ${uid}
|
|
500
501
|
`));
|
|
501
502
|
}
|
|
502
503
|
|
|
503
504
|
/**
|
|
504
505
|
* Assign a specific role to new user (appends if not present)
|
|
505
506
|
*/
|
|
506
|
-
async assignDefaultRole(
|
|
507
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
507
508
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
508
509
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
509
510
|
UPDATE ${sql.raw(usersTableName)}
|
|
510
511
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
511
|
-
WHERE id = ${
|
|
512
|
+
WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))
|
|
512
513
|
`));
|
|
513
514
|
}
|
|
514
515
|
|
|
515
516
|
/**
|
|
516
517
|
* Get user with their roles
|
|
517
518
|
*/
|
|
518
|
-
async getUserWithRoles(
|
|
519
|
-
const user = await this.getUserById(
|
|
519
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[] } | null> {
|
|
520
|
+
const user = await this.getUserById(uid);
|
|
520
521
|
if (!user) return null;
|
|
521
522
|
|
|
522
|
-
const roles = await this.getUserRoles(
|
|
523
|
+
const roles = await this.getUserRoles(uid);
|
|
523
524
|
return { user,
|
|
524
525
|
roles };
|
|
525
526
|
}
|
|
@@ -540,20 +541,20 @@ export class RefreshTokenService {
|
|
|
540
541
|
}
|
|
541
542
|
}
|
|
542
543
|
|
|
543
|
-
async createToken(
|
|
544
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
|
|
544
545
|
// Fallback to empty string because UNIQUE constraints treat NULLs as strictly distinct in standard Postgres.
|
|
545
|
-
// We want (
|
|
546
|
+
// We want (uid, NULL, NULL) to collide and overwrite, so we map undefined/null to empty strings.
|
|
546
547
|
const safeUserAgent = userAgent || "";
|
|
547
548
|
const safeIpAddress = ipAddress || "";
|
|
548
549
|
|
|
549
|
-
// Atomic upsert keyed on the device session (
|
|
550
|
+
// Atomic upsert keyed on the device session (uid, user_agent, ip_address).
|
|
550
551
|
// A DELETE-then-INSERT races under concurrent refreshes — cookie-mode boot can
|
|
551
552
|
// fire two /refresh calls at once (both carrying the same refresh cookie): both
|
|
552
553
|
// DELETE, then the second INSERT violates `unique_device_session` and 500s.
|
|
553
554
|
// A single INSERT ... ON CONFLICT DO UPDATE rotates the token atomically.
|
|
554
555
|
await this.db.insert(this.refreshTokensTable)
|
|
555
556
|
.values({
|
|
556
|
-
|
|
557
|
+
uid,
|
|
557
558
|
tokenHash,
|
|
558
559
|
expiresAt,
|
|
559
560
|
userAgent: safeUserAgent,
|
|
@@ -561,7 +562,7 @@ export class RefreshTokenService {
|
|
|
561
562
|
})
|
|
562
563
|
.onConflictDoUpdate({
|
|
563
564
|
target: [
|
|
564
|
-
this.refreshTokensTable.
|
|
565
|
+
this.refreshTokensTable.uid,
|
|
565
566
|
this.refreshTokensTable.userAgent,
|
|
566
567
|
this.refreshTokensTable.ipAddress
|
|
567
568
|
],
|
|
@@ -573,7 +574,7 @@ export class RefreshTokenService {
|
|
|
573
574
|
const [token] = await this.db
|
|
574
575
|
.select({
|
|
575
576
|
id: this.refreshTokensTable.id,
|
|
576
|
-
|
|
577
|
+
uid: this.refreshTokensTable.uid,
|
|
577
578
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
578
579
|
expiresAt: this.refreshTokensTable.expiresAt,
|
|
579
580
|
createdAt: this.refreshTokensTable.createdAt,
|
|
@@ -590,15 +591,15 @@ export class RefreshTokenService {
|
|
|
590
591
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
591
592
|
}
|
|
592
593
|
|
|
593
|
-
async deleteAllForUser(
|
|
594
|
-
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));
|
|
595
596
|
}
|
|
596
597
|
|
|
597
|
-
async listForUser(
|
|
598
|
+
async listForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
598
599
|
const tokens = await this.db
|
|
599
600
|
.select({
|
|
600
601
|
id: this.refreshTokensTable.id,
|
|
601
|
-
|
|
602
|
+
uid: this.refreshTokensTable.uid,
|
|
602
603
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
603
604
|
expiresAt: this.refreshTokensTable.expiresAt,
|
|
604
605
|
createdAt: this.refreshTokensTable.createdAt,
|
|
@@ -606,15 +607,15 @@ export class RefreshTokenService {
|
|
|
606
607
|
ipAddress: this.refreshTokensTable.ipAddress
|
|
607
608
|
})
|
|
608
609
|
.from(this.refreshTokensTable)
|
|
609
|
-
.where(eq(this.refreshTokensTable.
|
|
610
|
+
.where(eq(this.refreshTokensTable.uid, uid))
|
|
610
611
|
.orderBy(this.refreshTokensTable.createdAt);
|
|
611
612
|
|
|
612
613
|
return tokens as RefreshTokenInfo[];
|
|
613
614
|
}
|
|
614
615
|
|
|
615
|
-
async deleteById(id: string,
|
|
616
|
+
async deleteById(id: string, uid: string): Promise<void> {
|
|
616
617
|
await this.db.delete(this.refreshTokensTable)
|
|
617
|
-
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.
|
|
618
|
+
.where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
|
|
618
619
|
}
|
|
619
620
|
}
|
|
620
621
|
|
|
@@ -644,16 +645,16 @@ export class PasswordResetTokenService {
|
|
|
644
645
|
/**
|
|
645
646
|
* Create a password reset token
|
|
646
647
|
*/
|
|
647
|
-
async createToken(
|
|
648
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
648
649
|
// Delete any existing unused tokens for this user
|
|
649
650
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
650
651
|
await this.db.execute(sql`
|
|
651
652
|
DELETE FROM ${sql.raw(tableName)}
|
|
652
|
-
WHERE
|
|
653
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
653
654
|
`);
|
|
654
655
|
|
|
655
656
|
await this.db.insert(this.passwordResetTokensTable).values({
|
|
656
|
-
|
|
657
|
+
uid,
|
|
657
658
|
tokenHash,
|
|
658
659
|
expiresAt
|
|
659
660
|
});
|
|
@@ -662,21 +663,21 @@ export class PasswordResetTokenService {
|
|
|
662
663
|
/**
|
|
663
664
|
* Find a valid (not expired, not used) token by hash
|
|
664
665
|
*/
|
|
665
|
-
async findValidByHash(tokenHash: string): Promise<{
|
|
666
|
+
async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {
|
|
666
667
|
const [token] = await this.db
|
|
667
668
|
.select({
|
|
668
|
-
|
|
669
|
+
uid: this.passwordResetTokensTable.uid,
|
|
669
670
|
expiresAt: this.passwordResetTokensTable.expiresAt
|
|
670
671
|
})
|
|
671
672
|
.from(this.passwordResetTokensTable)
|
|
672
|
-
.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 }>;
|
|
673
674
|
|
|
674
675
|
if (!token) return null;
|
|
675
676
|
|
|
676
677
|
// Check if expired or used
|
|
677
678
|
const tableName = this.getQualifiedPasswordResetTokensTableName();
|
|
678
679
|
const result = await this.db.execute(sql`
|
|
679
|
-
SELECT
|
|
680
|
+
SELECT uid, expires_at
|
|
680
681
|
FROM ${sql.raw(tableName)}
|
|
681
682
|
WHERE token_hash = ${tokenHash}
|
|
682
683
|
AND used_at IS NULL
|
|
@@ -685,9 +686,9 @@ export class PasswordResetTokenService {
|
|
|
685
686
|
|
|
686
687
|
if (result.rows.length === 0) return null;
|
|
687
688
|
|
|
688
|
-
const row = result.rows[0] as {
|
|
689
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
689
690
|
return {
|
|
690
|
-
|
|
691
|
+
uid: row.uid,
|
|
691
692
|
expiresAt: new Date(row.expires_at)
|
|
692
693
|
};
|
|
693
694
|
}
|
|
@@ -705,8 +706,8 @@ export class PasswordResetTokenService {
|
|
|
705
706
|
/**
|
|
706
707
|
* Delete all tokens for a user
|
|
707
708
|
*/
|
|
708
|
-
async deleteAllForUser(
|
|
709
|
-
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));
|
|
710
711
|
}
|
|
711
712
|
|
|
712
713
|
/**
|
|
@@ -741,16 +742,16 @@ export class MagicLinkTokenService {
|
|
|
741
742
|
return `"${schema}"."${name}"`;
|
|
742
743
|
}
|
|
743
744
|
|
|
744
|
-
async createToken(
|
|
745
|
+
async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
745
746
|
// Delete any existing unused tokens for this user
|
|
746
747
|
const tableName = this.getQualifiedTableName();
|
|
747
748
|
await this.db.execute(sql`
|
|
748
749
|
DELETE FROM ${sql.raw(tableName)}
|
|
749
|
-
WHERE
|
|
750
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
750
751
|
`);
|
|
751
752
|
|
|
752
753
|
await this.db.insert(this.magicLinkTokensTable).values({
|
|
753
|
-
|
|
754
|
+
uid,
|
|
754
755
|
tokenHash,
|
|
755
756
|
expiresAt
|
|
756
757
|
});
|
|
@@ -759,7 +760,7 @@ export class MagicLinkTokenService {
|
|
|
759
760
|
async findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
760
761
|
const tableName = this.getQualifiedTableName();
|
|
761
762
|
const result = await this.db.execute(sql`
|
|
762
|
-
SELECT
|
|
763
|
+
SELECT uid, expires_at
|
|
763
764
|
FROM ${sql.raw(tableName)}
|
|
764
765
|
WHERE token_hash = ${tokenHash}
|
|
765
766
|
AND used_at IS NULL
|
|
@@ -768,9 +769,9 @@ export class MagicLinkTokenService {
|
|
|
768
769
|
|
|
769
770
|
if (result.rows.length === 0) return null;
|
|
770
771
|
|
|
771
|
-
const row = result.rows[0] as {
|
|
772
|
+
const row = result.rows[0] as { uid: string; expires_at: string | number | Date };
|
|
772
773
|
return {
|
|
773
|
-
|
|
774
|
+
uid: row.uid,
|
|
774
775
|
expiresAt: new Date(row.expires_at)
|
|
775
776
|
};
|
|
776
777
|
}
|
|
@@ -803,8 +804,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
803
804
|
|
|
804
805
|
// Refresh token operations
|
|
805
806
|
|
|
806
|
-
async createRefreshToken(
|
|
807
|
-
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);
|
|
808
809
|
}
|
|
809
810
|
|
|
810
811
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -815,22 +816,22 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
815
816
|
await this.refreshTokenService.deleteByHash(tokenHash);
|
|
816
817
|
}
|
|
817
818
|
|
|
818
|
-
async deleteAllRefreshTokensForUser(
|
|
819
|
-
await this.refreshTokenService.deleteAllForUser(
|
|
819
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
820
|
+
await this.refreshTokenService.deleteAllForUser(uid);
|
|
820
821
|
}
|
|
821
822
|
|
|
822
|
-
async listRefreshTokensForUser(
|
|
823
|
-
return this.refreshTokenService.listForUser(
|
|
823
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
824
|
+
return this.refreshTokenService.listForUser(uid);
|
|
824
825
|
}
|
|
825
826
|
|
|
826
|
-
async deleteRefreshTokenById(id: string,
|
|
827
|
-
await this.refreshTokenService.deleteById(id,
|
|
827
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
828
|
+
await this.refreshTokenService.deleteById(id, uid);
|
|
828
829
|
}
|
|
829
830
|
|
|
830
831
|
// Password reset token operations
|
|
831
832
|
|
|
832
|
-
async createPasswordResetToken(
|
|
833
|
-
await this.passwordResetTokenService.createToken(
|
|
833
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
834
|
+
await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);
|
|
834
835
|
}
|
|
835
836
|
|
|
836
837
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -841,8 +842,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
841
842
|
await this.passwordResetTokenService.markAsUsed(tokenHash);
|
|
842
843
|
}
|
|
843
844
|
|
|
844
|
-
async deleteAllPasswordResetTokensForUser(
|
|
845
|
-
await this.passwordResetTokenService.deleteAllForUser(
|
|
845
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
846
|
+
await this.passwordResetTokenService.deleteAllForUser(uid);
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -851,8 +852,8 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
851
852
|
|
|
852
853
|
// Magic link token operations
|
|
853
854
|
|
|
854
|
-
async createMagicLinkToken(
|
|
855
|
-
await this.magicLinkTokenService.createToken(
|
|
855
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
856
|
+
await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);
|
|
856
857
|
}
|
|
857
858
|
|
|
858
859
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -899,12 +900,12 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
899
900
|
return this.userService.getUserByIdentity(provider, providerId);
|
|
900
901
|
}
|
|
901
902
|
|
|
902
|
-
async getUserIdentities(
|
|
903
|
-
return this.userService.getUserIdentities(
|
|
903
|
+
async getUserIdentities(uid: string): Promise<UserIdentityData[]> {
|
|
904
|
+
return this.userService.getUserIdentities(uid);
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
async linkUserIdentity(
|
|
907
|
-
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);
|
|
908
909
|
}
|
|
909
910
|
|
|
910
911
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|
|
@@ -939,24 +940,24 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
939
940
|
return this.userService.getUserByVerificationToken(token);
|
|
940
941
|
}
|
|
941
942
|
|
|
942
|
-
async getUserRoles(
|
|
943
|
-
return this.userService.getUserRoles(
|
|
943
|
+
async getUserRoles(uid: string): Promise<RoleData[]> {
|
|
944
|
+
return this.userService.getUserRoles(uid);
|
|
944
945
|
}
|
|
945
946
|
|
|
946
|
-
async getUserRoleIds(
|
|
947
|
-
return this.userService.getUserRoleIds(
|
|
947
|
+
async getUserRoleIds(uid: string): Promise<string[]> {
|
|
948
|
+
return this.userService.getUserRoleIds(uid);
|
|
948
949
|
}
|
|
949
950
|
|
|
950
|
-
async setUserRoles(
|
|
951
|
-
await this.userService.setUserRoles(
|
|
951
|
+
async setUserRoles(uid: string, roleIds: string[]): Promise<void> {
|
|
952
|
+
await this.userService.setUserRoles(uid, roleIds);
|
|
952
953
|
}
|
|
953
954
|
|
|
954
|
-
async assignDefaultRole(
|
|
955
|
-
await this.userService.assignDefaultRole(
|
|
955
|
+
async assignDefaultRole(uid: string, roleId: string): Promise<void> {
|
|
956
|
+
await this.userService.assignDefaultRole(uid, roleId);
|
|
956
957
|
}
|
|
957
958
|
|
|
958
|
-
async getUserWithRoles(
|
|
959
|
-
return this.userService.getUserWithRoles(
|
|
959
|
+
async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
|
|
960
|
+
return this.userService.getUserWithRoles(uid);
|
|
960
961
|
}
|
|
961
962
|
|
|
962
963
|
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
@@ -1017,8 +1018,8 @@ collectionPermissions: null }
|
|
|
1017
1018
|
|
|
1018
1019
|
// Token operations (delegate to PostgresTokenRepository)
|
|
1019
1020
|
|
|
1020
|
-
async createRefreshToken(
|
|
1021
|
-
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);
|
|
1022
1023
|
}
|
|
1023
1024
|
|
|
1024
1025
|
async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
|
|
@@ -1029,20 +1030,20 @@ collectionPermissions: null }
|
|
|
1029
1030
|
await this.tokenRepository.deleteRefreshToken(tokenHash);
|
|
1030
1031
|
}
|
|
1031
1032
|
|
|
1032
|
-
async deleteAllRefreshTokensForUser(
|
|
1033
|
-
await this.tokenRepository.deleteAllRefreshTokensForUser(
|
|
1033
|
+
async deleteAllRefreshTokensForUser(uid: string): Promise<void> {
|
|
1034
|
+
await this.tokenRepository.deleteAllRefreshTokensForUser(uid);
|
|
1034
1035
|
}
|
|
1035
1036
|
|
|
1036
|
-
async listRefreshTokensForUser(
|
|
1037
|
-
return this.tokenRepository.listRefreshTokensForUser(
|
|
1037
|
+
async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {
|
|
1038
|
+
return this.tokenRepository.listRefreshTokensForUser(uid);
|
|
1038
1039
|
}
|
|
1039
1040
|
|
|
1040
|
-
async deleteRefreshTokenById(id: string,
|
|
1041
|
-
await this.tokenRepository.deleteRefreshTokenById(id,
|
|
1041
|
+
async deleteRefreshTokenById(id: string, uid: string): Promise<void> {
|
|
1042
|
+
await this.tokenRepository.deleteRefreshTokenById(id, uid);
|
|
1042
1043
|
}
|
|
1043
1044
|
|
|
1044
|
-
async createPasswordResetToken(
|
|
1045
|
-
await this.tokenRepository.createPasswordResetToken(
|
|
1045
|
+
async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1046
|
+
await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);
|
|
1046
1047
|
}
|
|
1047
1048
|
|
|
1048
1049
|
async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
|
|
@@ -1053,8 +1054,8 @@ collectionPermissions: null }
|
|
|
1053
1054
|
await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
|
|
1054
1055
|
}
|
|
1055
1056
|
|
|
1056
|
-
async deleteAllPasswordResetTokensForUser(
|
|
1057
|
-
await this.tokenRepository.deleteAllPasswordResetTokensForUser(
|
|
1057
|
+
async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {
|
|
1058
|
+
await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);
|
|
1058
1059
|
}
|
|
1059
1060
|
|
|
1060
1061
|
async deleteExpiredTokens(): Promise<void> {
|
|
@@ -1063,8 +1064,8 @@ collectionPermissions: null }
|
|
|
1063
1064
|
|
|
1064
1065
|
// Magic link token operations
|
|
1065
1066
|
|
|
1066
|
-
async createMagicLinkToken(
|
|
1067
|
-
await this.tokenRepository.createMagicLinkToken(
|
|
1067
|
+
async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {
|
|
1068
|
+
await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);
|
|
1068
1069
|
}
|
|
1069
1070
|
|
|
1070
1071
|
async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {
|
|
@@ -1085,12 +1086,12 @@ collectionPermissions: null }
|
|
|
1085
1086
|
return this._mfaService;
|
|
1086
1087
|
}
|
|
1087
1088
|
|
|
1088
|
-
async createMfaFactor(
|
|
1089
|
-
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);
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1092
|
-
async getMfaFactors(
|
|
1093
|
-
return this.getMfaService().getMfaFactors(
|
|
1093
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1094
|
+
return this.getMfaService().getMfaFactors(uid);
|
|
1094
1095
|
}
|
|
1095
1096
|
|
|
1096
1097
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
@@ -1101,8 +1102,8 @@ collectionPermissions: null }
|
|
|
1101
1102
|
return this.getMfaService().verifyMfaFactor(factorId);
|
|
1102
1103
|
}
|
|
1103
1104
|
|
|
1104
|
-
async deleteMfaFactor(factorId: string,
|
|
1105
|
-
return this.getMfaService().deleteMfaFactor(factorId,
|
|
1105
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1106
|
+
return this.getMfaService().deleteMfaFactor(factorId, uid);
|
|
1106
1107
|
}
|
|
1107
1108
|
|
|
1108
1109
|
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
@@ -1117,24 +1118,24 @@ collectionPermissions: null }
|
|
|
1117
1118
|
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
1118
1119
|
}
|
|
1119
1120
|
|
|
1120
|
-
async createRecoveryCodes(
|
|
1121
|
-
return this.getMfaService().createRecoveryCodes(
|
|
1121
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1122
|
+
return this.getMfaService().createRecoveryCodes(uid, codeHashes);
|
|
1122
1123
|
}
|
|
1123
1124
|
|
|
1124
|
-
async useRecoveryCode(
|
|
1125
|
-
return this.getMfaService().useRecoveryCode(
|
|
1125
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1126
|
+
return this.getMfaService().useRecoveryCode(uid, codeHash);
|
|
1126
1127
|
}
|
|
1127
1128
|
|
|
1128
|
-
async getUnusedRecoveryCodeCount(
|
|
1129
|
-
return this.getMfaService().getUnusedRecoveryCodeCount(
|
|
1129
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1130
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(uid);
|
|
1130
1131
|
}
|
|
1131
1132
|
|
|
1132
|
-
async deleteAllRecoveryCodes(
|
|
1133
|
-
return this.getMfaService().deleteAllRecoveryCodes(
|
|
1133
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1134
|
+
return this.getMfaService().deleteAllRecoveryCodes(uid);
|
|
1134
1135
|
}
|
|
1135
1136
|
|
|
1136
|
-
async hasVerifiedMfaFactors(
|
|
1137
|
-
return this.getMfaService().hasVerifiedMfaFactors(
|
|
1137
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1138
|
+
return this.getMfaService().hasVerifiedMfaFactors(uid);
|
|
1138
1139
|
}
|
|
1139
1140
|
}
|
|
1140
1141
|
|
|
@@ -1154,22 +1155,22 @@ export class MfaService implements MfaRepository {
|
|
|
1154
1155
|
}
|
|
1155
1156
|
|
|
1156
1157
|
async createMfaFactor(
|
|
1157
|
-
|
|
1158
|
+
uid: string,
|
|
1158
1159
|
factorType: "totp",
|
|
1159
1160
|
secretEncrypted: string,
|
|
1160
1161
|
friendlyName?: string
|
|
1161
1162
|
): Promise<MfaFactor> {
|
|
1162
1163
|
const tableName = this.qualify("mfa_factors");
|
|
1163
1164
|
const result = await this.db.execute(sql`
|
|
1164
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1165
|
-
VALUES (${
|
|
1166
|
-
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
|
|
1167
1168
|
`);
|
|
1168
1169
|
|
|
1169
1170
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1170
1171
|
return {
|
|
1171
1172
|
id: row.id as string,
|
|
1172
|
-
|
|
1173
|
+
uid: row.uid as string,
|
|
1173
1174
|
factorType: row.factor_type as "totp",
|
|
1174
1175
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1175
1176
|
verified: row.verified as boolean,
|
|
@@ -1178,18 +1179,18 @@ export class MfaService implements MfaRepository {
|
|
|
1178
1179
|
};
|
|
1179
1180
|
}
|
|
1180
1181
|
|
|
1181
|
-
async getMfaFactors(
|
|
1182
|
+
async getMfaFactors(uid: string): Promise<MfaFactor[]> {
|
|
1182
1183
|
const tableName = this.qualify("mfa_factors");
|
|
1183
1184
|
const result = await this.db.execute(sql`
|
|
1184
|
-
SELECT id,
|
|
1185
|
+
SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at
|
|
1185
1186
|
FROM ${sql.raw(tableName)}
|
|
1186
|
-
WHERE
|
|
1187
|
+
WHERE uid = ${uid}
|
|
1187
1188
|
ORDER BY created_at
|
|
1188
1189
|
`);
|
|
1189
1190
|
|
|
1190
1191
|
return (result.rows as Array<Record<string, unknown>>).map(row => ({
|
|
1191
1192
|
id: row.id as string,
|
|
1192
|
-
|
|
1193
|
+
uid: row.uid as string,
|
|
1193
1194
|
factorType: row.factor_type as "totp",
|
|
1194
1195
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1195
1196
|
verified: row.verified as boolean,
|
|
@@ -1201,7 +1202,7 @@ export class MfaService implements MfaRepository {
|
|
|
1201
1202
|
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1202
1203
|
const tableName = this.qualify("mfa_factors");
|
|
1203
1204
|
const result = await this.db.execute(sql`
|
|
1204
|
-
SELECT id,
|
|
1205
|
+
SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
1205
1206
|
FROM ${sql.raw(tableName)}
|
|
1206
1207
|
WHERE id = ${factorId}
|
|
1207
1208
|
`);
|
|
@@ -1211,7 +1212,7 @@ export class MfaService implements MfaRepository {
|
|
|
1211
1212
|
const row = result.rows[0] as Record<string, unknown>;
|
|
1212
1213
|
return {
|
|
1213
1214
|
id: row.id as string,
|
|
1214
|
-
|
|
1215
|
+
uid: row.uid as string,
|
|
1215
1216
|
factorType: row.factor_type as "totp",
|
|
1216
1217
|
secretEncrypted: row.secret_encrypted as string,
|
|
1217
1218
|
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
@@ -1230,11 +1231,11 @@ export class MfaService implements MfaRepository {
|
|
|
1230
1231
|
`);
|
|
1231
1232
|
}
|
|
1232
1233
|
|
|
1233
|
-
async deleteMfaFactor(factorId: string,
|
|
1234
|
+
async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
|
|
1234
1235
|
const tableName = this.qualify("mfa_factors");
|
|
1235
1236
|
await this.db.execute(sql`
|
|
1236
1237
|
DELETE FROM ${sql.raw(tableName)}
|
|
1237
|
-
WHERE id = ${factorId} AND
|
|
1238
|
+
WHERE id = ${factorId} AND uid = ${uid}
|
|
1238
1239
|
`);
|
|
1239
1240
|
}
|
|
1240
1241
|
|
|
@@ -1287,56 +1288,56 @@ export class MfaService implements MfaRepository {
|
|
|
1287
1288
|
`);
|
|
1288
1289
|
}
|
|
1289
1290
|
|
|
1290
|
-
async createRecoveryCodes(
|
|
1291
|
+
async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
|
|
1291
1292
|
const tableName = this.qualify("recovery_codes");
|
|
1292
1293
|
// Delete existing codes first
|
|
1293
1294
|
await this.db.execute(sql`
|
|
1294
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1295
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1295
1296
|
`);
|
|
1296
1297
|
|
|
1297
1298
|
// Insert new codes
|
|
1298
1299
|
for (const hash of codeHashes) {
|
|
1299
1300
|
await this.db.execute(sql`
|
|
1300
|
-
INSERT INTO ${sql.raw(tableName)} (
|
|
1301
|
-
VALUES (${
|
|
1301
|
+
INSERT INTO ${sql.raw(tableName)} (uid, code_hash)
|
|
1302
|
+
VALUES (${uid}, ${hash})
|
|
1302
1303
|
`);
|
|
1303
1304
|
}
|
|
1304
1305
|
}
|
|
1305
1306
|
|
|
1306
|
-
async useRecoveryCode(
|
|
1307
|
+
async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
|
|
1307
1308
|
const tableName = this.qualify("recovery_codes");
|
|
1308
1309
|
const result = await this.db.execute(sql`
|
|
1309
1310
|
UPDATE ${sql.raw(tableName)}
|
|
1310
1311
|
SET used_at = NOW()
|
|
1311
|
-
WHERE
|
|
1312
|
+
WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
1312
1313
|
RETURNING id
|
|
1313
1314
|
`);
|
|
1314
1315
|
|
|
1315
1316
|
return result.rows.length > 0;
|
|
1316
1317
|
}
|
|
1317
1318
|
|
|
1318
|
-
async getUnusedRecoveryCodeCount(
|
|
1319
|
+
async getUnusedRecoveryCodeCount(uid: string): Promise<number> {
|
|
1319
1320
|
const tableName = this.qualify("recovery_codes");
|
|
1320
1321
|
const result = await this.db.execute(sql`
|
|
1321
1322
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1322
|
-
WHERE
|
|
1323
|
+
WHERE uid = ${uid} AND used_at IS NULL
|
|
1323
1324
|
`);
|
|
1324
1325
|
|
|
1325
1326
|
return (result.rows[0] as { count: number }).count;
|
|
1326
1327
|
}
|
|
1327
1328
|
|
|
1328
|
-
async deleteAllRecoveryCodes(
|
|
1329
|
+
async deleteAllRecoveryCodes(uid: string): Promise<void> {
|
|
1329
1330
|
const tableName = this.qualify("recovery_codes");
|
|
1330
1331
|
await this.db.execute(sql`
|
|
1331
|
-
DELETE FROM ${sql.raw(tableName)} WHERE
|
|
1332
|
+
DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
|
|
1332
1333
|
`);
|
|
1333
1334
|
}
|
|
1334
1335
|
|
|
1335
|
-
async hasVerifiedMfaFactors(
|
|
1336
|
+
async hasVerifiedMfaFactors(uid: string): Promise<boolean> {
|
|
1336
1337
|
const tableName = this.qualify("mfa_factors");
|
|
1337
1338
|
const result = await this.db.execute(sql`
|
|
1338
1339
|
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1339
|
-
WHERE
|
|
1340
|
+
WHERE uid = ${uid} AND verified = TRUE
|
|
1340
1341
|
`);
|
|
1341
1342
|
|
|
1342
1343
|
return (result.rows[0] as { count: number }).count > 0;
|