@rebasepro/server-postgresql 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +844 -160
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +842 -158
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +4 -2
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- package/vite.config.ts +1 -1
|
@@ -3,6 +3,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
3
3
|
import { getTableConfig, AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
4
4
|
import { getColumnMeta } from "../services/entity-helpers";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
|
+
import { logger } from "@rebasepro/server-core";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Default roles to seed on first run
|
|
@@ -12,22 +13,19 @@ const DEFAULT_ROLES = [
|
|
|
12
13
|
id: "admin",
|
|
13
14
|
name: "Admin",
|
|
14
15
|
is_admin: true,
|
|
15
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
16
|
-
config: { createCollections: true, editCollections: "all", deleteCollections: "all" }
|
|
16
|
+
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
id: "editor",
|
|
20
20
|
name: "Editor",
|
|
21
21
|
is_admin: false,
|
|
22
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
23
|
-
config: { createCollections: true, editCollections: "own", deleteCollections: "own" }
|
|
22
|
+
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
24
23
|
},
|
|
25
24
|
{
|
|
26
25
|
id: "viewer",
|
|
27
26
|
name: "Viewer",
|
|
28
27
|
is_admin: false,
|
|
29
|
-
default_permissions: { read: true, create: false, edit: false, delete: false }
|
|
30
|
-
config: null
|
|
28
|
+
default_permissions: { read: true, create: false, edit: false, delete: false }
|
|
31
29
|
}
|
|
32
30
|
];
|
|
33
31
|
|
|
@@ -36,7 +34,7 @@ const DEFAULT_ROLES = [
|
|
|
36
34
|
* This runs on startup to ensure the database is ready for auth
|
|
37
35
|
*/
|
|
38
36
|
export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: PostgresCollectionRegistry): Promise<void> {
|
|
39
|
-
|
|
37
|
+
logger.info("🔍 Checking auth tables...");
|
|
40
38
|
|
|
41
39
|
try {
|
|
42
40
|
// Resolve dynamic user table name and ID type
|
|
@@ -123,7 +121,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
123
121
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
124
122
|
default_permissions JSONB,
|
|
125
123
|
collection_permissions JSONB,
|
|
126
|
-
config JSONB,
|
|
127
124
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
128
125
|
)
|
|
129
126
|
`);
|
|
@@ -234,10 +231,76 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
234
231
|
// Seed default roles if none exist
|
|
235
232
|
await seedDefaultRoles(db, rolesTableName);
|
|
236
233
|
|
|
237
|
-
|
|
234
|
+
// ── Migration: Add is_anonymous column (safe for existing tables) ────
|
|
235
|
+
await db.execute(sql`
|
|
236
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
237
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
238
|
+
`);
|
|
239
|
+
|
|
240
|
+
// ── MFA tables ──────────────────────────────────────────────────────
|
|
241
|
+
const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
|
|
242
|
+
const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
|
|
243
|
+
const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
|
|
244
|
+
|
|
245
|
+
// Create mfa_factors table
|
|
246
|
+
await db.execute(sql`
|
|
247
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
|
|
248
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
249
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
250
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
251
|
+
secret_encrypted TEXT NOT NULL,
|
|
252
|
+
friendly_name TEXT,
|
|
253
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
254
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
255
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
256
|
+
)
|
|
257
|
+
`);
|
|
258
|
+
|
|
259
|
+
// Create indexes on mfa_factors
|
|
260
|
+
await db.execute(sql`
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
262
|
+
ON ${sql.raw(mfaFactorsTableName)}(user_id)
|
|
263
|
+
`);
|
|
264
|
+
|
|
265
|
+
// Create mfa_challenges table
|
|
266
|
+
await db.execute(sql`
|
|
267
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
|
|
268
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
269
|
+
factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
270
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
271
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
272
|
+
ip_address TEXT,
|
|
273
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
274
|
+
)
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
// Create indexes on mfa_challenges
|
|
278
|
+
await db.execute(sql`
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
280
|
+
ON ${sql.raw(mfaChallengesTableName)}(factor_id)
|
|
281
|
+
`);
|
|
282
|
+
|
|
283
|
+
// Create recovery_codes table
|
|
284
|
+
await db.execute(sql`
|
|
285
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
|
|
286
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
287
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
288
|
+
code_hash TEXT NOT NULL,
|
|
289
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
290
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
291
|
+
)
|
|
292
|
+
`);
|
|
293
|
+
|
|
294
|
+
// Create indexes on recovery_codes
|
|
295
|
+
await db.execute(sql`
|
|
296
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
297
|
+
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
298
|
+
`);
|
|
299
|
+
|
|
300
|
+
logger.info("✅ Auth tables ready");
|
|
238
301
|
} catch (error) {
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
logger.error("❌ Failed to create auth tables", { error });
|
|
303
|
+
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
241
304
|
}
|
|
242
305
|
}
|
|
243
306
|
|
|
@@ -250,25 +313,24 @@ async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Pro
|
|
|
250
313
|
const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
|
|
251
314
|
|
|
252
315
|
if (count > 0) {
|
|
253
|
-
|
|
316
|
+
logger.info(`📋 Found ${count} existing roles`);
|
|
254
317
|
return;
|
|
255
318
|
}
|
|
256
319
|
|
|
257
|
-
|
|
320
|
+
logger.info("🌱 Seeding default roles...");
|
|
258
321
|
|
|
259
322
|
for (const role of DEFAULT_ROLES) {
|
|
260
323
|
await db.execute(sql`
|
|
261
|
-
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions
|
|
324
|
+
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
262
325
|
VALUES (
|
|
263
326
|
${role.id},
|
|
264
327
|
${role.name},
|
|
265
328
|
${role.is_admin},
|
|
266
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
267
|
-
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
329
|
+
${JSON.stringify(role.default_permissions)}::jsonb
|
|
268
330
|
)
|
|
269
331
|
ON CONFLICT (id) DO NOTHING
|
|
270
332
|
`);
|
|
271
333
|
}
|
|
272
334
|
|
|
273
|
-
|
|
335
|
+
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
274
336
|
}
|
package/src/auth/services.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
@@ -97,6 +100,7 @@ export class UserService implements UserRepository {
|
|
|
97
100
|
const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;
|
|
98
101
|
const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;
|
|
99
102
|
const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;
|
|
103
|
+
const isAnonymous = (row.is_anonymous ?? row.isAnonymous ?? false) as boolean;
|
|
100
104
|
const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;
|
|
101
105
|
const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;
|
|
102
106
|
|
|
@@ -110,6 +114,7 @@ export class UserService implements UserRepository {
|
|
|
110
114
|
"email_verified", "emailVerified",
|
|
111
115
|
"email_verification_token", "emailVerificationToken",
|
|
112
116
|
"email_verification_sent_at", "emailVerificationSentAt",
|
|
117
|
+
"is_anonymous", "isAnonymous",
|
|
113
118
|
"created_at", "createdAt",
|
|
114
119
|
"updated_at", "updatedAt",
|
|
115
120
|
"metadata"
|
|
@@ -131,6 +136,7 @@ export class UserService implements UserRepository {
|
|
|
131
136
|
emailVerified,
|
|
132
137
|
emailVerificationToken,
|
|
133
138
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
139
|
+
isAnonymous,
|
|
134
140
|
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
|
135
141
|
updatedAt: updatedAt ? new Date(updatedAt) : new Date(),
|
|
136
142
|
metadata
|
|
@@ -150,6 +156,7 @@ export class UserService implements UserRepository {
|
|
|
150
156
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
151
157
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
152
158
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
159
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
153
160
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
154
161
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
155
162
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -162,6 +169,7 @@ export class UserService implements UserRepository {
|
|
|
162
169
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
163
170
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
164
171
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
172
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
165
173
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
166
174
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
167
175
|
|
|
@@ -179,6 +187,7 @@ export class UserService implements UserRepository {
|
|
|
179
187
|
tableColKey !== emailVerifiedKey &&
|
|
180
188
|
tableColKey !== emailVerificationTokenKey &&
|
|
181
189
|
tableColKey !== emailVerificationSentAtKey &&
|
|
190
|
+
tableColKey !== isAnonymousKey &&
|
|
182
191
|
tableColKey !== createdAtKey &&
|
|
183
192
|
tableColKey !== updatedAtKey &&
|
|
184
193
|
tableColKey !== metadataKey) {
|
|
@@ -424,19 +433,18 @@ export class UserService implements UserRepository {
|
|
|
424
433
|
async getUserRoles(userId: string): Promise<Role[]> {
|
|
425
434
|
const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
|
|
426
435
|
const result = await this.db.execute(sql`
|
|
427
|
-
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
436
|
+
SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
|
|
428
437
|
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
429
438
|
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
430
439
|
WHERE ur.user_id = ${userId}
|
|
431
440
|
`);
|
|
432
441
|
|
|
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
|
|
442
|
+
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 }>).map(row => ({
|
|
434
443
|
id: row.id,
|
|
435
444
|
name: row.name,
|
|
436
445
|
isAdmin: row.is_admin,
|
|
437
446
|
defaultPermissions: row.default_permissions,
|
|
438
|
-
collectionPermissions: row.collection_permissions
|
|
439
|
-
config: row.config
|
|
447
|
+
collectionPermissions: row.collection_permissions
|
|
440
448
|
}));
|
|
441
449
|
}
|
|
442
450
|
|
|
@@ -518,65 +526,61 @@ export class RoleService implements RoleRepository {
|
|
|
518
526
|
async getRoleById(id: string): Promise<Role | null> {
|
|
519
527
|
const tableName = this.getQualifiedRolesTableName();
|
|
520
528
|
const result = await this.db.execute(sql`
|
|
521
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
529
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
522
530
|
FROM ${sql.raw(tableName)}
|
|
523
531
|
WHERE id = ${id}
|
|
524
532
|
`);
|
|
525
533
|
|
|
526
534
|
if (result.rows.length === 0) return null;
|
|
527
535
|
|
|
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
|
|
536
|
+
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 };
|
|
529
537
|
return {
|
|
530
538
|
id: row.id,
|
|
531
539
|
name: row.name,
|
|
532
540
|
isAdmin: row.is_admin,
|
|
533
541
|
defaultPermissions: row.default_permissions,
|
|
534
|
-
collectionPermissions: row.collection_permissions
|
|
535
|
-
config: row.config
|
|
542
|
+
collectionPermissions: row.collection_permissions
|
|
536
543
|
};
|
|
537
544
|
}
|
|
538
545
|
|
|
539
546
|
async listRoles(): Promise<Role[]> {
|
|
540
547
|
const tableName = this.getQualifiedRolesTableName();
|
|
541
548
|
const result = await this.db.execute(sql`
|
|
542
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
549
|
+
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
543
550
|
FROM ${sql.raw(tableName)}
|
|
544
551
|
ORDER BY name
|
|
545
552
|
`);
|
|
546
553
|
|
|
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
|
|
554
|
+
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 }>).map(row => ({
|
|
548
555
|
id: row.id,
|
|
549
556
|
name: row.name,
|
|
550
557
|
isAdmin: row.is_admin,
|
|
551
558
|
defaultPermissions: row.default_permissions,
|
|
552
|
-
collectionPermissions: row.collection_permissions
|
|
553
|
-
config: row.config
|
|
559
|
+
collectionPermissions: row.collection_permissions
|
|
554
560
|
}));
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
|
|
558
564
|
const tableName = this.getQualifiedRolesTableName();
|
|
559
565
|
const result = await this.db.execute(sql`
|
|
560
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions
|
|
566
|
+
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
561
567
|
VALUES (
|
|
562
568
|
${data.id},
|
|
563
569
|
${data.name},
|
|
564
570
|
${data.isAdmin ?? false},
|
|
565
571
|
${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
|
|
572
|
+
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
568
573
|
)
|
|
569
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
574
|
+
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
570
575
|
`);
|
|
571
576
|
|
|
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
|
|
577
|
+
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 };
|
|
573
578
|
return {
|
|
574
579
|
id: row.id,
|
|
575
580
|
name: row.name,
|
|
576
581
|
isAdmin: row.is_admin,
|
|
577
582
|
defaultPermissions: row.default_permissions,
|
|
578
|
-
collectionPermissions: row.collection_permissions
|
|
579
|
-
config: row.config
|
|
583
|
+
collectionPermissions: row.collection_permissions
|
|
580
584
|
};
|
|
581
585
|
}
|
|
582
586
|
|
|
@@ -591,8 +595,7 @@ export class RoleService implements RoleRepository {
|
|
|
591
595
|
name = ${data.name ?? existing.name},
|
|
592
596
|
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
593
597
|
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
|
|
598
|
+
collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb
|
|
596
599
|
WHERE id = ${id}
|
|
597
600
|
`);
|
|
598
601
|
|
|
@@ -979,8 +982,7 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
979
982
|
return this.roleService.createRole({
|
|
980
983
|
...data,
|
|
981
984
|
defaultPermissions: data.defaultPermissions ?? null,
|
|
982
|
-
collectionPermissions: data.collectionPermissions ?? null
|
|
983
|
-
config: data.config ?? null
|
|
985
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
984
986
|
});
|
|
985
987
|
}
|
|
986
988
|
|
|
@@ -1037,6 +1039,273 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
1037
1039
|
async deleteExpiredTokens(): Promise<void> {
|
|
1038
1040
|
await this.tokenRepository.deleteExpiredTokens();
|
|
1039
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
// MFA operations (delegate to MfaService)
|
|
1044
|
+
|
|
1045
|
+
private _mfaService: MfaService | null = null;
|
|
1046
|
+
private getMfaService(): MfaService {
|
|
1047
|
+
if (!this._mfaService) {
|
|
1048
|
+
this._mfaService = new MfaService(this.db);
|
|
1049
|
+
}
|
|
1050
|
+
return this._mfaService;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
|
|
1054
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async getMfaFactors(userId: string): Promise<MfaFactor[]> {
|
|
1058
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1062
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async verifyMfaFactor(factorId: string): Promise<void> {
|
|
1066
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
|
|
1070
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
1074
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
|
|
1078
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async verifyMfaChallenge(challengeId: string): Promise<void> {
|
|
1082
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
|
|
1086
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
|
|
1090
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
|
|
1094
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async deleteAllRecoveryCodes(userId: string): Promise<void> {
|
|
1098
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
|
|
1102
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// =============================================================================
|
|
1107
|
+
// MFA SERVICE
|
|
1108
|
+
// =============================================================================
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* PostgreSQL implementation of MfaRepository.
|
|
1112
|
+
* Handles all MFA-related database operations.
|
|
1113
|
+
*/
|
|
1114
|
+
export class MfaService implements MfaRepository {
|
|
1115
|
+
constructor(private db: NodePgDatabase, private schemaName: string = "rebase") {}
|
|
1116
|
+
|
|
1117
|
+
private qualify(tableName: string): string {
|
|
1118
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async createMfaFactor(
|
|
1122
|
+
userId: string,
|
|
1123
|
+
factorType: "totp",
|
|
1124
|
+
secretEncrypted: string,
|
|
1125
|
+
friendlyName?: string
|
|
1126
|
+
): Promise<MfaFactor> {
|
|
1127
|
+
const tableName = this.qualify("mfa_factors");
|
|
1128
|
+
const result = await this.db.execute(sql`
|
|
1129
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
1130
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
1131
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
1132
|
+
`);
|
|
1133
|
+
|
|
1134
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1135
|
+
return {
|
|
1136
|
+
id: row.id as string,
|
|
1137
|
+
userId: row.user_id as string,
|
|
1138
|
+
factorType: row.factor_type as "totp",
|
|
1139
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1140
|
+
verified: row.verified as boolean,
|
|
1141
|
+
createdAt: new Date(row.created_at as string),
|
|
1142
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async getMfaFactors(userId: string): Promise<MfaFactor[]> {
|
|
1147
|
+
const tableName = this.qualify("mfa_factors");
|
|
1148
|
+
const result = await this.db.execute(sql`
|
|
1149
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
1150
|
+
FROM ${sql.raw(tableName)}
|
|
1151
|
+
WHERE user_id = ${userId}
|
|
1152
|
+
ORDER BY created_at
|
|
1153
|
+
`);
|
|
1154
|
+
|
|
1155
|
+
return (result.rows as Array<Record<string, unknown>>).map(row => ({
|
|
1156
|
+
id: row.id as string,
|
|
1157
|
+
userId: row.user_id as string,
|
|
1158
|
+
factorType: row.factor_type as "totp",
|
|
1159
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1160
|
+
verified: row.verified as boolean,
|
|
1161
|
+
createdAt: new Date(row.created_at as string),
|
|
1162
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1163
|
+
}));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {
|
|
1167
|
+
const tableName = this.qualify("mfa_factors");
|
|
1168
|
+
const result = await this.db.execute(sql`
|
|
1169
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
1170
|
+
FROM ${sql.raw(tableName)}
|
|
1171
|
+
WHERE id = ${factorId}
|
|
1172
|
+
`);
|
|
1173
|
+
|
|
1174
|
+
if (result.rows.length === 0) return null;
|
|
1175
|
+
|
|
1176
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1177
|
+
return {
|
|
1178
|
+
id: row.id as string,
|
|
1179
|
+
userId: row.user_id as string,
|
|
1180
|
+
factorType: row.factor_type as "totp",
|
|
1181
|
+
secretEncrypted: row.secret_encrypted as string,
|
|
1182
|
+
friendlyName: (row.friendly_name as string | null) ?? undefined,
|
|
1183
|
+
verified: row.verified as boolean,
|
|
1184
|
+
createdAt: new Date(row.created_at as string),
|
|
1185
|
+
updatedAt: new Date(row.updated_at as string)
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async verifyMfaFactor(factorId: string): Promise<void> {
|
|
1190
|
+
const tableName = this.qualify("mfa_factors");
|
|
1191
|
+
await this.db.execute(sql`
|
|
1192
|
+
UPDATE ${sql.raw(tableName)}
|
|
1193
|
+
SET verified = TRUE, updated_at = NOW()
|
|
1194
|
+
WHERE id = ${factorId}
|
|
1195
|
+
`);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async deleteMfaFactor(factorId: string, userId: string): Promise<void> {
|
|
1199
|
+
const tableName = this.qualify("mfa_factors");
|
|
1200
|
+
await this.db.execute(sql`
|
|
1201
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
1202
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
1203
|
+
`);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
|
|
1207
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1208
|
+
// Challenges expire in 5 minutes
|
|
1209
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
|
1210
|
+
const result = await this.db.execute(sql`
|
|
1211
|
+
INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
1212
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
1213
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
1214
|
+
`);
|
|
1215
|
+
|
|
1216
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1217
|
+
return {
|
|
1218
|
+
id: row.id as string,
|
|
1219
|
+
factorId: row.factor_id as string,
|
|
1220
|
+
createdAt: new Date(row.created_at as string),
|
|
1221
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
|
|
1222
|
+
ipAddress: (row.ip_address as string | null) ?? undefined
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
|
|
1227
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1228
|
+
const result = await this.db.execute(sql`
|
|
1229
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
1230
|
+
FROM ${sql.raw(tableName)}
|
|
1231
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
1232
|
+
`);
|
|
1233
|
+
|
|
1234
|
+
if (result.rows.length === 0) return null;
|
|
1235
|
+
|
|
1236
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
1237
|
+
return {
|
|
1238
|
+
id: row.id as string,
|
|
1239
|
+
factorId: row.factor_id as string,
|
|
1240
|
+
createdAt: new Date(row.created_at as string),
|
|
1241
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,
|
|
1242
|
+
ipAddress: (row.ip_address as string | null) ?? undefined
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async verifyMfaChallenge(challengeId: string): Promise<void> {
|
|
1247
|
+
const tableName = this.qualify("mfa_challenges");
|
|
1248
|
+
await this.db.execute(sql`
|
|
1249
|
+
UPDATE ${sql.raw(tableName)}
|
|
1250
|
+
SET verified_at = NOW()
|
|
1251
|
+
WHERE id = ${challengeId}
|
|
1252
|
+
`);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
async createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void> {
|
|
1256
|
+
const tableName = this.qualify("recovery_codes");
|
|
1257
|
+
// Delete existing codes first
|
|
1258
|
+
await this.db.execute(sql`
|
|
1259
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
1260
|
+
`);
|
|
1261
|
+
|
|
1262
|
+
// Insert new codes
|
|
1263
|
+
for (const hash of codeHashes) {
|
|
1264
|
+
await this.db.execute(sql`
|
|
1265
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
|
|
1266
|
+
VALUES (${userId}, ${hash})
|
|
1267
|
+
`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
async useRecoveryCode(userId: string, codeHash: string): Promise<boolean> {
|
|
1272
|
+
const tableName = this.qualify("recovery_codes");
|
|
1273
|
+
const result = await this.db.execute(sql`
|
|
1274
|
+
UPDATE ${sql.raw(tableName)}
|
|
1275
|
+
SET used_at = NOW()
|
|
1276
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
1277
|
+
RETURNING id
|
|
1278
|
+
`);
|
|
1279
|
+
|
|
1280
|
+
return result.rows.length > 0;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async getUnusedRecoveryCodeCount(userId: string): Promise<number> {
|
|
1284
|
+
const tableName = this.qualify("recovery_codes");
|
|
1285
|
+
const result = await this.db.execute(sql`
|
|
1286
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1287
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
1288
|
+
`);
|
|
1289
|
+
|
|
1290
|
+
return (result.rows[0] as { count: number }).count;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async deleteAllRecoveryCodes(userId: string): Promise<void> {
|
|
1294
|
+
const tableName = this.qualify("recovery_codes");
|
|
1295
|
+
await this.db.execute(sql`
|
|
1296
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
1297
|
+
`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async hasVerifiedMfaFactors(userId: string): Promise<boolean> {
|
|
1301
|
+
const tableName = this.qualify("mfa_factors");
|
|
1302
|
+
const result = await this.db.execute(sql`
|
|
1303
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
1304
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
1305
|
+
`);
|
|
1306
|
+
|
|
1307
|
+
return (result.rows[0] as { count: number }).count > 0;
|
|
1308
|
+
}
|
|
1040
1309
|
}
|
|
1041
1310
|
|
|
1042
1311
|
// =============================================================================
|