@rebasepro/server-postgresql 0.2.4 → 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.
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/index.es.js +286 -365
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +283 -362
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
- package/dist/server-postgresql/src/auth/services.d.ts +6 -31
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/properties.d.ts +7 -5
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +10 -0
- package/src/PostgresBootstrapper.ts +3 -12
- package/src/auth/ensure-tables.ts +52 -101
- package/src/auth/services.ts +71 -170
- package/src/schema/auth-schema.ts +13 -69
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +7 -0
- package/test/auth-services.test.ts +7 -150
- package/test/doctor.test.ts +6 -2
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -14
- package/src/schema/default-collections.ts +0 -69
|
@@ -5,29 +5,7 @@ import { getColumnMeta } from "../services/entity-helpers";
|
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
import { logger } from "@rebasepro/server-core";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
* Default roles to seed on first run
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_ROLES = [
|
|
12
|
-
{
|
|
13
|
-
id: "admin",
|
|
14
|
-
name: "Admin",
|
|
15
|
-
is_admin: true,
|
|
16
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: "editor",
|
|
20
|
-
name: "Editor",
|
|
21
|
-
is_admin: false,
|
|
22
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: "viewer",
|
|
26
|
-
name: "Viewer",
|
|
27
|
-
is_admin: false,
|
|
28
|
-
default_permissions: { read: true, create: false, edit: false, delete: false }
|
|
29
|
-
}
|
|
30
|
-
];
|
|
8
|
+
|
|
31
9
|
|
|
32
10
|
/**
|
|
33
11
|
* Auto-create auth tables if they don't exist
|
|
@@ -64,31 +42,19 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
64
42
|
}
|
|
65
43
|
}
|
|
66
44
|
|
|
67
|
-
|
|
68
|
-
let rolesSchema = "rebase";
|
|
69
|
-
if (registry) {
|
|
70
|
-
const rolesTable = registry.getTable("roles");
|
|
71
|
-
if (rolesTable) {
|
|
72
|
-
rolesSchema = getTableConfig(rolesTable).schema || "public";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
45
|
+
|
|
75
46
|
|
|
76
47
|
// ── Create schemas (idempotent) ──────────────────────────────────
|
|
77
48
|
if (usersSchema !== "public") {
|
|
78
49
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
|
|
79
50
|
}
|
|
80
|
-
if (rolesSchema !== "public" && rolesSchema !== usersSchema) {
|
|
81
|
-
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
|
|
82
|
-
}
|
|
83
51
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
const userIdentitiesTable = `"${
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
|
|
91
|
-
const appConfigTableName = `"${rolesSchema}"."app_config"`;
|
|
53
|
+
const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
|
|
54
|
+
const userIdentitiesTable = `"${authSchema}"."user_identities"`;
|
|
55
|
+
const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
|
|
56
|
+
const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
|
|
57
|
+
const appConfigTableName = `"${authSchema}"."app_config"`;
|
|
92
58
|
|
|
93
59
|
// ── Create tables (idempotent) ──────────────────────────────────
|
|
94
60
|
|
|
@@ -113,32 +79,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
113
79
|
`);
|
|
114
80
|
|
|
115
81
|
|
|
116
|
-
// Create roles table
|
|
117
|
-
await db.execute(sql`
|
|
118
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
|
|
119
|
-
id TEXT PRIMARY KEY,
|
|
120
|
-
name TEXT NOT NULL,
|
|
121
|
-
is_admin BOOLEAN DEFAULT FALSE,
|
|
122
|
-
default_permissions JSONB,
|
|
123
|
-
collection_permissions JSONB,
|
|
124
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
125
|
-
)
|
|
126
|
-
`);
|
|
127
|
-
|
|
128
|
-
// Create user_roles junction table
|
|
129
|
-
await db.execute(sql`
|
|
130
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
|
|
131
|
-
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
132
|
-
role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
|
|
133
|
-
PRIMARY KEY (user_id, role_id)
|
|
134
|
-
)
|
|
135
|
-
`);
|
|
136
|
-
|
|
137
|
-
// Create index on user_id for faster lookups
|
|
138
|
-
await db.execute(sql`
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_user_roles_user
|
|
140
|
-
ON ${sql.raw(userRolesTableName)}(user_id)
|
|
141
|
-
`);
|
|
142
82
|
|
|
143
83
|
// Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
|
|
144
84
|
await db.execute(sql`
|
|
@@ -229,7 +169,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
229
169
|
});
|
|
230
170
|
|
|
231
171
|
// Seed default roles if none exist
|
|
232
|
-
|
|
172
|
+
// (no-op: roles are now stored inline on the users table)
|
|
233
173
|
|
|
234
174
|
// ── Migration: Add is_anonymous column (safe for existing tables) ────
|
|
235
175
|
await db.execute(sql`
|
|
@@ -237,10 +177,51 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
237
177
|
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
238
178
|
`);
|
|
239
179
|
|
|
180
|
+
// ── Migration: Add inline roles column (safe for existing tables) ────
|
|
181
|
+
await db.execute(sql`
|
|
182
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
183
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
184
|
+
`);
|
|
185
|
+
|
|
186
|
+
// ── Migration: Copy roles from legacy junction table to inline column ──
|
|
187
|
+
// If the old rebase.user_roles and rebase.roles tables exist, migrate
|
|
188
|
+
// the data into the new TEXT[] column then drop the legacy tables.
|
|
189
|
+
try {
|
|
190
|
+
const legacyCheck = await db.execute(sql`
|
|
191
|
+
SELECT EXISTS (
|
|
192
|
+
SELECT 1 FROM information_schema.tables
|
|
193
|
+
WHERE table_schema = 'rebase' AND table_name = 'user_roles'
|
|
194
|
+
) AS has_user_roles
|
|
195
|
+
`);
|
|
196
|
+
const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;
|
|
197
|
+
|
|
198
|
+
if (hasLegacyTables) {
|
|
199
|
+
logger.info("🔄 Migrating roles from legacy user_roles table...");
|
|
200
|
+
// Update users' roles column from the junction table
|
|
201
|
+
await db.execute(sql`
|
|
202
|
+
UPDATE ${sql.raw(usersTableName)} u
|
|
203
|
+
SET roles = COALESCE((
|
|
204
|
+
SELECT array_agg(ur.role_id)
|
|
205
|
+
FROM "rebase"."user_roles" ur
|
|
206
|
+
WHERE ur.user_id = u.id
|
|
207
|
+
), '{}')
|
|
208
|
+
WHERE u.roles = '{}' OR u.roles IS NULL
|
|
209
|
+
`);
|
|
210
|
+
|
|
211
|
+
// Drop legacy tables (junction first due to FK)
|
|
212
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
|
|
213
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
|
|
214
|
+
logger.info("✅ Legacy roles tables migrated and dropped");
|
|
215
|
+
}
|
|
216
|
+
} catch (migrationError: unknown) {
|
|
217
|
+
// Non-fatal: log and continue — the column exists and will work
|
|
218
|
+
logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
240
221
|
// ── MFA tables ──────────────────────────────────────────────────────
|
|
241
|
-
const mfaFactorsTableName = `"${
|
|
242
|
-
const mfaChallengesTableName = `"${
|
|
243
|
-
const recoveryCodesTableName = `"${
|
|
222
|
+
const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
|
|
223
|
+
const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
|
|
224
|
+
const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
|
|
244
225
|
|
|
245
226
|
// Create mfa_factors table
|
|
246
227
|
await db.execute(sql`
|
|
@@ -304,33 +285,3 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
304
285
|
}
|
|
305
286
|
}
|
|
306
287
|
|
|
307
|
-
/**
|
|
308
|
-
* Seed default roles if the roles table is empty
|
|
309
|
-
*/
|
|
310
|
-
async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Promise<void> {
|
|
311
|
-
// Check if any roles exist
|
|
312
|
-
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
313
|
-
const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
|
|
314
|
-
|
|
315
|
-
if (count > 0) {
|
|
316
|
-
logger.info(`📋 Found ${count} existing roles`);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
logger.info("🌱 Seeding default roles...");
|
|
321
|
-
|
|
322
|
-
for (const role of DEFAULT_ROLES) {
|
|
323
|
-
await db.execute(sql`
|
|
324
|
-
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
325
|
-
VALUES (
|
|
326
|
-
${role.id},
|
|
327
|
-
${role.name},
|
|
328
|
-
${role.is_admin},
|
|
329
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
330
|
-
)
|
|
331
|
-
ON CONFLICT (id) DO NOTHING
|
|
332
|
-
`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
336
|
-
}
|
package/src/auth/services.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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,
|
|
4
|
+
import { users, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
|
|
5
5
|
import {
|
|
6
6
|
UserRepository,
|
|
7
7
|
RoleRepository,
|
|
@@ -28,8 +28,6 @@ export type { Role };
|
|
|
28
28
|
|
|
29
29
|
export interface AuthSchemaTables {
|
|
30
30
|
users: PgTable & Record<string, AnyPgColumn>;
|
|
31
|
-
roles: PgTable & Record<string, AnyPgColumn>;
|
|
32
|
-
userRoles: PgTable & Record<string, AnyPgColumn>;
|
|
33
31
|
refreshTokens: PgTable & Record<string, AnyPgColumn>;
|
|
34
32
|
passwordResetTokens: PgTable & Record<string, AnyPgColumn>;
|
|
35
33
|
appConfig: PgTable & Record<string, AnyPgColumn>;
|
|
@@ -61,25 +59,19 @@ function getColumn(table: (PgTable & Record<string, AnyPgColumn>) | undefined, .
|
|
|
61
59
|
export class UserService implements UserRepository {
|
|
62
60
|
private usersTable: PgTable & Record<string, AnyPgColumn>;
|
|
63
61
|
private userIdentitiesTable: PgTable & Record<string, AnyPgColumn>;
|
|
64
|
-
private userRolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
65
|
-
private rolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
66
62
|
|
|
67
63
|
constructor(
|
|
68
64
|
private db: NodePgDatabase,
|
|
69
65
|
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
70
66
|
) {
|
|
71
|
-
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users
|
|
67
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
72
68
|
const tables = tableOrTables as Partial<AuthSchemaTables>;
|
|
73
69
|
this.usersTable = (tables.users || users) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
74
70
|
this.userIdentitiesTable = (tables.userIdentities || userIdentities) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
75
|
-
this.userRolesTable = (tables.userRoles || userRoles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
76
|
-
this.rolesTable = (tables.roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
77
71
|
} else {
|
|
78
72
|
const table = tableOrTables as (PgTable & Record<string, AnyPgColumn>) | undefined;
|
|
79
73
|
this.usersTable = table || (users as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
80
74
|
this.userIdentitiesTable = userIdentities as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
81
|
-
this.userRolesTable = userRoles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
82
|
-
this.rolesTable = roles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
83
75
|
}
|
|
84
76
|
}
|
|
85
77
|
|
|
@@ -115,6 +107,7 @@ export class UserService implements UserRepository {
|
|
|
115
107
|
"email_verification_token", "emailVerificationToken",
|
|
116
108
|
"email_verification_sent_at", "emailVerificationSentAt",
|
|
117
109
|
"is_anonymous", "isAnonymous",
|
|
110
|
+
"roles",
|
|
118
111
|
"created_at", "createdAt",
|
|
119
112
|
"updated_at", "updatedAt",
|
|
120
113
|
"metadata"
|
|
@@ -315,11 +308,9 @@ export class UserService implements UserRepository {
|
|
|
315
308
|
const idColumn = idCol ? idCol.name : "id";
|
|
316
309
|
|
|
317
310
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
318
|
-
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
319
|
-
|
|
320
311
|
const conditions = [];
|
|
321
312
|
if (roleId) {
|
|
322
|
-
conditions.push(sql
|
|
313
|
+
conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
|
|
323
314
|
}
|
|
324
315
|
if (search) {
|
|
325
316
|
const pattern = `%${search}%`;
|
|
@@ -331,7 +322,7 @@ export class UserService implements UserRepository {
|
|
|
331
322
|
// Sorting: users with roles first if no role filter, then by requested order
|
|
332
323
|
const orderByClause = roleId
|
|
333
324
|
? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`
|
|
334
|
-
: sql`ORDER BY (
|
|
325
|
+
: sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
|
|
335
326
|
|
|
336
327
|
const countResult = await this.db.execute(sql`
|
|
337
328
|
SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
|
|
@@ -428,23 +419,25 @@ export class UserService implements UserRepository {
|
|
|
428
419
|
}
|
|
429
420
|
|
|
430
421
|
/**
|
|
431
|
-
* Get roles for a user from database
|
|
422
|
+
* Get roles for a user from database (inline TEXT[] column)
|
|
432
423
|
*/
|
|
433
424
|
async getUserRoles(userId: string): Promise<Role[]> {
|
|
434
|
-
const
|
|
425
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
435
426
|
const result = await this.db.execute(sql`
|
|
436
|
-
SELECT
|
|
437
|
-
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
438
|
-
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
439
|
-
WHERE ur.user_id = ${userId}
|
|
427
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
|
|
440
428
|
`);
|
|
441
429
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
448
441
|
}));
|
|
449
442
|
}
|
|
450
443
|
|
|
@@ -452,37 +445,39 @@ export class UserService implements UserRepository {
|
|
|
452
445
|
* Get role IDs for a user
|
|
453
446
|
*/
|
|
454
447
|
async getUserRoleIds(userId: string): Promise<string[]> {
|
|
455
|
-
const
|
|
456
|
-
|
|
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 ?? [];
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
/**
|
|
460
|
-
* Set roles for a user
|
|
460
|
+
* Set roles for a user (replaces existing roles)
|
|
461
461
|
*/
|
|
462
462
|
async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
await this.db.execute(sql`
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
|
|
471
|
-
VALUES (${userId}, ${roleId})
|
|
472
|
-
ON CONFLICT DO NOTHING
|
|
473
|
-
`);
|
|
474
|
-
}
|
|
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
|
+
`);
|
|
475
470
|
}
|
|
476
471
|
|
|
477
472
|
/**
|
|
478
|
-
* Assign a specific role to new user
|
|
473
|
+
* Assign a specific role to new user (appends if not present)
|
|
479
474
|
*/
|
|
480
475
|
async assignDefaultRole(userId: string, roleId: string): Promise<void> {
|
|
481
|
-
const
|
|
476
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
482
477
|
await this.db.execute(sql`
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
478
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
479
|
+
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
480
|
+
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
486
481
|
`);
|
|
487
482
|
}
|
|
488
483
|
|
|
@@ -499,114 +494,7 @@ export class UserService implements UserRepository {
|
|
|
499
494
|
}
|
|
500
495
|
}
|
|
501
496
|
|
|
502
|
-
/**
|
|
503
|
-
* PostgreSQL implementation of RoleRepository.
|
|
504
|
-
* Handles all role-related database operations using Drizzle ORM.
|
|
505
|
-
*/
|
|
506
|
-
export class RoleService implements RoleRepository {
|
|
507
|
-
private rolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
508
|
-
|
|
509
|
-
constructor(
|
|
510
|
-
private db: NodePgDatabase,
|
|
511
|
-
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
512
|
-
) {
|
|
513
|
-
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).roles || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
514
|
-
this.rolesTable = ((tableOrTables as Partial<AuthSchemaTables>).roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
515
|
-
} else {
|
|
516
|
-
this.rolesTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (roles as unknown as PgTable & Record<string, AnyPgColumn>);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private getQualifiedRolesTableName(): string {
|
|
521
|
-
const name = getTableName(this.rolesTable);
|
|
522
|
-
const schema = getTableConfig(this.rolesTable).schema || "public";
|
|
523
|
-
return `"${schema}"."${name}"`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async getRoleById(id: string): Promise<Role | null> {
|
|
527
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
528
|
-
const result = await this.db.execute(sql`
|
|
529
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
530
|
-
FROM ${sql.raw(tableName)}
|
|
531
|
-
WHERE id = ${id}
|
|
532
|
-
`);
|
|
533
|
-
|
|
534
|
-
if (result.rows.length === 0) return null;
|
|
535
|
-
|
|
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 };
|
|
537
|
-
return {
|
|
538
|
-
id: row.id,
|
|
539
|
-
name: row.name,
|
|
540
|
-
isAdmin: row.is_admin,
|
|
541
|
-
defaultPermissions: row.default_permissions,
|
|
542
|
-
collectionPermissions: row.collection_permissions
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async listRoles(): Promise<Role[]> {
|
|
547
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
548
|
-
const result = await this.db.execute(sql`
|
|
549
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions
|
|
550
|
-
FROM ${sql.raw(tableName)}
|
|
551
|
-
ORDER BY name
|
|
552
|
-
`);
|
|
553
|
-
|
|
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 => ({
|
|
555
|
-
id: row.id,
|
|
556
|
-
name: row.name,
|
|
557
|
-
isAdmin: row.is_admin,
|
|
558
|
-
defaultPermissions: row.default_permissions,
|
|
559
|
-
collectionPermissions: row.collection_permissions
|
|
560
|
-
}));
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
|
|
564
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
565
|
-
const result = await this.db.execute(sql`
|
|
566
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
|
|
567
|
-
VALUES (
|
|
568
|
-
${data.id},
|
|
569
|
-
${data.name},
|
|
570
|
-
${data.isAdmin ?? false},
|
|
571
|
-
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
572
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
|
|
573
|
-
)
|
|
574
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions
|
|
575
|
-
`);
|
|
576
|
-
|
|
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 };
|
|
578
|
-
return {
|
|
579
|
-
id: row.id,
|
|
580
|
-
name: row.name,
|
|
581
|
-
isAdmin: row.is_admin,
|
|
582
|
-
defaultPermissions: row.default_permissions,
|
|
583
|
-
collectionPermissions: row.collection_permissions
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async updateRole(id: string, data: Partial<Omit<Role, "id">>): Promise<Role | null> {
|
|
588
|
-
const existing = await this.getRoleById(id);
|
|
589
|
-
if (!existing) return null;
|
|
590
|
-
|
|
591
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
592
|
-
await this.db.execute(sql`
|
|
593
|
-
UPDATE ${sql.raw(tableName)}
|
|
594
|
-
SET
|
|
595
|
-
name = ${data.name ?? existing.name},
|
|
596
|
-
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
597
|
-
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
598
|
-
collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb
|
|
599
|
-
WHERE id = ${id}
|
|
600
|
-
`);
|
|
601
|
-
|
|
602
|
-
return this.getRoleById(id);
|
|
603
|
-
}
|
|
604
497
|
|
|
605
|
-
async deleteRole(id: string): Promise<void> {
|
|
606
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
607
|
-
await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
498
|
|
|
611
499
|
export class RefreshTokenService {
|
|
612
500
|
private refreshTokensTable: PgTable & Record<string, AnyPgColumn>;
|
|
@@ -878,7 +766,6 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
878
766
|
*/
|
|
879
767
|
export class PostgresAuthRepository implements AuthRepository {
|
|
880
768
|
private userService: UserService;
|
|
881
|
-
private roleService: RoleService;
|
|
882
769
|
private tokenRepository: PostgresTokenRepository;
|
|
883
770
|
|
|
884
771
|
constructor(
|
|
@@ -886,7 +773,6 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
886
773
|
tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
|
|
887
774
|
) {
|
|
888
775
|
this.userService = new UserService(db, tableOrTables);
|
|
889
|
-
this.roleService = new RoleService(db, tableOrTables);
|
|
890
776
|
this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
|
|
891
777
|
}
|
|
892
778
|
|
|
@@ -968,30 +854,48 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
968
854
|
return this.userService.getUserWithRoles(userId);
|
|
969
855
|
}
|
|
970
856
|
|
|
971
|
-
// Role operations (
|
|
857
|
+
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
972
858
|
|
|
973
859
|
async getRoleById(id: string): Promise<RoleData | null> {
|
|
974
|
-
return
|
|
860
|
+
return {
|
|
861
|
+
id,
|
|
862
|
+
name: id,
|
|
863
|
+
isAdmin: id === "admin",
|
|
864
|
+
defaultPermissions: null,
|
|
865
|
+
collectionPermissions: null
|
|
866
|
+
};
|
|
975
867
|
}
|
|
976
868
|
|
|
977
869
|
async listRoles(): Promise<RoleData[]> {
|
|
978
|
-
return
|
|
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
|
+
];
|
|
979
875
|
}
|
|
980
876
|
|
|
981
|
-
async createRole(
|
|
982
|
-
return
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
+
};
|
|
987
885
|
}
|
|
988
886
|
|
|
989
887
|
async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
|
|
990
|
-
return
|
|
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
|
+
};
|
|
991
895
|
}
|
|
992
896
|
|
|
993
|
-
async deleteRole(
|
|
994
|
-
|
|
897
|
+
async deleteRole(_id: string): Promise<void> {
|
|
898
|
+
// No-op: roles are inline strings on users
|
|
995
899
|
}
|
|
996
900
|
|
|
997
901
|
// Token operations (delegate to PostgresTokenRepository)
|
|
@@ -1314,6 +1218,3 @@ export class MfaService implements MfaRepository {
|
|
|
1314
1218
|
|
|
1315
1219
|
/** PostgreSQL user repository implementation */
|
|
1316
1220
|
export type PostgresUserRepository = UserService;
|
|
1317
|
-
|
|
1318
|
-
/** PostgreSQL role repository implementation */
|
|
1319
|
-
export type PostgresRoleRepository = RoleService;
|