@rebasepro/server-postgresql 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/index.es.js +1075 -470
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1071 -466
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +3 -1
  9. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  10. package/dist/server-postgresql/src/auth/services.d.ts +48 -31
  11. package/dist/server-postgresql/src/connection.d.ts +25 -0
  12. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2135 -41
  13. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  14. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  15. package/dist/server-postgresql/src/services/entityService.d.ts +6 -0
  16. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  17. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  18. package/dist/types/src/controllers/auth.d.ts +4 -26
  19. package/dist/types/src/controllers/client.d.ts +25 -43
  20. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  21. package/dist/types/src/controllers/data.d.ts +4 -0
  22. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  23. package/dist/types/src/controllers/registry.d.ts +5 -4
  24. package/dist/types/src/rebase_context.d.ts +1 -1
  25. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  26. package/dist/types/src/types/backend.d.ts +2 -2
  27. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  28. package/dist/types/src/types/collections.d.ts +0 -4
  29. package/dist/types/src/types/component_ref.d.ts +1 -1
  30. package/dist/types/src/types/cron.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +1 -0
  32. package/dist/types/src/types/export_import.d.ts +1 -1
  33. package/dist/types/src/types/formex.d.ts +2 -2
  34. package/dist/types/src/types/properties.d.ts +9 -7
  35. package/dist/types/src/types/translations.d.ts +28 -12
  36. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  37. package/dist/types/src/users/index.d.ts +0 -1
  38. package/dist/types/src/users/user.d.ts +0 -1
  39. package/package.json +6 -6
  40. package/src/PostgresBackendDriver.ts +14 -2
  41. package/src/PostgresBootstrapper.ts +30 -20
  42. package/src/auth/ensure-tables.ts +116 -103
  43. package/src/auth/services.ts +347 -177
  44. package/src/connection.ts +77 -0
  45. package/src/data-transformer.ts +2 -2
  46. package/src/schema/auth-schema.ts +85 -75
  47. package/src/schema/doctor.ts +44 -3
  48. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  49. package/src/schema/generate-drizzle-schema.ts +6 -6
  50. package/src/schema/introspect-db-logic.ts +7 -0
  51. package/src/services/EntityFetchService.ts +69 -10
  52. package/src/services/EntityPersistService.ts +9 -0
  53. package/src/services/entityService.ts +9 -0
  54. package/src/services/realtimeService.ts +214 -2
  55. package/src/utils/drizzle-conditions.ts +74 -2
  56. package/src/websocket.ts +10 -2
  57. package/test/auth-services.test.ts +10 -166
  58. package/test/doctor.test.ts +6 -2
  59. package/test/drizzle-conditions.test.ts +168 -0
  60. package/vite.config.ts +1 -1
  61. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  62. package/dist/types/src/users/roles.d.ts +0 -22
  63. package/src/schema/default-collections.ts +0 -69
@@ -1,11 +1,12 @@
1
1
  import { eq, getTableName, sql } from "drizzle-orm";
2
2
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import { getTableConfig, PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
4
- import { users, roles, userRoles, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
4
+ import { users, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
5
5
  import {
6
6
  UserRepository,
7
7
  RoleRepository,
8
8
  TokenRepository,
9
+ MfaRepository,
9
10
  AuthRepository,
10
11
  UserData,
11
12
  CreateUserData,
@@ -16,6 +17,8 @@ import {
16
17
  UserIdentityData,
17
18
  ListUsersOptions,
18
19
  PaginatedUsersResult,
20
+ MfaFactor,
21
+ MfaChallengeInfo,
19
22
  RoleData as Role
20
23
  // @ts-ignore
21
24
  } from "@rebasepro/server-core";
@@ -25,8 +28,6 @@ export type { Role };
25
28
 
26
29
  export interface AuthSchemaTables {
27
30
  users: PgTable & Record<string, AnyPgColumn>;
28
- roles: PgTable & Record<string, AnyPgColumn>;
29
- userRoles: PgTable & Record<string, AnyPgColumn>;
30
31
  refreshTokens: PgTable & Record<string, AnyPgColumn>;
31
32
  passwordResetTokens: PgTable & Record<string, AnyPgColumn>;
32
33
  appConfig: PgTable & Record<string, AnyPgColumn>;
@@ -58,25 +59,19 @@ function getColumn(table: (PgTable & Record<string, AnyPgColumn>) | undefined, .
58
59
  export class UserService implements UserRepository {
59
60
  private usersTable: PgTable & Record<string, AnyPgColumn>;
60
61
  private userIdentitiesTable: PgTable & Record<string, AnyPgColumn>;
61
- private userRolesTable: PgTable & Record<string, AnyPgColumn>;
62
- private rolesTable: PgTable & Record<string, AnyPgColumn>;
63
62
 
64
63
  constructor(
65
64
  private db: NodePgDatabase,
66
65
  tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
67
66
  ) {
68
- if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users || (tableOrTables as Partial<AuthSchemaTables>).roles)) {
67
+ if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {
69
68
  const tables = tableOrTables as Partial<AuthSchemaTables>;
70
69
  this.usersTable = (tables.users || users) as unknown as PgTable & Record<string, AnyPgColumn>;
71
70
  this.userIdentitiesTable = (tables.userIdentities || userIdentities) as unknown as PgTable & Record<string, AnyPgColumn>;
72
- this.userRolesTable = (tables.userRoles || userRoles) as unknown as PgTable & Record<string, AnyPgColumn>;
73
- this.rolesTable = (tables.roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
74
71
  } else {
75
72
  const table = tableOrTables as (PgTable & Record<string, AnyPgColumn>) | undefined;
76
73
  this.usersTable = table || (users as unknown as PgTable & Record<string, AnyPgColumn>);
77
74
  this.userIdentitiesTable = userIdentities as unknown as PgTable & Record<string, AnyPgColumn>;
78
- this.userRolesTable = userRoles as unknown as PgTable & Record<string, AnyPgColumn>;
79
- this.rolesTable = roles as unknown as PgTable & Record<string, AnyPgColumn>;
80
75
  }
81
76
  }
82
77
 
@@ -97,6 +92,7 @@ export class UserService implements UserRepository {
97
92
  const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;
98
93
  const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;
99
94
  const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;
95
+ const isAnonymous = (row.is_anonymous ?? row.isAnonymous ?? false) as boolean;
100
96
  const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;
101
97
  const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;
102
98
 
@@ -110,6 +106,8 @@ export class UserService implements UserRepository {
110
106
  "email_verified", "emailVerified",
111
107
  "email_verification_token", "emailVerificationToken",
112
108
  "email_verification_sent_at", "emailVerificationSentAt",
109
+ "is_anonymous", "isAnonymous",
110
+ "roles",
113
111
  "created_at", "createdAt",
114
112
  "updated_at", "updatedAt",
115
113
  "metadata"
@@ -131,6 +129,7 @@ export class UserService implements UserRepository {
131
129
  emailVerified,
132
130
  emailVerificationToken,
133
131
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
132
+ isAnonymous,
134
133
  createdAt: createdAt ? new Date(createdAt) : new Date(),
135
134
  updatedAt: updatedAt ? new Date(updatedAt) : new Date(),
136
135
  metadata
@@ -150,6 +149,7 @@ export class UserService implements UserRepository {
150
149
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
151
150
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
152
151
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
152
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
153
153
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
154
154
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
155
155
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -162,6 +162,7 @@ export class UserService implements UserRepository {
162
162
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
163
163
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
164
164
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
165
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
165
166
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
166
167
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
167
168
 
@@ -179,6 +180,7 @@ export class UserService implements UserRepository {
179
180
  tableColKey !== emailVerifiedKey &&
180
181
  tableColKey !== emailVerificationTokenKey &&
181
182
  tableColKey !== emailVerificationSentAtKey &&
183
+ tableColKey !== isAnonymousKey &&
182
184
  tableColKey !== createdAtKey &&
183
185
  tableColKey !== updatedAtKey &&
184
186
  tableColKey !== metadataKey) {
@@ -306,11 +308,9 @@ export class UserService implements UserRepository {
306
308
  const idColumn = idCol ? idCol.name : "id";
307
309
 
308
310
  const usersTableName = this.getQualifiedUsersTableName();
309
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
310
-
311
311
  const conditions = [];
312
312
  if (roleId) {
313
- conditions.push(sql`EXISTS (SELECT 1 FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
313
+ conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
314
314
  }
315
315
  if (search) {
316
316
  const pattern = `%${search}%`;
@@ -322,7 +322,7 @@ export class UserService implements UserRepository {
322
322
  // Sorting: users with roles first if no role filter, then by requested order
323
323
  const orderByClause = roleId
324
324
  ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`
325
- : sql`ORDER BY (SELECT count(*) FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)}) DESC, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
325
+ : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
326
326
 
327
327
  const countResult = await this.db.execute(sql`
328
328
  SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
@@ -419,24 +419,25 @@ export class UserService implements UserRepository {
419
419
  }
420
420
 
421
421
  /**
422
- * Get roles for a user from database
422
+ * Get roles for a user from database (inline TEXT[] column)
423
423
  */
424
424
  async getUserRoles(userId: string): Promise<Role[]> {
425
- const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
425
+ const usersTableName = this.getQualifiedUsersTableName();
426
426
  const result = await this.db.execute(sql`
427
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
428
- FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
429
- INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
430
- WHERE ur.user_id = ${userId}
427
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
431
428
  `);
432
429
 
433
- return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null }>).map(row => ({
434
- id: row.id,
435
- name: row.name,
436
- isAdmin: row.is_admin,
437
- defaultPermissions: row.default_permissions,
438
- collectionPermissions: row.collection_permissions,
439
- config: row.config
430
+ if (result.rows.length === 0) return [];
431
+
432
+ const row = result.rows[0] as { roles: string[] | null };
433
+ const roleIds = row.roles ?? [];
434
+
435
+ return roleIds.map(id => ({
436
+ id,
437
+ name: id,
438
+ isAdmin: id === "admin",
439
+ defaultPermissions: null,
440
+ collectionPermissions: null
440
441
  }));
441
442
  }
442
443
 
@@ -444,37 +445,39 @@ export class UserService implements UserRepository {
444
445
  * Get role IDs for a user
445
446
  */
446
447
  async getUserRoleIds(userId: string): Promise<string[]> {
447
- const roles = await this.getUserRoles(userId);
448
- return roles.map(r => r.id);
448
+ const usersTableName = this.getQualifiedUsersTableName();
449
+ const result = await this.db.execute(sql`
450
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
451
+ `);
452
+
453
+ if (result.rows.length === 0) return [];
454
+
455
+ const row = result.rows[0] as { roles: string[] | null };
456
+ return row.roles ?? [];
449
457
  }
450
458
 
451
459
  /**
452
- * Set roles for a user
460
+ * Set roles for a user (replaces existing roles)
453
461
  */
454
462
  async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
455
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
456
- // Delete existing roles
457
- await this.db.execute(sql`DELETE FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
458
-
459
- // Insert new roles
460
- for (const roleId of roleIds) {
461
- await this.db.execute(sql`
462
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
463
- VALUES (${userId}, ${roleId})
464
- ON CONFLICT DO NOTHING
465
- `);
466
- }
463
+ const usersTableName = this.getQualifiedUsersTableName();
464
+ const rolesArray = `{${roleIds.join(",")}}`;
465
+ await this.db.execute(sql`
466
+ UPDATE ${sql.raw(usersTableName)}
467
+ SET roles = ${rolesArray}::text[], updated_at = NOW()
468
+ WHERE id = ${userId}
469
+ `);
467
470
  }
468
471
 
469
472
  /**
470
- * Assign a specific role to new user
473
+ * Assign a specific role to new user (appends if not present)
471
474
  */
472
475
  async assignDefaultRole(userId: string, roleId: string): Promise<void> {
473
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
476
+ const usersTableName = this.getQualifiedUsersTableName();
474
477
  await this.db.execute(sql`
475
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
476
- VALUES (${userId}, ${roleId})
477
- ON CONFLICT DO NOTHING
478
+ UPDATE ${sql.raw(usersTableName)}
479
+ SET roles = array_append(roles, ${roleId}), updated_at = NOW()
480
+ WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
478
481
  `);
479
482
  }
480
483
 
@@ -491,119 +494,7 @@ export class UserService implements UserRepository {
491
494
  }
492
495
  }
493
496
 
494
- /**
495
- * PostgreSQL implementation of RoleRepository.
496
- * Handles all role-related database operations using Drizzle ORM.
497
- */
498
- export class RoleService implements RoleRepository {
499
- private rolesTable: PgTable & Record<string, AnyPgColumn>;
500
497
 
501
- constructor(
502
- private db: NodePgDatabase,
503
- tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
504
- ) {
505
- if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).roles || (tableOrTables as Partial<AuthSchemaTables>).users)) {
506
- this.rolesTable = ((tableOrTables as Partial<AuthSchemaTables>).roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
507
- } else {
508
- this.rolesTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (roles as unknown as PgTable & Record<string, AnyPgColumn>);
509
- }
510
- }
511
-
512
- private getQualifiedRolesTableName(): string {
513
- const name = getTableName(this.rolesTable);
514
- const schema = getTableConfig(this.rolesTable).schema || "public";
515
- return `"${schema}"."${name}"`;
516
- }
517
-
518
- async getRoleById(id: string): Promise<Role | null> {
519
- const tableName = this.getQualifiedRolesTableName();
520
- const result = await this.db.execute(sql`
521
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
522
- FROM ${sql.raw(tableName)}
523
- WHERE id = ${id}
524
- `);
525
-
526
- if (result.rows.length === 0) return null;
527
-
528
- const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null };
529
- return {
530
- id: row.id,
531
- name: row.name,
532
- isAdmin: row.is_admin,
533
- defaultPermissions: row.default_permissions,
534
- collectionPermissions: row.collection_permissions,
535
- config: row.config
536
- };
537
- }
538
-
539
- async listRoles(): Promise<Role[]> {
540
- const tableName = this.getQualifiedRolesTableName();
541
- const result = await this.db.execute(sql`
542
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
543
- FROM ${sql.raw(tableName)}
544
- ORDER BY name
545
- `);
546
-
547
- return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null }>).map(row => ({
548
- id: row.id,
549
- name: row.name,
550
- isAdmin: row.is_admin,
551
- defaultPermissions: row.default_permissions,
552
- collectionPermissions: row.collection_permissions,
553
- config: row.config
554
- }));
555
- }
556
-
557
- async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
558
- const tableName = this.getQualifiedRolesTableName();
559
- const result = await this.db.execute(sql`
560
- INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
561
- VALUES (
562
- ${data.id},
563
- ${data.name},
564
- ${data.isAdmin ?? false},
565
- ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
566
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
567
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
568
- )
569
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
570
- `);
571
-
572
- const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null };
573
- return {
574
- id: row.id,
575
- name: row.name,
576
- isAdmin: row.is_admin,
577
- defaultPermissions: row.default_permissions,
578
- collectionPermissions: row.collection_permissions,
579
- config: row.config
580
- };
581
- }
582
-
583
- async updateRole(id: string, data: Partial<Omit<Role, "id">>): Promise<Role | null> {
584
- const existing = await this.getRoleById(id);
585
- if (!existing) return null;
586
-
587
- const tableName = this.getQualifiedRolesTableName();
588
- await this.db.execute(sql`
589
- UPDATE ${sql.raw(tableName)}
590
- SET
591
- name = ${data.name ?? existing.name},
592
- is_admin = ${data.isAdmin ?? existing.isAdmin},
593
- default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
594
- collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb,
595
- config = ${data.config ? JSON.stringify(data.config) : (existing.config ? JSON.stringify(existing.config) : null)}::jsonb
596
- WHERE id = ${id}
597
- `);
598
-
599
- return this.getRoleById(id);
600
- }
601
-
602
- async deleteRole(id: string): Promise<void> {
603
- const tableName = this.getQualifiedRolesTableName();
604
- await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
605
- }
606
- }
607
498
 
608
499
  export class RefreshTokenService {
609
500
  private refreshTokensTable: PgTable & Record<string, AnyPgColumn>;
@@ -875,7 +766,6 @@ export class PostgresTokenRepository implements TokenRepository {
875
766
  */
876
767
  export class PostgresAuthRepository implements AuthRepository {
877
768
  private userService: UserService;
878
- private roleService: RoleService;
879
769
  private tokenRepository: PostgresTokenRepository;
880
770
 
881
771
  constructor(
@@ -883,7 +773,6 @@ export class PostgresAuthRepository implements AuthRepository {
883
773
  tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
884
774
  ) {
885
775
  this.userService = new UserService(db, tableOrTables);
886
- this.roleService = new RoleService(db, tableOrTables);
887
776
  this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
888
777
  }
889
778
 
@@ -965,31 +854,48 @@ export class PostgresAuthRepository implements AuthRepository {
965
854
  return this.userService.getUserWithRoles(userId);
966
855
  }
967
856
 
968
- // Role operations (delegate to RoleService)
857
+ // Role operations (roles are inline on users, synthesized from string IDs)
969
858
 
970
859
  async getRoleById(id: string): Promise<RoleData | null> {
971
- return this.roleService.getRoleById(id);
860
+ return {
861
+ id,
862
+ name: id,
863
+ isAdmin: id === "admin",
864
+ defaultPermissions: null,
865
+ collectionPermissions: null
866
+ };
972
867
  }
973
868
 
974
869
  async listRoles(): Promise<RoleData[]> {
975
- return this.roleService.listRoles();
870
+ return [
871
+ { id: "admin", name: "Admin", isAdmin: true, defaultPermissions: null, collectionPermissions: null },
872
+ { id: "editor", name: "Editor", isAdmin: false, defaultPermissions: null, collectionPermissions: null },
873
+ { id: "viewer", name: "Viewer", isAdmin: false, defaultPermissions: null, collectionPermissions: null }
874
+ ];
976
875
  }
977
876
 
978
- async createRole(data: CreateRoleData): Promise<RoleData> {
979
- return this.roleService.createRole({
980
- ...data,
981
- defaultPermissions: data.defaultPermissions ?? null,
982
- collectionPermissions: data.collectionPermissions ?? null,
983
- config: data.config ?? null
984
- });
877
+ async createRole(_data: CreateRoleData): Promise<RoleData> {
878
+ return {
879
+ id: _data.id,
880
+ name: _data.name,
881
+ isAdmin: _data.isAdmin ?? false,
882
+ defaultPermissions: _data.defaultPermissions ?? null,
883
+ collectionPermissions: _data.collectionPermissions ?? null
884
+ };
985
885
  }
986
886
 
987
887
  async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
988
- return this.roleService.updateRole(id, data);
888
+ return {
889
+ id,
890
+ name: data.name ?? id,
891
+ isAdmin: data.isAdmin ?? (id === "admin"),
892
+ defaultPermissions: data.defaultPermissions ?? null,
893
+ collectionPermissions: data.collectionPermissions ?? null
894
+ };
989
895
  }
990
896
 
991
- async deleteRole(id: string): Promise<void> {
992
- await this.roleService.deleteRole(id);
897
+ async deleteRole(_id: string): Promise<void> {
898
+ // No-op: roles are inline strings on users
993
899
  }
994
900
 
995
901
  // Token operations (delegate to PostgresTokenRepository)
@@ -1037,6 +943,273 @@ export class PostgresAuthRepository implements AuthRepository {
1037
943
  async deleteExpiredTokens(): Promise<void> {
1038
944
  await this.tokenRepository.deleteExpiredTokens();
1039
945
  }
946
+
947
+ // MFA operations (delegate to MfaService)
948
+
949
+ private _mfaService: MfaService | null = null;
950
+ private getMfaService(): MfaService {
951
+ if (!this._mfaService) {
952
+ this._mfaService = new MfaService(this.db);
953
+ }
954
+ return this._mfaService;
955
+ }
956
+
957
+ async createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
958
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
959
+ }
960
+
961
+ async getMfaFactors(userId: string): Promise<MfaFactor[]> {
962
+ return this.getMfaService().getMfaFactors(userId);
963
+ }
964
+
965
+ async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
966
+ return this.getMfaService().getMfaFactorById(factorId);
967
+ }
968
+
969
+ async verifyMfaFactor(factorId: string): Promise<void> {
970
+ return this.getMfaService().verifyMfaFactor(factorId);
971
+ }
972
+
973
+ async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
974
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
975
+ }
976
+
977
+ async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
978
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
979
+ }
980
+
981
+ async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
982
+ return this.getMfaService().getMfaChallengeById(challengeId);
983
+ }
984
+
985
+ async verifyMfaChallenge(challengeId: string): Promise<void> {
986
+ return this.getMfaService().verifyMfaChallenge(challengeId);
987
+ }
988
+
989
+ async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
990
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
991
+ }
992
+
993
+ async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
994
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
995
+ }
996
+
997
+ async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
998
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
999
+ }
1000
+
1001
+ async deleteAllRecoveryCodes(userId: string): Promise<void> {
1002
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
1003
+ }
1004
+
1005
+ async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
1006
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
1007
+ }
1008
+ }
1009
+
1010
+ // =============================================================================
1011
+ // MFA SERVICE
1012
+ // =============================================================================
1013
+
1014
+ /**
1015
+ * PostgreSQL implementation of MfaRepository.
1016
+ * Handles all MFA-related database operations.
1017
+ */
1018
+ export class MfaService implements MfaRepository {
1019
+ constructor(private db: NodePgDatabase, private schemaName: string = "rebase") {}
1020
+
1021
+ private qualify(tableName: string): string {
1022
+ return `"${this.schemaName}"."${tableName}"`;
1023
+ }
1024
+
1025
+ async createMfaFactor(
1026
+ userId: string,
1027
+ factorType: "totp",
1028
+ secretEncrypted: string,
1029
+ friendlyName?: string
1030
+ ): Promise<MfaFactor> {
1031
+ const tableName = this.qualify("mfa_factors");
1032
+ const result = await this.db.execute(sql`
1033
+ INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
1034
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
1035
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
1036
+ `);
1037
+
1038
+ const row = result.rows[0] as Record<string, unknown>;
1039
+ return {
1040
+ id: row.id as string,
1041
+ userId: row.user_id as string,
1042
+ factorType: row.factor_type as "totp",
1043
+ friendlyName: (row.friendly_name as string | null) ?? undefined,
1044
+ verified: row.verified as boolean,
1045
+ createdAt: new Date(row.created_at as string),
1046
+ updatedAt: new Date(row.updated_at as string)
1047
+ };
1048
+ }
1049
+
1050
+ async getMfaFactors(userId: string): Promise<MfaFactor[]> {
1051
+ const tableName = this.qualify("mfa_factors");
1052
+ const result = await this.db.execute(sql`
1053
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
1054
+ FROM ${sql.raw(tableName)}
1055
+ WHERE user_id = ${userId}
1056
+ ORDER BY created_at
1057
+ `);
1058
+
1059
+ return (result.rows as Array<Record<string, unknown>>).map(row => ({
1060
+ id: row.id as string,
1061
+ userId: row.user_id as string,
1062
+ factorType: row.factor_type as "totp",
1063
+ friendlyName: (row.friendly_name as string | null) ?? undefined,
1064
+ verified: row.verified as boolean,
1065
+ createdAt: new Date(row.created_at as string),
1066
+ updatedAt: new Date(row.updated_at as string)
1067
+ }));
1068
+ }
1069
+
1070
+ async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
1071
+ const tableName = this.qualify("mfa_factors");
1072
+ const result = await this.db.execute(sql`
1073
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
1074
+ FROM ${sql.raw(tableName)}
1075
+ WHERE id = ${factorId}
1076
+ `);
1077
+
1078
+ if (result.rows.length === 0) return null;
1079
+
1080
+ const row = result.rows[0] as Record<string, unknown>;
1081
+ return {
1082
+ id: row.id as string,
1083
+ userId: row.user_id as string,
1084
+ factorType: row.factor_type as "totp",
1085
+ secretEncrypted: row.secret_encrypted as string,
1086
+ friendlyName: (row.friendly_name as string | null) ?? undefined,
1087
+ verified: row.verified as boolean,
1088
+ createdAt: new Date(row.created_at as string),
1089
+ updatedAt: new Date(row.updated_at as string)
1090
+ };
1091
+ }
1092
+
1093
+ async verifyMfaFactor(factorId: string): Promise<void> {
1094
+ const tableName = this.qualify("mfa_factors");
1095
+ await this.db.execute(sql`
1096
+ UPDATE ${sql.raw(tableName)}
1097
+ SET verified = TRUE, updated_at = NOW()
1098
+ WHERE id = ${factorId}
1099
+ `);
1100
+ }
1101
+
1102
+ async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
1103
+ const tableName = this.qualify("mfa_factors");
1104
+ await this.db.execute(sql`
1105
+ DELETE FROM ${sql.raw(tableName)}
1106
+ WHERE id = ${factorId} AND user_id = ${userId}
1107
+ `);
1108
+ }
1109
+
1110
+ async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
1111
+ const tableName = this.qualify("mfa_challenges");
1112
+ // Challenges expire in 5 minutes
1113
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
1114
+ const result = await this.db.execute(sql`
1115
+ INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
1116
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
1117
+ RETURNING id, factor_id, created_at, verified_at, ip_address
1118
+ `);
1119
+
1120
+ const row = result.rows[0] as Record<string, unknown>;
1121
+ return {
1122
+ id: row.id as string,
1123
+ factorId: row.factor_id as string,
1124
+ createdAt: new Date(row.created_at as string),
1125
+ verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
1126
+ ipAddress: (row.ip_address as string | null) ?? undefined
1127
+ };
1128
+ }
1129
+
1130
+ async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
1131
+ const tableName = this.qualify("mfa_challenges");
1132
+ const result = await this.db.execute(sql`
1133
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
1134
+ FROM ${sql.raw(tableName)}
1135
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
1136
+ `);
1137
+
1138
+ if (result.rows.length === 0) return null;
1139
+
1140
+ const row = result.rows[0] as Record<string, unknown>;
1141
+ return {
1142
+ id: row.id as string,
1143
+ factorId: row.factor_id as string,
1144
+ createdAt: new Date(row.created_at as string),
1145
+ verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
1146
+ ipAddress: (row.ip_address as string | null) ?? undefined
1147
+ };
1148
+ }
1149
+
1150
+ async verifyMfaChallenge(challengeId: string): Promise<void> {
1151
+ const tableName = this.qualify("mfa_challenges");
1152
+ await this.db.execute(sql`
1153
+ UPDATE ${sql.raw(tableName)}
1154
+ SET verified_at = NOW()
1155
+ WHERE id = ${challengeId}
1156
+ `);
1157
+ }
1158
+
1159
+ async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
1160
+ const tableName = this.qualify("recovery_codes");
1161
+ // Delete existing codes first
1162
+ await this.db.execute(sql`
1163
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
1164
+ `);
1165
+
1166
+ // Insert new codes
1167
+ for (const hash of codeHashes) {
1168
+ await this.db.execute(sql`
1169
+ INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
1170
+ VALUES (${userId}, ${hash})
1171
+ `);
1172
+ }
1173
+ }
1174
+
1175
+ async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
1176
+ const tableName = this.qualify("recovery_codes");
1177
+ const result = await this.db.execute(sql`
1178
+ UPDATE ${sql.raw(tableName)}
1179
+ SET used_at = NOW()
1180
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
1181
+ RETURNING id
1182
+ `);
1183
+
1184
+ return result.rows.length > 0;
1185
+ }
1186
+
1187
+ async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
1188
+ const tableName = this.qualify("recovery_codes");
1189
+ const result = await this.db.execute(sql`
1190
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
1191
+ WHERE user_id = ${userId} AND used_at IS NULL
1192
+ `);
1193
+
1194
+ return (result.rows[0] as { count: number }).count;
1195
+ }
1196
+
1197
+ async deleteAllRecoveryCodes(userId: string): Promise<void> {
1198
+ const tableName = this.qualify("recovery_codes");
1199
+ await this.db.execute(sql`
1200
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
1201
+ `);
1202
+ }
1203
+
1204
+ async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
1205
+ const tableName = this.qualify("mfa_factors");
1206
+ const result = await this.db.execute(sql`
1207
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
1208
+ WHERE user_id = ${userId} AND verified = TRUE
1209
+ `);
1210
+
1211
+ return (result.rows[0] as { count: number }).count > 0;
1212
+ }
1040
1213
  }
1041
1214
 
1042
1215
  // =============================================================================
@@ -1045,6 +1218,3 @@ export class PostgresAuthRepository implements AuthRepository {
1045
1218
 
1046
1219
  /** PostgreSQL user repository implementation */
1047
1220
  export type PostgresUserRepository = UserService;
1048
-
1049
- /** PostgreSQL role repository implementation */
1050
- export type PostgresRoleRepository = RoleService;