@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.
- package/dist/common/src/collections/default-collections.d.ts +9 -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 +1075 -470
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1071 -466
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +3 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +48 -31
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2135 -41
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +6 -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 +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -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 +5 -60
- 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/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 +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- 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 +14 -2
- package/src/PostgresBootstrapper.ts +30 -20
- package/src/auth/ensure-tables.ts +116 -103
- package/src/auth/services.ts +347 -177
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +85 -75
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +9 -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 +10 -166
- package/test/doctor.test.ts +6 -2
- package/test/drizzle-conditions.test.ts +168 -0
- package/vite.config.ts +1 -1
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -22
- package/src/schema/default-collections.ts +0 -69
package/dist/index.es.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Pool, Client } from "pg";
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
|
|
4
|
-
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb,
|
|
4
|
+
import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, text, boolean, varchar, uuid, unique, getTableConfig } from "drizzle-orm/pg-core";
|
|
5
5
|
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import { promises } from "fs";
|
|
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
|
|
|
11
11
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12
12
|
import { EventEmitter } from "events";
|
|
13
13
|
import { inspect } from "util";
|
|
14
|
-
import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
|
|
14
|
+
import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
|
|
15
15
|
const DEFAULT_POOL = {
|
|
16
16
|
max: 20,
|
|
17
17
|
idleTimeoutMillis: 3e4,
|
|
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
51
51
|
connectionString
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
55
|
+
const opts = {
|
|
56
|
+
...DEFAULT_POOL,
|
|
57
|
+
max: 5,
|
|
58
|
+
...poolConfig
|
|
59
|
+
};
|
|
60
|
+
const pgPoolConfig = {
|
|
61
|
+
connectionString,
|
|
62
|
+
max: opts.max,
|
|
63
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
64
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
65
|
+
query_timeout: opts.queryTimeout,
|
|
66
|
+
statement_timeout: opts.statementTimeout,
|
|
67
|
+
keepAlive: opts.keepAlive,
|
|
68
|
+
keepAliveInitialDelayMillis: 0
|
|
69
|
+
};
|
|
70
|
+
const pool = new Pool(pgPoolConfig);
|
|
71
|
+
pool.on("error", (err) => {
|
|
72
|
+
console.error("[pg-direct-pool] Unexpected pool error:", err.message);
|
|
73
|
+
});
|
|
74
|
+
const db = schema ? drizzle(pool, {
|
|
75
|
+
schema
|
|
76
|
+
}) : drizzle(pool);
|
|
77
|
+
return {
|
|
78
|
+
db,
|
|
79
|
+
pool,
|
|
80
|
+
connectionString
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
84
|
+
const opts = {
|
|
85
|
+
...DEFAULT_POOL,
|
|
86
|
+
max: 10,
|
|
87
|
+
...poolConfig
|
|
88
|
+
};
|
|
89
|
+
const pgPoolConfig = {
|
|
90
|
+
connectionString,
|
|
91
|
+
max: opts.max,
|
|
92
|
+
idleTimeoutMillis: opts.idleTimeoutMillis,
|
|
93
|
+
connectionTimeoutMillis: opts.connectionTimeoutMillis,
|
|
94
|
+
query_timeout: opts.queryTimeout,
|
|
95
|
+
statement_timeout: opts.statementTimeout,
|
|
96
|
+
keepAlive: opts.keepAlive,
|
|
97
|
+
keepAliveInitialDelayMillis: 0
|
|
98
|
+
};
|
|
99
|
+
const pool = new Pool(pgPoolConfig);
|
|
100
|
+
pool.on("error", (err) => {
|
|
101
|
+
console.error("[pg-replica-pool] Unexpected pool error:", err.message);
|
|
102
|
+
});
|
|
103
|
+
const db = schema ? drizzle(pool, {
|
|
104
|
+
schema
|
|
105
|
+
}) : drizzle(pool);
|
|
106
|
+
return {
|
|
107
|
+
db,
|
|
108
|
+
pool,
|
|
109
|
+
connectionString
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
113
|
+
__proto__: null,
|
|
114
|
+
createDirectDatabaseConnection,
|
|
115
|
+
createPostgresDatabaseConnection,
|
|
116
|
+
createReadReplicaConnection
|
|
117
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
54
118
|
class Vector {
|
|
55
119
|
value;
|
|
56
120
|
constructor(value) {
|
|
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
|
|
|
970
1034
|
return output;
|
|
971
1035
|
}
|
|
972
1036
|
for (const key in source) {
|
|
1037
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
973
1040
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
974
1041
|
const sourceValue = source[key];
|
|
975
1042
|
const outputValue = output[key];
|
|
@@ -2644,6 +2711,142 @@ class CollectionRegistry {
|
|
|
2644
2711
|
};
|
|
2645
2712
|
}
|
|
2646
2713
|
}
|
|
2714
|
+
const defaultUsersCollection = {
|
|
2715
|
+
name: "Users",
|
|
2716
|
+
singularName: "User",
|
|
2717
|
+
slug: "users",
|
|
2718
|
+
table: "users",
|
|
2719
|
+
schema: "rebase",
|
|
2720
|
+
icon: "Users",
|
|
2721
|
+
group: "Settings",
|
|
2722
|
+
openEntityMode: "dialog",
|
|
2723
|
+
disableDefaultActions: ["copy"],
|
|
2724
|
+
sort: ["createdAt", "desc"],
|
|
2725
|
+
properties: {
|
|
2726
|
+
id: {
|
|
2727
|
+
name: "ID",
|
|
2728
|
+
type: "string",
|
|
2729
|
+
isId: "uuid",
|
|
2730
|
+
ui: {
|
|
2731
|
+
readOnly: true
|
|
2732
|
+
}
|
|
2733
|
+
},
|
|
2734
|
+
email: {
|
|
2735
|
+
name: "Email",
|
|
2736
|
+
type: "string",
|
|
2737
|
+
validation: {
|
|
2738
|
+
required: true,
|
|
2739
|
+
unique: true
|
|
2740
|
+
}
|
|
2741
|
+
},
|
|
2742
|
+
displayName: {
|
|
2743
|
+
name: "Name",
|
|
2744
|
+
type: "string",
|
|
2745
|
+
columnName: "display_name",
|
|
2746
|
+
validation: {
|
|
2747
|
+
required: true
|
|
2748
|
+
}
|
|
2749
|
+
},
|
|
2750
|
+
photoURL: {
|
|
2751
|
+
name: "Photo URL",
|
|
2752
|
+
type: "string",
|
|
2753
|
+
columnName: "photo_url",
|
|
2754
|
+
url: "image"
|
|
2755
|
+
},
|
|
2756
|
+
roles: {
|
|
2757
|
+
name: "Roles",
|
|
2758
|
+
type: "array",
|
|
2759
|
+
columnType: "text[]",
|
|
2760
|
+
of: {
|
|
2761
|
+
name: "Role",
|
|
2762
|
+
type: "string",
|
|
2763
|
+
enum: {
|
|
2764
|
+
admin: "Admin",
|
|
2765
|
+
editor: "Editor",
|
|
2766
|
+
viewer: "Viewer"
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
},
|
|
2770
|
+
passwordHash: {
|
|
2771
|
+
name: "Password Hash",
|
|
2772
|
+
type: "string",
|
|
2773
|
+
columnName: "password_hash",
|
|
2774
|
+
ui: {
|
|
2775
|
+
hideFromCollection: true,
|
|
2776
|
+
disabled: {
|
|
2777
|
+
hidden: true
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
},
|
|
2781
|
+
emailVerified: {
|
|
2782
|
+
name: "Email Verified",
|
|
2783
|
+
type: "boolean",
|
|
2784
|
+
columnName: "email_verified",
|
|
2785
|
+
defaultValue: false,
|
|
2786
|
+
ui: {
|
|
2787
|
+
hideFromCollection: true,
|
|
2788
|
+
disabled: {
|
|
2789
|
+
hidden: true
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
},
|
|
2793
|
+
emailVerificationToken: {
|
|
2794
|
+
name: "Email Verification Token",
|
|
2795
|
+
type: "string",
|
|
2796
|
+
columnName: "email_verification_token",
|
|
2797
|
+
ui: {
|
|
2798
|
+
hideFromCollection: true,
|
|
2799
|
+
disabled: {
|
|
2800
|
+
hidden: true
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
},
|
|
2804
|
+
emailVerificationSentAt: {
|
|
2805
|
+
name: "Email Verification Sent At",
|
|
2806
|
+
type: "date",
|
|
2807
|
+
columnName: "email_verification_sent_at",
|
|
2808
|
+
ui: {
|
|
2809
|
+
hideFromCollection: true,
|
|
2810
|
+
disabled: {
|
|
2811
|
+
hidden: true
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
},
|
|
2815
|
+
metadata: {
|
|
2816
|
+
name: "Metadata",
|
|
2817
|
+
type: "map",
|
|
2818
|
+
defaultValue: {},
|
|
2819
|
+
ui: {
|
|
2820
|
+
hideFromCollection: true,
|
|
2821
|
+
disabled: {
|
|
2822
|
+
hidden: true
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2826
|
+
createdAt: {
|
|
2827
|
+
name: "Created At",
|
|
2828
|
+
type: "date",
|
|
2829
|
+
columnName: "created_at",
|
|
2830
|
+
ui: {
|
|
2831
|
+
readOnly: true
|
|
2832
|
+
}
|
|
2833
|
+
},
|
|
2834
|
+
updatedAt: {
|
|
2835
|
+
name: "Updated At",
|
|
2836
|
+
type: "date",
|
|
2837
|
+
columnName: "updated_at",
|
|
2838
|
+
autoValue: "on_update",
|
|
2839
|
+
ui: {
|
|
2840
|
+
hideFromCollection: true,
|
|
2841
|
+
disabled: {
|
|
2842
|
+
hidden: true
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
listProperties: ["displayName", "email", "roles", "createdAt"],
|
|
2848
|
+
propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
|
|
2849
|
+
};
|
|
2647
2850
|
function mapOperator(op) {
|
|
2648
2851
|
switch (op) {
|
|
2649
2852
|
case "==":
|
|
@@ -2890,6 +3093,9 @@ function createDriverAccessor(driver, slug) {
|
|
|
2890
3093
|
}
|
|
2891
3094
|
});
|
|
2892
3095
|
},
|
|
3096
|
+
deleteAll: driver.deleteAll ? async () => {
|
|
3097
|
+
return driver.deleteAll(slug);
|
|
3098
|
+
} : void 0,
|
|
2893
3099
|
count: driver.countEntities ? async (params) => {
|
|
2894
3100
|
return driver.countEntities({
|
|
2895
3101
|
path: slug,
|
|
@@ -2984,7 +3190,13 @@ class DrizzleConditionBuilder {
|
|
|
2984
3190
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
2985
3191
|
if (!filterParam) continue;
|
|
2986
3192
|
const [op, value] = filterParam;
|
|
2987
|
-
|
|
3193
|
+
let fieldColumn = table[field];
|
|
3194
|
+
if (!fieldColumn) {
|
|
3195
|
+
const relationKey = `${field}_id`;
|
|
3196
|
+
if (relationKey in table) {
|
|
3197
|
+
fieldColumn = table[relationKey];
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
2988
3200
|
if (!fieldColumn) {
|
|
2989
3201
|
console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
2990
3202
|
continue;
|
|
@@ -3026,6 +3238,17 @@ class DrizzleConditionBuilder {
|
|
|
3026
3238
|
return null;
|
|
3027
3239
|
case "array-contains":
|
|
3028
3240
|
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3241
|
+
case "array-contains-any":
|
|
3242
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3243
|
+
const textValues = value.map((v) => String(v));
|
|
3244
|
+
return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
|
|
3245
|
+
}
|
|
3246
|
+
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
3247
|
+
case "not-in":
|
|
3248
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
3249
|
+
return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
|
|
3250
|
+
}
|
|
3251
|
+
return null;
|
|
3029
3252
|
default:
|
|
3030
3253
|
console.warn(`Unsupported filter operation: ${op}`);
|
|
3031
3254
|
return null;
|
|
@@ -3541,6 +3764,40 @@ class DrizzleConditionBuilder {
|
|
|
3541
3764
|
return null;
|
|
3542
3765
|
}
|
|
3543
3766
|
}
|
|
3767
|
+
/**
|
|
3768
|
+
* Build vector similarity search expressions for pgvector.
|
|
3769
|
+
*
|
|
3770
|
+
* Returns:
|
|
3771
|
+
* - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
|
|
3772
|
+
* - `filter`: optional WHERE clause for distance threshold
|
|
3773
|
+
* - `distanceSelect`: SQL expression for selecting the distance as `_distance`
|
|
3774
|
+
*/
|
|
3775
|
+
static buildVectorSearchConditions(table, vectorSearch) {
|
|
3776
|
+
const column = table[vectorSearch.property];
|
|
3777
|
+
if (!column) {
|
|
3778
|
+
throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
3779
|
+
}
|
|
3780
|
+
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
3781
|
+
const distanceFn = vectorSearch.distance || "cosine";
|
|
3782
|
+
let operator;
|
|
3783
|
+
switch (distanceFn) {
|
|
3784
|
+
case "cosine":
|
|
3785
|
+
operator = "<=>";
|
|
3786
|
+
break;
|
|
3787
|
+
case "l2":
|
|
3788
|
+
operator = "<->";
|
|
3789
|
+
break;
|
|
3790
|
+
case "inner_product":
|
|
3791
|
+
operator = "<#>";
|
|
3792
|
+
break;
|
|
3793
|
+
}
|
|
3794
|
+
const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
|
|
3795
|
+
return {
|
|
3796
|
+
orderBy: distanceExpr,
|
|
3797
|
+
filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
|
|
3798
|
+
distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3544
3801
|
}
|
|
3545
3802
|
const PostgresConditionBuilder = DrizzleConditionBuilder;
|
|
3546
3803
|
function getColumnMeta(col) {
|
|
@@ -5486,7 +5743,7 @@ class EntityFetchService {
|
|
|
5486
5743
|
const qb = this.getQueryBuilder(tableName);
|
|
5487
5744
|
const withConfig = this.buildWithConfig(collection);
|
|
5488
5745
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
5489
|
-
if (qb && !options.searchString && !hasRelations) {
|
|
5746
|
+
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
|
|
5490
5747
|
try {
|
|
5491
5748
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
5492
5749
|
const results2 = await qb.findMany(queryOpts);
|
|
@@ -5500,7 +5757,14 @@ class EntityFetchService {
|
|
|
5500
5757
|
console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
|
|
5501
5758
|
}
|
|
5502
5759
|
}
|
|
5503
|
-
let
|
|
5760
|
+
let vectorMeta;
|
|
5761
|
+
if (options.vectorSearch) {
|
|
5762
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
5763
|
+
}
|
|
5764
|
+
let query = vectorMeta ? this.db.select({
|
|
5765
|
+
table_row: table,
|
|
5766
|
+
_distance: vectorMeta.distanceSelect
|
|
5767
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5504
5768
|
const allConditions = [];
|
|
5505
5769
|
if (options.searchString) {
|
|
5506
5770
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5511,12 +5775,17 @@ class EntityFetchService {
|
|
|
5511
5775
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5512
5776
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5513
5777
|
}
|
|
5778
|
+
if (vectorMeta?.filter) {
|
|
5779
|
+
allConditions.push(vectorMeta.filter);
|
|
5780
|
+
}
|
|
5514
5781
|
if (allConditions.length > 0) {
|
|
5515
5782
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5516
5783
|
if (finalCondition) query = query.where(finalCondition);
|
|
5517
5784
|
}
|
|
5518
5785
|
const orderExpressions = [];
|
|
5519
|
-
if (
|
|
5786
|
+
if (vectorMeta) {
|
|
5787
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
5788
|
+
} else if (options.orderBy) {
|
|
5520
5789
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5521
5790
|
if (orderByField) {
|
|
5522
5791
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5532,10 +5801,14 @@ class EntityFetchService {
|
|
|
5532
5801
|
if (finalCondition) query = query.where(finalCondition);
|
|
5533
5802
|
}
|
|
5534
5803
|
}
|
|
5535
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
5804
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5536
5805
|
if (limitValue) query = query.limit(limitValue);
|
|
5537
5806
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5538
|
-
const
|
|
5807
|
+
const rawResults = await query;
|
|
5808
|
+
const results = vectorMeta ? rawResults.map((r) => ({
|
|
5809
|
+
...r.table_row,
|
|
5810
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
5811
|
+
})) : rawResults;
|
|
5539
5812
|
return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
5540
5813
|
}
|
|
5541
5814
|
/**
|
|
@@ -5757,7 +6030,7 @@ class EntityFetchService {
|
|
|
5757
6030
|
const idField = table[idInfo.fieldName];
|
|
5758
6031
|
const tableName = getTableName$1(table);
|
|
5759
6032
|
const qb = this.getQueryBuilder(tableName);
|
|
5760
|
-
if (qb && !options.searchString) {
|
|
6033
|
+
if (qb && !options.searchString && !options.vectorSearch) {
|
|
5761
6034
|
try {
|
|
5762
6035
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
5763
6036
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
@@ -5903,7 +6176,14 @@ class EntityFetchService {
|
|
|
5903
6176
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
5904
6177
|
const idInfo = idInfoArray[0];
|
|
5905
6178
|
const idField = table[idInfo.fieldName];
|
|
5906
|
-
let
|
|
6179
|
+
let vectorMeta;
|
|
6180
|
+
if (options.vectorSearch) {
|
|
6181
|
+
vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
6182
|
+
}
|
|
6183
|
+
let query = vectorMeta ? this.db.select({
|
|
6184
|
+
table_row: table,
|
|
6185
|
+
_distance: vectorMeta.distanceSelect
|
|
6186
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
5907
6187
|
const allConditions = [];
|
|
5908
6188
|
if (options.searchString) {
|
|
5909
6189
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -5914,12 +6194,17 @@ class EntityFetchService {
|
|
|
5914
6194
|
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
5915
6195
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
5916
6196
|
}
|
|
6197
|
+
if (vectorMeta?.filter) {
|
|
6198
|
+
allConditions.push(vectorMeta.filter);
|
|
6199
|
+
}
|
|
5917
6200
|
if (allConditions.length > 0) {
|
|
5918
6201
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
5919
6202
|
if (finalCondition) query = query.where(finalCondition);
|
|
5920
6203
|
}
|
|
5921
6204
|
const orderExpressions = [];
|
|
5922
|
-
if (
|
|
6205
|
+
if (vectorMeta) {
|
|
6206
|
+
orderExpressions.push(asc(vectorMeta.orderBy));
|
|
6207
|
+
} else if (options.orderBy) {
|
|
5923
6208
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
5924
6209
|
if (orderByField) {
|
|
5925
6210
|
orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
@@ -5927,10 +6212,17 @@ class EntityFetchService {
|
|
|
5927
6212
|
}
|
|
5928
6213
|
orderExpressions.push(desc(idField));
|
|
5929
6214
|
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
5930
|
-
const limitValue = options.searchString ? options.limit || 50 : options.limit;
|
|
6215
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
5931
6216
|
if (limitValue) query = query.limit(limitValue);
|
|
5932
6217
|
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
5933
|
-
|
|
6218
|
+
const rawResults = await query;
|
|
6219
|
+
if (vectorMeta) {
|
|
6220
|
+
return rawResults.map((r) => ({
|
|
6221
|
+
...r.table_row,
|
|
6222
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
6223
|
+
}));
|
|
6224
|
+
}
|
|
6225
|
+
return rawResults;
|
|
5934
6226
|
}
|
|
5935
6227
|
/**
|
|
5936
6228
|
* Check if the Drizzle instance has the relational query API available
|
|
@@ -6068,6 +6360,14 @@ class EntityPersistService {
|
|
|
6068
6360
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
6069
6361
|
await this.db.delete(table).where(eq(idField, parsedId));
|
|
6070
6362
|
}
|
|
6363
|
+
/**
|
|
6364
|
+
* Delete all entities from a collection
|
|
6365
|
+
*/
|
|
6366
|
+
async deleteAll(collectionPath, _databaseId) {
|
|
6367
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6368
|
+
const table = getTableForCollection(collection, this.registry);
|
|
6369
|
+
await this.db.delete(table);
|
|
6370
|
+
}
|
|
6071
6371
|
/**
|
|
6072
6372
|
* Save an entity (create or update)
|
|
6073
6373
|
*/
|
|
@@ -6399,6 +6699,12 @@ class EntityService {
|
|
|
6399
6699
|
async deleteEntity(collectionPath, entityId, databaseId) {
|
|
6400
6700
|
return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
|
|
6401
6701
|
}
|
|
6702
|
+
/**
|
|
6703
|
+
* Delete all entities from a collection
|
|
6704
|
+
*/
|
|
6705
|
+
async deleteAll(collectionPath, databaseId) {
|
|
6706
|
+
return this.persistService.deleteAll(collectionPath, databaseId);
|
|
6707
|
+
}
|
|
6402
6708
|
/**
|
|
6403
6709
|
* Execute raw SQL
|
|
6404
6710
|
*/
|
|
@@ -6684,7 +6990,8 @@ class PostgresBackendDriver {
|
|
|
6684
6990
|
startAfter,
|
|
6685
6991
|
orderBy,
|
|
6686
6992
|
searchString,
|
|
6687
|
-
order
|
|
6993
|
+
order,
|
|
6994
|
+
vectorSearch
|
|
6688
6995
|
}) {
|
|
6689
6996
|
const entities = await this.entityService.fetchCollection(path2, {
|
|
6690
6997
|
filter,
|
|
@@ -6694,7 +7001,8 @@ class PostgresBackendDriver {
|
|
|
6694
7001
|
offset,
|
|
6695
7002
|
startAfter,
|
|
6696
7003
|
databaseId: collection?.databaseId,
|
|
6697
|
-
searchString
|
|
7004
|
+
searchString,
|
|
7005
|
+
vectorSearch
|
|
6698
7006
|
});
|
|
6699
7007
|
const {
|
|
6700
7008
|
collection: resolvedCollection,
|
|
@@ -7097,6 +7405,10 @@ class PostgresBackendDriver {
|
|
|
7097
7405
|
await this.realtimeService.notifyEntityUpdate(entity.path, entity.id.toString(), null, entity.databaseId || resolvedCollection?.databaseId);
|
|
7098
7406
|
}
|
|
7099
7407
|
}
|
|
7408
|
+
async deleteAll(path2) {
|
|
7409
|
+
await this.entityService.deleteAll(path2);
|
|
7410
|
+
await this.realtimeService.notifyEntityUpdate(path2, "*", null);
|
|
7411
|
+
}
|
|
7100
7412
|
async checkUniqueField(path2, name, value, entityId, collection) {
|
|
7101
7413
|
return this.entityService.checkUniqueField(path2, name, value, entityId, collection?.databaseId);
|
|
7102
7414
|
}
|
|
@@ -7366,11 +7678,11 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7366
7678
|
console.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object:", this.user);
|
|
7367
7679
|
userId = "anonymous";
|
|
7368
7680
|
}
|
|
7369
|
-
const
|
|
7681
|
+
const userRoles = this.user?.roles ?? [];
|
|
7370
7682
|
if (!this.user?.roles) {
|
|
7371
7683
|
console.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object:", this.user);
|
|
7372
7684
|
}
|
|
7373
|
-
const normalizedRoles =
|
|
7685
|
+
const normalizedRoles = userRoles.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
|
|
7374
7686
|
const rolesString = normalizedRoles.join(",");
|
|
7375
7687
|
await tx.execute(sql`
|
|
7376
7688
|
SELECT
|
|
@@ -7378,7 +7690,7 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7378
7690
|
set_config('app.user_roles', ${rolesString}, true),
|
|
7379
7691
|
set_config('app.jwt', ${JSON.stringify({
|
|
7380
7692
|
sub: userId,
|
|
7381
|
-
roles:
|
|
7693
|
+
roles: userRoles
|
|
7382
7694
|
})}, true)
|
|
7383
7695
|
`);
|
|
7384
7696
|
const txEntityService = new EntityService(tx, this.delegate.registry);
|
|
@@ -7433,6 +7745,9 @@ class AuthenticatedPostgresBackendDriver {
|
|
|
7433
7745
|
async deleteEntity(props) {
|
|
7434
7746
|
return this.withTransaction((delegate) => delegate.deleteEntity(props));
|
|
7435
7747
|
}
|
|
7748
|
+
async deleteAll(path2) {
|
|
7749
|
+
return this.delegate.deleteAll(path2);
|
|
7750
|
+
}
|
|
7436
7751
|
async checkUniqueField(path2, name, value, entityId, collection) {
|
|
7437
7752
|
return this.withTransaction((delegate) => delegate.checkUniqueField(path2, name, value, entityId, collection));
|
|
7438
7753
|
}
|
|
@@ -7512,11 +7827,10 @@ class DatabasePoolManager {
|
|
|
7512
7827
|
this.pools.clear();
|
|
7513
7828
|
}
|
|
7514
7829
|
}
|
|
7515
|
-
function createAuthSchema(
|
|
7516
|
-
const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
|
|
7830
|
+
function createAuthSchema(usersSchemaName = "rebase") {
|
|
7517
7831
|
const usersSchema2 = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
7518
|
-
const
|
|
7519
|
-
const usersTableCreator =
|
|
7832
|
+
const tableCreator = usersSchema2 ? usersSchema2.table.bind(usersSchema2) : pgTable;
|
|
7833
|
+
const usersTableCreator = tableCreator;
|
|
7520
7834
|
const users2 = usersTableCreator("users", {
|
|
7521
7835
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
7522
7836
|
email: varchar("email", {
|
|
@@ -7537,38 +7851,13 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7537
7851
|
length: 255
|
|
7538
7852
|
}),
|
|
7539
7853
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
7854
|
+
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
7855
|
+
roles: text("roles").array().default([]).notNull(),
|
|
7540
7856
|
metadata: jsonb("metadata").$type().default({}).notNull(),
|
|
7541
7857
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7542
7858
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7543
7859
|
});
|
|
7544
|
-
const
|
|
7545
|
-
id: varchar("id", {
|
|
7546
|
-
length: 50
|
|
7547
|
-
}).primaryKey(),
|
|
7548
|
-
// 'admin', 'editor', 'viewer'
|
|
7549
|
-
name: varchar("name", {
|
|
7550
|
-
length: 100
|
|
7551
|
-
}).notNull(),
|
|
7552
|
-
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
7553
|
-
defaultPermissions: jsonb("default_permissions").$type(),
|
|
7554
|
-
collectionPermissions: jsonb("collection_permissions").$type(),
|
|
7555
|
-
config: jsonb("config").$type()
|
|
7556
|
-
});
|
|
7557
|
-
const userRoles2 = rolesTableCreator("user_roles", {
|
|
7558
|
-
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7559
|
-
onDelete: "cascade"
|
|
7560
|
-
}),
|
|
7561
|
-
roleId: varchar("role_id", {
|
|
7562
|
-
length: 50
|
|
7563
|
-
}).notNull().references(() => roles2.id, {
|
|
7564
|
-
onDelete: "cascade"
|
|
7565
|
-
})
|
|
7566
|
-
}, (table) => ({
|
|
7567
|
-
pk: primaryKey({
|
|
7568
|
-
columns: [table.userId, table.roleId]
|
|
7569
|
-
})
|
|
7570
|
-
}));
|
|
7571
|
-
const refreshTokens2 = rolesTableCreator("refresh_tokens", {
|
|
7860
|
+
const refreshTokens2 = tableCreator("refresh_tokens", {
|
|
7572
7861
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
7573
7862
|
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7574
7863
|
onDelete: "cascade"
|
|
@@ -7587,7 +7876,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7587
7876
|
}, (table) => ({
|
|
7588
7877
|
uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
|
|
7589
7878
|
}));
|
|
7590
|
-
const passwordResetTokens2 =
|
|
7879
|
+
const passwordResetTokens2 = tableCreator("password_reset_tokens", {
|
|
7591
7880
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
7592
7881
|
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7593
7882
|
onDelete: "cascade"
|
|
@@ -7599,14 +7888,14 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7599
7888
|
usedAt: timestamp("used_at"),
|
|
7600
7889
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7601
7890
|
});
|
|
7602
|
-
const appConfig2 =
|
|
7891
|
+
const appConfig2 = tableCreator("app_config", {
|
|
7603
7892
|
key: varchar("key", {
|
|
7604
7893
|
length: 100
|
|
7605
7894
|
}).primaryKey(),
|
|
7606
7895
|
value: jsonb("value").notNull(),
|
|
7607
7896
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7608
7897
|
});
|
|
7609
|
-
const userIdentities2 =
|
|
7898
|
+
const userIdentities2 = tableCreator("user_identities", {
|
|
7610
7899
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
7611
7900
|
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7612
7901
|
onDelete: "cascade"
|
|
@@ -7624,74 +7913,126 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
|
|
|
7624
7913
|
}, (table) => ({
|
|
7625
7914
|
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
7626
7915
|
}));
|
|
7916
|
+
const mfaFactors2 = tableCreator("mfa_factors", {
|
|
7917
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7918
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7919
|
+
onDelete: "cascade"
|
|
7920
|
+
}),
|
|
7921
|
+
factorType: varchar("factor_type", {
|
|
7922
|
+
length: 20
|
|
7923
|
+
}).notNull(),
|
|
7924
|
+
// 'totp'
|
|
7925
|
+
secretEncrypted: varchar("secret_encrypted", {
|
|
7926
|
+
length: 500
|
|
7927
|
+
}).notNull(),
|
|
7928
|
+
friendlyName: varchar("friendly_name", {
|
|
7929
|
+
length: 255
|
|
7930
|
+
}),
|
|
7931
|
+
verified: boolean("verified").default(false).notNull(),
|
|
7932
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7933
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
7934
|
+
});
|
|
7935
|
+
const mfaChallenges2 = tableCreator("mfa_challenges", {
|
|
7936
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7937
|
+
factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
|
|
7938
|
+
onDelete: "cascade"
|
|
7939
|
+
}),
|
|
7940
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
7941
|
+
verifiedAt: timestamp("verified_at"),
|
|
7942
|
+
ipAddress: varchar("ip_address", {
|
|
7943
|
+
length: 45
|
|
7944
|
+
}),
|
|
7945
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
7946
|
+
});
|
|
7947
|
+
const recoveryCodes2 = tableCreator("recovery_codes", {
|
|
7948
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
7949
|
+
userId: uuid("user_id").notNull().references(() => users2.id, {
|
|
7950
|
+
onDelete: "cascade"
|
|
7951
|
+
}),
|
|
7952
|
+
codeHash: varchar("code_hash", {
|
|
7953
|
+
length: 255
|
|
7954
|
+
}).notNull(),
|
|
7955
|
+
usedAt: timestamp("used_at"),
|
|
7956
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
7957
|
+
});
|
|
7627
7958
|
return {
|
|
7628
|
-
rolesSchema,
|
|
7629
7959
|
usersSchema: usersSchema2,
|
|
7630
7960
|
users: users2,
|
|
7631
|
-
roles: roles2,
|
|
7632
|
-
userRoles: userRoles2,
|
|
7633
7961
|
refreshTokens: refreshTokens2,
|
|
7634
7962
|
passwordResetTokens: passwordResetTokens2,
|
|
7635
7963
|
appConfig: appConfig2,
|
|
7636
|
-
userIdentities: userIdentities2
|
|
7964
|
+
userIdentities: userIdentities2,
|
|
7965
|
+
mfaFactors: mfaFactors2,
|
|
7966
|
+
mfaChallenges: mfaChallenges2,
|
|
7967
|
+
recoveryCodes: recoveryCodes2
|
|
7637
7968
|
};
|
|
7638
7969
|
}
|
|
7639
|
-
const defaultAuthSchema = createAuthSchema("rebase"
|
|
7640
|
-
const rebaseSchema = defaultAuthSchema.rolesSchema;
|
|
7970
|
+
const defaultAuthSchema = createAuthSchema("rebase");
|
|
7641
7971
|
const usersSchema = defaultAuthSchema.usersSchema;
|
|
7642
7972
|
const users = defaultAuthSchema.users;
|
|
7643
|
-
const roles = defaultAuthSchema.roles;
|
|
7644
|
-
const userRoles = defaultAuthSchema.userRoles;
|
|
7645
7973
|
const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
7646
7974
|
const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
7647
7975
|
const appConfig = defaultAuthSchema.appConfig;
|
|
7648
7976
|
const userIdentities = defaultAuthSchema.userIdentities;
|
|
7977
|
+
const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
7978
|
+
const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
7979
|
+
const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
7649
7980
|
const usersRelations = relations(users, ({
|
|
7650
7981
|
many
|
|
7651
7982
|
}) => ({
|
|
7652
|
-
userRoles: many(userRoles),
|
|
7653
7983
|
refreshTokens: many(refreshTokens),
|
|
7654
7984
|
passwordResetTokens: many(passwordResetTokens),
|
|
7655
|
-
userIdentities: many(userIdentities)
|
|
7985
|
+
userIdentities: many(userIdentities),
|
|
7986
|
+
mfaFactors: many(mfaFactors),
|
|
7987
|
+
recoveryCodes: many(recoveryCodes)
|
|
7656
7988
|
}));
|
|
7657
|
-
const
|
|
7658
|
-
|
|
7989
|
+
const refreshTokensRelations = relations(refreshTokens, ({
|
|
7990
|
+
one
|
|
7659
7991
|
}) => ({
|
|
7660
|
-
|
|
7992
|
+
user: one(users, {
|
|
7993
|
+
fields: [refreshTokens.userId],
|
|
7994
|
+
references: [users.id]
|
|
7995
|
+
})
|
|
7661
7996
|
}));
|
|
7662
|
-
const
|
|
7997
|
+
const passwordResetTokensRelations = relations(passwordResetTokens, ({
|
|
7663
7998
|
one
|
|
7664
7999
|
}) => ({
|
|
7665
8000
|
user: one(users, {
|
|
7666
|
-
fields: [
|
|
8001
|
+
fields: [passwordResetTokens.userId],
|
|
7667
8002
|
references: [users.id]
|
|
7668
|
-
}),
|
|
7669
|
-
role: one(roles, {
|
|
7670
|
-
fields: [userRoles.roleId],
|
|
7671
|
-
references: [roles.id]
|
|
7672
8003
|
})
|
|
7673
8004
|
}));
|
|
7674
|
-
const
|
|
8005
|
+
const userIdentitiesRelations = relations(userIdentities, ({
|
|
7675
8006
|
one
|
|
7676
8007
|
}) => ({
|
|
7677
8008
|
user: one(users, {
|
|
7678
|
-
fields: [
|
|
8009
|
+
fields: [userIdentities.userId],
|
|
7679
8010
|
references: [users.id]
|
|
7680
8011
|
})
|
|
7681
8012
|
}));
|
|
7682
|
-
const
|
|
7683
|
-
one
|
|
8013
|
+
const mfaFactorsRelations = relations(mfaFactors, ({
|
|
8014
|
+
one,
|
|
8015
|
+
many
|
|
7684
8016
|
}) => ({
|
|
7685
8017
|
user: one(users, {
|
|
7686
|
-
fields: [
|
|
8018
|
+
fields: [mfaFactors.userId],
|
|
7687
8019
|
references: [users.id]
|
|
8020
|
+
}),
|
|
8021
|
+
challenges: many(mfaChallenges)
|
|
8022
|
+
}));
|
|
8023
|
+
const mfaChallengesRelations = relations(mfaChallenges, ({
|
|
8024
|
+
one
|
|
8025
|
+
}) => ({
|
|
8026
|
+
factor: one(mfaFactors, {
|
|
8027
|
+
fields: [mfaChallenges.factorId],
|
|
8028
|
+
references: [mfaFactors.id]
|
|
7688
8029
|
})
|
|
7689
8030
|
}));
|
|
7690
|
-
const
|
|
8031
|
+
const recoveryCodesRelations = relations(recoveryCodes, ({
|
|
7691
8032
|
one
|
|
7692
8033
|
}) => ({
|
|
7693
8034
|
user: one(users, {
|
|
7694
|
-
fields: [
|
|
8035
|
+
fields: [recoveryCodes.userId],
|
|
7695
8036
|
references: [users.id]
|
|
7696
8037
|
})
|
|
7697
8038
|
}));
|
|
@@ -7751,6 +8092,8 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
7751
8092
|
columnDefinition = `${enumName}("${colName}")`;
|
|
7752
8093
|
} else if ("isId" in stringProp && stringProp.isId === "uuid") {
|
|
7753
8094
|
columnDefinition = `uuid("${colName}")`;
|
|
8095
|
+
} else if (stringProp.columnType === "uuid") {
|
|
8096
|
+
columnDefinition = `uuid("${colName}")`;
|
|
7754
8097
|
} else if (stringProp.columnType === "text") {
|
|
7755
8098
|
columnDefinition = `text("${colName}")`;
|
|
7756
8099
|
} else if (stringProp.columnType === "char") {
|
|
@@ -7818,11 +8161,38 @@ const getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
7818
8161
|
}
|
|
7819
8162
|
break;
|
|
7820
8163
|
}
|
|
7821
|
-
case "map":
|
|
8164
|
+
case "map": {
|
|
8165
|
+
const mapProp = prop;
|
|
8166
|
+
if (mapProp.columnType === "json") {
|
|
8167
|
+
columnDefinition = `json("${colName}")`;
|
|
8168
|
+
} else {
|
|
8169
|
+
columnDefinition = `jsonb("${colName}")`;
|
|
8170
|
+
}
|
|
8171
|
+
break;
|
|
8172
|
+
}
|
|
7822
8173
|
case "array": {
|
|
7823
|
-
const
|
|
7824
|
-
|
|
8174
|
+
const arrayProp = prop;
|
|
8175
|
+
let colType = arrayProp.columnType;
|
|
8176
|
+
if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
|
|
8177
|
+
const ofProp = arrayProp.of;
|
|
8178
|
+
if (ofProp.type === "string") {
|
|
8179
|
+
colType = "text[]";
|
|
8180
|
+
} else if (ofProp.type === "number") {
|
|
8181
|
+
colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
8182
|
+
} else if (ofProp.type === "boolean") {
|
|
8183
|
+
colType = "boolean[]";
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
if (colType === "json") {
|
|
7825
8187
|
columnDefinition = `json("${colName}")`;
|
|
8188
|
+
} else if (colType === "text[]") {
|
|
8189
|
+
columnDefinition = `text("${colName}").array()`;
|
|
8190
|
+
} else if (colType === "integer[]") {
|
|
8191
|
+
columnDefinition = `integer("${colName}").array()`;
|
|
8192
|
+
} else if (colType === "boolean[]") {
|
|
8193
|
+
columnDefinition = `boolean("${colName}").array()`;
|
|
8194
|
+
} else if (colType === "numeric[]") {
|
|
8195
|
+
columnDefinition = `numeric("${colName}").array()`;
|
|
7826
8196
|
} else {
|
|
7827
8197
|
columnDefinition = `jsonb("${colName}")`;
|
|
7828
8198
|
}
|
|
@@ -7906,8 +8276,8 @@ const resolveRawSql = (expression) => {
|
|
|
7906
8276
|
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
7907
8277
|
return `sql\`${resolved}\``;
|
|
7908
8278
|
};
|
|
7909
|
-
const wrapWithRoleCheck = (clause,
|
|
7910
|
-
const rolesArrayString = `ARRAY[${
|
|
8279
|
+
const wrapWithRoleCheck = (clause, roles) => {
|
|
8280
|
+
const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
|
|
7911
8281
|
const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
|
|
7912
8282
|
return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
|
|
7913
8283
|
};
|
|
@@ -7960,22 +8330,22 @@ const generatePolicyCode = (collection, rule, index) => {
|
|
|
7960
8330
|
};
|
|
7961
8331
|
const generateSinglePolicyCode = (collection, rule, operation, policyName) => {
|
|
7962
8332
|
const mode = rule.mode ?? "permissive";
|
|
7963
|
-
const
|
|
8333
|
+
const roles = rule.roles ? [...rule.roles].sort() : void 0;
|
|
7964
8334
|
const needsUsing = operation !== "insert";
|
|
7965
8335
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
7966
8336
|
let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
|
|
7967
8337
|
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
|
|
7968
|
-
if (
|
|
8338
|
+
if (roles && roles.length > 0) {
|
|
7969
8339
|
if (usingClause) {
|
|
7970
|
-
usingClause = wrapWithRoleCheck(usingClause,
|
|
8340
|
+
usingClause = wrapWithRoleCheck(usingClause, roles);
|
|
7971
8341
|
} else if (needsUsing) {
|
|
7972
|
-
const rolesArrayString = `ARRAY[${
|
|
8342
|
+
const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
|
|
7973
8343
|
usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
|
|
7974
8344
|
}
|
|
7975
8345
|
if (withCheckClause) {
|
|
7976
|
-
withCheckClause = wrapWithRoleCheck(withCheckClause,
|
|
8346
|
+
withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
|
|
7977
8347
|
} else if (needsWithCheck) {
|
|
7978
|
-
const rolesArrayString = `ARRAY[${
|
|
8348
|
+
const rolesArrayString = `ARRAY[${roles.map((r) => `'${r}'`).join(",")}]`;
|
|
7979
8349
|
withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
|
|
7980
8350
|
}
|
|
7981
8351
|
}
|
|
@@ -8298,91 +8668,7 @@ ${tableRelations.join(",\n")}
|
|
|
8298
8668
|
schemaContent += tablesExport + enumsExport + relationsExport;
|
|
8299
8669
|
return schemaContent;
|
|
8300
8670
|
};
|
|
8301
|
-
const
|
|
8302
|
-
name: "Users",
|
|
8303
|
-
singularName: "User",
|
|
8304
|
-
slug: "users",
|
|
8305
|
-
table: "users",
|
|
8306
|
-
schema: "rebase",
|
|
8307
|
-
icon: "Users",
|
|
8308
|
-
group: "Settings",
|
|
8309
|
-
properties: {
|
|
8310
|
-
id: {
|
|
8311
|
-
name: "ID",
|
|
8312
|
-
type: "string",
|
|
8313
|
-
isId: "uuid"
|
|
8314
|
-
},
|
|
8315
|
-
email: {
|
|
8316
|
-
name: "Email",
|
|
8317
|
-
type: "string",
|
|
8318
|
-
validation: {
|
|
8319
|
-
required: true,
|
|
8320
|
-
unique: true
|
|
8321
|
-
}
|
|
8322
|
-
},
|
|
8323
|
-
password_hash: {
|
|
8324
|
-
name: "Password Hash",
|
|
8325
|
-
type: "string",
|
|
8326
|
-
ui: {
|
|
8327
|
-
hideFromCollection: true
|
|
8328
|
-
}
|
|
8329
|
-
},
|
|
8330
|
-
display_name: {
|
|
8331
|
-
name: "Display Name",
|
|
8332
|
-
type: "string"
|
|
8333
|
-
},
|
|
8334
|
-
photo_url: {
|
|
8335
|
-
name: "Photo URL",
|
|
8336
|
-
type: "string"
|
|
8337
|
-
},
|
|
8338
|
-
email_verified: {
|
|
8339
|
-
name: "Email Verified",
|
|
8340
|
-
type: "boolean",
|
|
8341
|
-
defaultValue: false
|
|
8342
|
-
},
|
|
8343
|
-
email_verification_token: {
|
|
8344
|
-
name: "Email Verification Token",
|
|
8345
|
-
type: "string",
|
|
8346
|
-
ui: {
|
|
8347
|
-
hideFromCollection: true
|
|
8348
|
-
}
|
|
8349
|
-
},
|
|
8350
|
-
email_verification_sent_at: {
|
|
8351
|
-
name: "Email Verification Sent At",
|
|
8352
|
-
type: "date",
|
|
8353
|
-
ui: {
|
|
8354
|
-
hideFromCollection: true
|
|
8355
|
-
}
|
|
8356
|
-
},
|
|
8357
|
-
metadata: {
|
|
8358
|
-
name: "Metadata",
|
|
8359
|
-
type: "map",
|
|
8360
|
-
defaultValue: {},
|
|
8361
|
-
ui: {
|
|
8362
|
-
hideFromCollection: true
|
|
8363
|
-
}
|
|
8364
|
-
},
|
|
8365
|
-
created_at: {
|
|
8366
|
-
name: "Created At",
|
|
8367
|
-
type: "date",
|
|
8368
|
-
autoValue: "on_create",
|
|
8369
|
-
ui: {
|
|
8370
|
-
readOnly: true,
|
|
8371
|
-
hideFromCollection: true
|
|
8372
|
-
}
|
|
8373
|
-
},
|
|
8374
|
-
updated_at: {
|
|
8375
|
-
name: "Updated At",
|
|
8376
|
-
type: "date",
|
|
8377
|
-
autoValue: "on_update",
|
|
8378
|
-
ui: {
|
|
8379
|
-
readOnly: true,
|
|
8380
|
-
hideFromCollection: true
|
|
8381
|
-
}
|
|
8382
|
-
}
|
|
8383
|
-
}
|
|
8384
|
-
};
|
|
8385
|
-
const formatTerminalText = (text, options = {}) => {
|
|
8671
|
+
const formatTerminalText = (text2, options = {}) => {
|
|
8386
8672
|
let codes = "";
|
|
8387
8673
|
if (options.bold) codes += "\x1B[1m";
|
|
8388
8674
|
if (options.backgroundColor) {
|
|
@@ -8409,7 +8695,7 @@ const formatTerminalText = (text, options = {}) => {
|
|
|
8409
8695
|
};
|
|
8410
8696
|
codes += textColors[options.textColor];
|
|
8411
8697
|
}
|
|
8412
|
-
return `${codes}${
|
|
8698
|
+
return `${codes}${text2}\x1B[0m`;
|
|
8413
8699
|
};
|
|
8414
8700
|
const runGeneration = async (collectionsFilePath, outputPath) => {
|
|
8415
8701
|
try {
|
|
@@ -8447,10 +8733,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
|
|
|
8447
8733
|
if (!collections || !Array.isArray(collections)) {
|
|
8448
8734
|
collections = [];
|
|
8449
8735
|
}
|
|
8450
|
-
|
|
8451
|
-
if (!hasUsersCollection) {
|
|
8452
|
-
collections.push(defaultUsersCollection);
|
|
8453
|
-
}
|
|
8736
|
+
collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
|
|
8454
8737
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
8455
8738
|
const schemaContent = await generateSchema(collections);
|
|
8456
8739
|
if (outputPath) {
|
|
@@ -8511,6 +8794,13 @@ class RealtimeService extends EventEmitter {
|
|
|
8511
8794
|
this.entityService = new EntityService(db, registry);
|
|
8512
8795
|
}
|
|
8513
8796
|
clients = /* @__PURE__ */ new Map();
|
|
8797
|
+
// Broadcast channels: channel name → set of client IDs
|
|
8798
|
+
channels = /* @__PURE__ */ new Map();
|
|
8799
|
+
// Presence: channel → Map<clientId, { state, lastSeen }>
|
|
8800
|
+
presence = /* @__PURE__ */ new Map();
|
|
8801
|
+
presenceInterval;
|
|
8802
|
+
static PRESENCE_TIMEOUT_MS = 3e4;
|
|
8803
|
+
// 30s
|
|
8514
8804
|
entityService;
|
|
8515
8805
|
// Enhanced subscriptions storage with full request parameters
|
|
8516
8806
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -8637,8 +8927,19 @@ class RealtimeService extends EventEmitter {
|
|
|
8637
8927
|
}
|
|
8638
8928
|
}
|
|
8639
8929
|
}
|
|
8930
|
+
for (const [channel, members] of this.channels.entries()) {
|
|
8931
|
+
if (members.has(clientId)) {
|
|
8932
|
+
members.delete(clientId);
|
|
8933
|
+
this.removePresence(clientId, channel);
|
|
8934
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
8935
|
+
}
|
|
8936
|
+
}
|
|
8937
|
+
for (const [channel] of this.presence) {
|
|
8938
|
+
this.removePresence(clientId, channel);
|
|
8939
|
+
}
|
|
8640
8940
|
}
|
|
8641
8941
|
async handleMessage(clientId, message, authContext) {
|
|
8942
|
+
const payload = message.payload;
|
|
8642
8943
|
switch (message.type) {
|
|
8643
8944
|
case "subscribe_collection":
|
|
8644
8945
|
await this.handleCollectionSubscription(clientId, message.payload, authContext);
|
|
@@ -8649,6 +8950,25 @@ class RealtimeService extends EventEmitter {
|
|
|
8649
8950
|
case "unsubscribe":
|
|
8650
8951
|
await this.handleUnsubscribe(clientId, message.subscriptionId);
|
|
8651
8952
|
break;
|
|
8953
|
+
case "join_channel":
|
|
8954
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8955
|
+
break;
|
|
8956
|
+
case "leave_channel":
|
|
8957
|
+
this.leaveChannel(clientId, payload?.channel);
|
|
8958
|
+
break;
|
|
8959
|
+
case "broadcast":
|
|
8960
|
+
this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
|
|
8961
|
+
break;
|
|
8962
|
+
case "presence_track":
|
|
8963
|
+
this.joinChannel(clientId, payload?.channel);
|
|
8964
|
+
this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
|
|
8965
|
+
break;
|
|
8966
|
+
case "presence_untrack":
|
|
8967
|
+
this.removePresence(clientId, payload?.channel);
|
|
8968
|
+
break;
|
|
8969
|
+
case "presence_state":
|
|
8970
|
+
this.sendPresenceState(clientId, payload?.channel);
|
|
8971
|
+
break;
|
|
8652
8972
|
default:
|
|
8653
8973
|
this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
|
|
8654
8974
|
}
|
|
@@ -9106,6 +9426,132 @@ class RealtimeService extends EventEmitter {
|
|
|
9106
9426
|
return parentPaths;
|
|
9107
9427
|
}
|
|
9108
9428
|
// =============================================================================
|
|
9429
|
+
// Broadcast Channels
|
|
9430
|
+
// =============================================================================
|
|
9431
|
+
/** Join a broadcast channel */
|
|
9432
|
+
joinChannel(clientId, channel) {
|
|
9433
|
+
if (!this.channels.has(channel)) {
|
|
9434
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
9435
|
+
}
|
|
9436
|
+
this.channels.get(channel).add(clientId);
|
|
9437
|
+
this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
|
|
9438
|
+
}
|
|
9439
|
+
/** Leave a broadcast channel */
|
|
9440
|
+
leaveChannel(clientId, channel) {
|
|
9441
|
+
const members = this.channels.get(channel);
|
|
9442
|
+
if (members) {
|
|
9443
|
+
members.delete(clientId);
|
|
9444
|
+
if (members.size === 0) this.channels.delete(channel);
|
|
9445
|
+
}
|
|
9446
|
+
this.removePresence(clientId, channel);
|
|
9447
|
+
}
|
|
9448
|
+
/** Broadcast a message to all clients in a channel except sender */
|
|
9449
|
+
broadcastToChannel(clientId, channel, event, payload) {
|
|
9450
|
+
const members = this.channels.get(channel);
|
|
9451
|
+
if (!members) return;
|
|
9452
|
+
const message = JSON.stringify({
|
|
9453
|
+
type: "broadcast",
|
|
9454
|
+
channel,
|
|
9455
|
+
event,
|
|
9456
|
+
payload
|
|
9457
|
+
});
|
|
9458
|
+
for (const memberId of members) {
|
|
9459
|
+
if (memberId === clientId) continue;
|
|
9460
|
+
const ws = this.clients.get(memberId);
|
|
9461
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9462
|
+
ws.send(message);
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
// =============================================================================
|
|
9467
|
+
// Presence
|
|
9468
|
+
// =============================================================================
|
|
9469
|
+
/** Track presence in a channel */
|
|
9470
|
+
trackPresence(clientId, channel, state) {
|
|
9471
|
+
if (!this.presence.has(channel)) {
|
|
9472
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
9473
|
+
}
|
|
9474
|
+
const channelPresence = this.presence.get(channel);
|
|
9475
|
+
channelPresence.set(clientId, {
|
|
9476
|
+
state,
|
|
9477
|
+
lastSeen: Date.now()
|
|
9478
|
+
});
|
|
9479
|
+
this.broadcastPresenceDiff(channel, {
|
|
9480
|
+
[clientId]: state
|
|
9481
|
+
}, {});
|
|
9482
|
+
this.ensurePresenceCleanup();
|
|
9483
|
+
}
|
|
9484
|
+
/** Remove presence from a channel */
|
|
9485
|
+
removePresence(clientId, channel) {
|
|
9486
|
+
const channelPresence = this.presence.get(channel);
|
|
9487
|
+
if (!channelPresence) return;
|
|
9488
|
+
const entry = channelPresence.get(clientId);
|
|
9489
|
+
if (entry) {
|
|
9490
|
+
channelPresence.delete(clientId);
|
|
9491
|
+
this.broadcastPresenceDiff(channel, {}, {
|
|
9492
|
+
[clientId]: entry.state
|
|
9493
|
+
});
|
|
9494
|
+
}
|
|
9495
|
+
if (channelPresence.size === 0) {
|
|
9496
|
+
this.presence.delete(channel);
|
|
9497
|
+
}
|
|
9498
|
+
}
|
|
9499
|
+
/** Send full presence state to a specific client */
|
|
9500
|
+
sendPresenceState(clientId, channel) {
|
|
9501
|
+
const channelPresence = this.presence.get(channel);
|
|
9502
|
+
const presences = {};
|
|
9503
|
+
if (channelPresence) {
|
|
9504
|
+
for (const [id, {
|
|
9505
|
+
state
|
|
9506
|
+
}] of channelPresence) {
|
|
9507
|
+
presences[id] = state;
|
|
9508
|
+
}
|
|
9509
|
+
}
|
|
9510
|
+
const ws = this.clients.get(clientId);
|
|
9511
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9512
|
+
ws.send(JSON.stringify({
|
|
9513
|
+
type: "presence_state",
|
|
9514
|
+
channel,
|
|
9515
|
+
presences
|
|
9516
|
+
}));
|
|
9517
|
+
}
|
|
9518
|
+
}
|
|
9519
|
+
/** Broadcast presence diff (joins/leaves) to channel */
|
|
9520
|
+
broadcastPresenceDiff(channel, joins, leaves) {
|
|
9521
|
+
const members = this.channels.get(channel);
|
|
9522
|
+
if (!members) return;
|
|
9523
|
+
const message = JSON.stringify({
|
|
9524
|
+
type: "presence_diff",
|
|
9525
|
+
channel,
|
|
9526
|
+
joins,
|
|
9527
|
+
leaves
|
|
9528
|
+
});
|
|
9529
|
+
for (const memberId of members) {
|
|
9530
|
+
const ws = this.clients.get(memberId);
|
|
9531
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
9532
|
+
ws.send(message);
|
|
9533
|
+
}
|
|
9534
|
+
}
|
|
9535
|
+
}
|
|
9536
|
+
/** Periodic cleanup for stale presences */
|
|
9537
|
+
ensurePresenceCleanup() {
|
|
9538
|
+
if (this.presenceInterval) return;
|
|
9539
|
+
this.presenceInterval = setInterval(() => {
|
|
9540
|
+
const now = Date.now();
|
|
9541
|
+
for (const [channel, channelPresence] of this.presence) {
|
|
9542
|
+
for (const [clientId, entry] of channelPresence) {
|
|
9543
|
+
if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
|
|
9544
|
+
this.removePresence(clientId, channel);
|
|
9545
|
+
}
|
|
9546
|
+
}
|
|
9547
|
+
}
|
|
9548
|
+
if (this.presence.size === 0 && this.presenceInterval) {
|
|
9549
|
+
clearInterval(this.presenceInterval);
|
|
9550
|
+
this.presenceInterval = void 0;
|
|
9551
|
+
}
|
|
9552
|
+
}, 1e4);
|
|
9553
|
+
}
|
|
9554
|
+
// =============================================================================
|
|
9109
9555
|
// Lifecycle / Cleanup
|
|
9110
9556
|
// =============================================================================
|
|
9111
9557
|
/**
|
|
@@ -9126,6 +9572,12 @@ class RealtimeService extends EventEmitter {
|
|
|
9126
9572
|
}
|
|
9127
9573
|
this._subscriptions.clear();
|
|
9128
9574
|
this.subscriptionCallbacks.clear();
|
|
9575
|
+
this.channels.clear();
|
|
9576
|
+
this.presence.clear();
|
|
9577
|
+
if (this.presenceInterval) {
|
|
9578
|
+
clearInterval(this.presenceInterval);
|
|
9579
|
+
this.presenceInterval = void 0;
|
|
9580
|
+
}
|
|
9129
9581
|
await this.stopListening();
|
|
9130
9582
|
this.clients.clear();
|
|
9131
9583
|
this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
|
|
@@ -9627,15 +10079,15 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
9627
10079
|
wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
|
|
9628
10080
|
const delegate = await getScopedDelegate();
|
|
9629
10081
|
const admin = delegate.admin;
|
|
9630
|
-
let
|
|
10082
|
+
let roles = [];
|
|
9631
10083
|
if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {
|
|
9632
|
-
|
|
10084
|
+
roles = await admin.fetchAvailableRoles();
|
|
9633
10085
|
}
|
|
9634
|
-
wsDebug(`👤 [WebSocket Server] Fetched ${
|
|
10086
|
+
wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);
|
|
9635
10087
|
const response = {
|
|
9636
10088
|
type: "FETCH_ROLES_SUCCESS",
|
|
9637
10089
|
payload: {
|
|
9638
|
-
roles
|
|
10090
|
+
roles
|
|
9639
10091
|
},
|
|
9640
10092
|
requestId
|
|
9641
10093
|
};
|
|
@@ -9772,8 +10224,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
9772
10224
|
break;
|
|
9773
10225
|
case "subscribe_collection":
|
|
9774
10226
|
case "subscribe_entity":
|
|
9775
|
-
case "unsubscribe":
|
|
9776
|
-
|
|
10227
|
+
case "unsubscribe":
|
|
10228
|
+
case "join_channel":
|
|
10229
|
+
case "leave_channel":
|
|
10230
|
+
case "broadcast":
|
|
10231
|
+
case "presence_track":
|
|
10232
|
+
case "presence_untrack":
|
|
10233
|
+
case "presence_state": {
|
|
10234
|
+
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
9777
10235
|
const session = clientSessions.get(clientId);
|
|
9778
10236
|
const authContext = session?.user ? {
|
|
9779
10237
|
userId: session.user.userId,
|
|
@@ -9883,50 +10341,8 @@ class PostgresCollectionRegistry extends CollectionRegistry {
|
|
|
9883
10341
|
return collection.relations.map((r) => r.relationName || r.localKey || "").filter(Boolean);
|
|
9884
10342
|
}
|
|
9885
10343
|
}
|
|
9886
|
-
const DEFAULT_ROLES = [{
|
|
9887
|
-
id: "admin",
|
|
9888
|
-
name: "Admin",
|
|
9889
|
-
is_admin: true,
|
|
9890
|
-
default_permissions: {
|
|
9891
|
-
read: true,
|
|
9892
|
-
create: true,
|
|
9893
|
-
edit: true,
|
|
9894
|
-
delete: true
|
|
9895
|
-
},
|
|
9896
|
-
config: {
|
|
9897
|
-
createCollections: true,
|
|
9898
|
-
editCollections: "all",
|
|
9899
|
-
deleteCollections: "all"
|
|
9900
|
-
}
|
|
9901
|
-
}, {
|
|
9902
|
-
id: "editor",
|
|
9903
|
-
name: "Editor",
|
|
9904
|
-
is_admin: false,
|
|
9905
|
-
default_permissions: {
|
|
9906
|
-
read: true,
|
|
9907
|
-
create: true,
|
|
9908
|
-
edit: true,
|
|
9909
|
-
delete: true
|
|
9910
|
-
},
|
|
9911
|
-
config: {
|
|
9912
|
-
createCollections: true,
|
|
9913
|
-
editCollections: "own",
|
|
9914
|
-
deleteCollections: "own"
|
|
9915
|
-
}
|
|
9916
|
-
}, {
|
|
9917
|
-
id: "viewer",
|
|
9918
|
-
name: "Viewer",
|
|
9919
|
-
is_admin: false,
|
|
9920
|
-
default_permissions: {
|
|
9921
|
-
read: true,
|
|
9922
|
-
create: false,
|
|
9923
|
-
edit: false,
|
|
9924
|
-
delete: false
|
|
9925
|
-
},
|
|
9926
|
-
config: null
|
|
9927
|
-
}];
|
|
9928
10344
|
async function ensureAuthTablesExist(db, registry) {
|
|
9929
|
-
|
|
10345
|
+
logger.info("🔍 Checking auth tables...");
|
|
9930
10346
|
try {
|
|
9931
10347
|
let usersTableName = '"users"';
|
|
9932
10348
|
let userIdType = "TEXT";
|
|
@@ -9953,26 +10369,15 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
9953
10369
|
}
|
|
9954
10370
|
}
|
|
9955
10371
|
}
|
|
9956
|
-
let rolesSchema = "rebase";
|
|
9957
|
-
if (registry) {
|
|
9958
|
-
const rolesTable = registry.getTable("roles");
|
|
9959
|
-
if (rolesTable) {
|
|
9960
|
-
rolesSchema = getTableConfig(rolesTable).schema || "public";
|
|
9961
|
-
}
|
|
9962
|
-
}
|
|
9963
10372
|
if (usersSchema2 !== "public") {
|
|
9964
10373
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema2)}`);
|
|
9965
10374
|
}
|
|
9966
|
-
if (rolesSchema !== "public" && rolesSchema !== usersSchema2) {
|
|
9967
|
-
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
|
|
9968
|
-
}
|
|
9969
10375
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
9970
|
-
const
|
|
9971
|
-
const
|
|
9972
|
-
const
|
|
9973
|
-
const
|
|
9974
|
-
const
|
|
9975
|
-
const appConfigTableName = `"${rolesSchema}"."app_config"`;
|
|
10376
|
+
const authSchema = usersSchema2 === "public" ? "rebase" : usersSchema2;
|
|
10377
|
+
const userIdentitiesTable = `"${authSchema}"."user_identities"`;
|
|
10378
|
+
const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
|
|
10379
|
+
const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
|
|
10380
|
+
const appConfigTableName = `"${authSchema}"."app_config"`;
|
|
9976
10381
|
await db.execute(sql`
|
|
9977
10382
|
CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
|
|
9978
10383
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
@@ -9989,28 +10394,6 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
9989
10394
|
CREATE INDEX IF NOT EXISTS idx_user_identities_user
|
|
9990
10395
|
ON ${sql.raw(userIdentitiesTable)}(user_id)
|
|
9991
10396
|
`);
|
|
9992
|
-
await db.execute(sql`
|
|
9993
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
|
|
9994
|
-
id TEXT PRIMARY KEY,
|
|
9995
|
-
name TEXT NOT NULL,
|
|
9996
|
-
is_admin BOOLEAN DEFAULT FALSE,
|
|
9997
|
-
default_permissions JSONB,
|
|
9998
|
-
collection_permissions JSONB,
|
|
9999
|
-
config JSONB,
|
|
10000
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10001
|
-
)
|
|
10002
|
-
`);
|
|
10003
|
-
await db.execute(sql`
|
|
10004
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
|
|
10005
|
-
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10006
|
-
role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
|
|
10007
|
-
PRIMARY KEY (user_id, role_id)
|
|
10008
|
-
)
|
|
10009
|
-
`);
|
|
10010
|
-
await db.execute(sql`
|
|
10011
|
-
CREATE INDEX IF NOT EXISTS idx_user_roles_user
|
|
10012
|
-
ON ${sql.raw(userRolesTableName)}(user_id)
|
|
10013
|
-
`);
|
|
10014
10397
|
await db.execute(sql`
|
|
10015
10398
|
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
10016
10399
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
@@ -10078,35 +10461,93 @@ async function ensureAuthTablesExist(db, registry) {
|
|
|
10078
10461
|
$$ LANGUAGE sql STABLE
|
|
10079
10462
|
`);
|
|
10080
10463
|
});
|
|
10081
|
-
await seedDefaultRoles(db, rolesTableName);
|
|
10082
|
-
console.log("✅ Auth tables ready");
|
|
10083
|
-
} catch (error) {
|
|
10084
|
-
console.error("❌ Failed to create auth tables:", error);
|
|
10085
|
-
console.warn("⚠️ Continuing without creating auth tables.");
|
|
10086
|
-
}
|
|
10087
|
-
}
|
|
10088
|
-
async function seedDefaultRoles(db, rolesTableName) {
|
|
10089
|
-
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
10090
|
-
const count2 = parseInt(result.rows[0]?.count || "0", 10);
|
|
10091
|
-
if (count2 > 0) {
|
|
10092
|
-
console.log(`📋 Found ${count2} existing roles`);
|
|
10093
|
-
return;
|
|
10094
|
-
}
|
|
10095
|
-
console.log("🌱 Seeding default roles...");
|
|
10096
|
-
for (const role of DEFAULT_ROLES) {
|
|
10097
10464
|
await db.execute(sql`
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10465
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10466
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
10467
|
+
`);
|
|
10468
|
+
await db.execute(sql`
|
|
10469
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10470
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
10471
|
+
`);
|
|
10472
|
+
try {
|
|
10473
|
+
const legacyCheck = await db.execute(sql`
|
|
10474
|
+
SELECT EXISTS (
|
|
10475
|
+
SELECT 1 FROM information_schema.tables
|
|
10476
|
+
WHERE table_schema = 'rebase' AND table_name = 'user_roles'
|
|
10477
|
+
) AS has_user_roles
|
|
10478
|
+
`);
|
|
10479
|
+
const hasLegacyTables = legacyCheck.rows[0].has_user_roles;
|
|
10480
|
+
if (hasLegacyTables) {
|
|
10481
|
+
logger.info("🔄 Migrating roles from legacy user_roles table...");
|
|
10482
|
+
await db.execute(sql`
|
|
10483
|
+
UPDATE ${sql.raw(usersTableName)} u
|
|
10484
|
+
SET roles = COALESCE((
|
|
10485
|
+
SELECT array_agg(ur.role_id)
|
|
10486
|
+
FROM "rebase"."user_roles" ur
|
|
10487
|
+
WHERE ur.user_id = u.id
|
|
10488
|
+
), '{}')
|
|
10489
|
+
WHERE u.roles = '{}' OR u.roles IS NULL
|
|
10490
|
+
`);
|
|
10491
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
|
|
10492
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
|
|
10493
|
+
logger.info("✅ Legacy roles tables migrated and dropped");
|
|
10494
|
+
}
|
|
10495
|
+
} catch (migrationError) {
|
|
10496
|
+
logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
10497
|
+
}
|
|
10498
|
+
const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
|
|
10499
|
+
const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
|
|
10500
|
+
const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
|
|
10501
|
+
await db.execute(sql`
|
|
10502
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
|
|
10503
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10504
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10505
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
10506
|
+
secret_encrypted TEXT NOT NULL,
|
|
10507
|
+
friendly_name TEXT,
|
|
10508
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
10509
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10510
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10511
|
+
)
|
|
10512
|
+
`);
|
|
10513
|
+
await db.execute(sql`
|
|
10514
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
10515
|
+
ON ${sql.raw(mfaFactorsTableName)}(user_id)
|
|
10516
|
+
`);
|
|
10517
|
+
await db.execute(sql`
|
|
10518
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
|
|
10519
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10520
|
+
factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
10521
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
10522
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
10523
|
+
ip_address TEXT,
|
|
10524
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
10105
10525
|
)
|
|
10106
|
-
ON CONFLICT (id) DO NOTHING
|
|
10107
10526
|
`);
|
|
10527
|
+
await db.execute(sql`
|
|
10528
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
10529
|
+
ON ${sql.raw(mfaChallengesTableName)}(factor_id)
|
|
10530
|
+
`);
|
|
10531
|
+
await db.execute(sql`
|
|
10532
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
|
|
10533
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
10534
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
10535
|
+
code_hash TEXT NOT NULL,
|
|
10536
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
10537
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
10538
|
+
)
|
|
10539
|
+
`);
|
|
10540
|
+
await db.execute(sql`
|
|
10541
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
10542
|
+
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
10543
|
+
`);
|
|
10544
|
+
logger.info("✅ Auth tables ready");
|
|
10545
|
+
} catch (error) {
|
|
10546
|
+
logger.error("❌ Failed to create auth tables", {
|
|
10547
|
+
error
|
|
10548
|
+
});
|
|
10549
|
+
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
10108
10550
|
}
|
|
10109
|
-
console.log("✅ Default roles created: admin, editor, viewer");
|
|
10110
10551
|
}
|
|
10111
10552
|
function getColumnKey(table, ...keys2) {
|
|
10112
10553
|
if (!table) return void 0;
|
|
@@ -10127,24 +10568,18 @@ function getColumn(table, ...keys2) {
|
|
|
10127
10568
|
class UserService {
|
|
10128
10569
|
constructor(db, tableOrTables) {
|
|
10129
10570
|
this.db = db;
|
|
10130
|
-
if (tableOrTables &&
|
|
10571
|
+
if (tableOrTables && tableOrTables.users) {
|
|
10131
10572
|
const tables = tableOrTables;
|
|
10132
10573
|
this.usersTable = tables.users || users;
|
|
10133
10574
|
this.userIdentitiesTable = tables.userIdentities || userIdentities;
|
|
10134
|
-
this.userRolesTable = tables.userRoles || userRoles;
|
|
10135
|
-
this.rolesTable = tables.roles || roles;
|
|
10136
10575
|
} else {
|
|
10137
10576
|
const table = tableOrTables;
|
|
10138
10577
|
this.usersTable = table || users;
|
|
10139
10578
|
this.userIdentitiesTable = userIdentities;
|
|
10140
|
-
this.userRolesTable = userRoles;
|
|
10141
|
-
this.rolesTable = roles;
|
|
10142
10579
|
}
|
|
10143
10580
|
}
|
|
10144
10581
|
usersTable;
|
|
10145
10582
|
userIdentitiesTable;
|
|
10146
|
-
userRolesTable;
|
|
10147
|
-
rolesTable;
|
|
10148
10583
|
getQualifiedUsersTableName() {
|
|
10149
10584
|
const name = getTableName$1(this.usersTable);
|
|
10150
10585
|
const schema = getTableConfig(this.usersTable).schema || "public";
|
|
@@ -10160,12 +10595,13 @@ class UserService {
|
|
|
10160
10595
|
const emailVerified = row.email_verified ?? row.emailVerified ?? false;
|
|
10161
10596
|
const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
|
|
10162
10597
|
const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
|
|
10598
|
+
const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
|
|
10163
10599
|
const createdAt = row.created_at ?? row.createdAt;
|
|
10164
10600
|
const updatedAt = row.updated_at ?? row.updatedAt;
|
|
10165
10601
|
const metadata = {
|
|
10166
10602
|
...row.metadata || {}
|
|
10167
10603
|
};
|
|
10168
|
-
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10604
|
+
const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "roles", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
|
|
10169
10605
|
for (const [key, val] of Object.entries(row)) {
|
|
10170
10606
|
if (!knownKeys.has(key)) {
|
|
10171
10607
|
const camelKey = camelCase(key);
|
|
@@ -10181,6 +10617,7 @@ class UserService {
|
|
|
10181
10617
|
emailVerified,
|
|
10182
10618
|
emailVerificationToken,
|
|
10183
10619
|
emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
|
|
10620
|
+
isAnonymous,
|
|
10184
10621
|
createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
|
|
10185
10622
|
updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
|
|
10186
10623
|
metadata
|
|
@@ -10197,6 +10634,7 @@ class UserService {
|
|
|
10197
10634
|
const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
10198
10635
|
const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
10199
10636
|
const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
10637
|
+
const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
|
|
10200
10638
|
const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
|
|
10201
10639
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
10202
10640
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
@@ -10208,6 +10646,7 @@ class UserService {
|
|
|
10208
10646
|
if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
|
|
10209
10647
|
if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
|
|
10210
10648
|
if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
|
|
10649
|
+
if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
|
|
10211
10650
|
if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
|
|
10212
10651
|
if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
|
|
10213
10652
|
const metadata = {
|
|
@@ -10216,7 +10655,7 @@ class UserService {
|
|
|
10216
10655
|
const remainingMetadata = {};
|
|
10217
10656
|
for (const [key, val] of Object.entries(metadata)) {
|
|
10218
10657
|
const tableColKey = getColumnKey(this.usersTable, key);
|
|
10219
|
-
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10658
|
+
if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
|
|
10220
10659
|
payload[tableColKey] = val;
|
|
10221
10660
|
} else {
|
|
10222
10661
|
remainingMetadata[key] = val;
|
|
@@ -10313,19 +10752,18 @@ class UserService {
|
|
|
10313
10752
|
const displayNameCol = getColumn(this.usersTable, "displayName", "display_name");
|
|
10314
10753
|
const displayNameColumn = displayNameCol ? displayNameCol.name : "display_name";
|
|
10315
10754
|
const idCol = getColumn(this.usersTable, "id");
|
|
10316
|
-
|
|
10755
|
+
idCol ? idCol.name : "id";
|
|
10317
10756
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
10318
|
-
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
10319
10757
|
const conditions = [];
|
|
10320
10758
|
if (roleId) {
|
|
10321
|
-
conditions.push(sql
|
|
10759
|
+
conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
|
|
10322
10760
|
}
|
|
10323
10761
|
if (search) {
|
|
10324
10762
|
const pattern = `%${search}%`;
|
|
10325
10763
|
conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);
|
|
10326
10764
|
}
|
|
10327
10765
|
const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
|
10328
|
-
const orderByClause = roleId ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY (
|
|
10766
|
+
const orderByClause = roleId ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}` : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
|
|
10329
10767
|
const countResult = await this.db.execute(sql`
|
|
10330
10768
|
SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
|
|
10331
10769
|
${whereClause}
|
|
@@ -10399,55 +10837,57 @@ class UserService {
|
|
|
10399
10837
|
return row ? this.mapRowToUser(row) : null;
|
|
10400
10838
|
}
|
|
10401
10839
|
/**
|
|
10402
|
-
* Get roles for a user from database
|
|
10840
|
+
* Get roles for a user from database (inline TEXT[] column)
|
|
10403
10841
|
*/
|
|
10404
10842
|
async getUserRoles(userId) {
|
|
10405
|
-
const
|
|
10843
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
10406
10844
|
const result = await this.db.execute(sql`
|
|
10407
|
-
SELECT
|
|
10408
|
-
FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
|
|
10409
|
-
INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
|
|
10410
|
-
WHERE ur.user_id = ${userId}
|
|
10845
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
|
|
10411
10846
|
`);
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10847
|
+
if (result.rows.length === 0) return [];
|
|
10848
|
+
const row = result.rows[0];
|
|
10849
|
+
const roleIds = row.roles ?? [];
|
|
10850
|
+
return roleIds.map((id) => ({
|
|
10851
|
+
id,
|
|
10852
|
+
name: id,
|
|
10853
|
+
isAdmin: id === "admin",
|
|
10854
|
+
defaultPermissions: null,
|
|
10855
|
+
collectionPermissions: null
|
|
10419
10856
|
}));
|
|
10420
10857
|
}
|
|
10421
10858
|
/**
|
|
10422
10859
|
* Get role IDs for a user
|
|
10423
10860
|
*/
|
|
10424
10861
|
async getUserRoleIds(userId) {
|
|
10425
|
-
const
|
|
10426
|
-
|
|
10862
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
10863
|
+
const result = await this.db.execute(sql`
|
|
10864
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
|
|
10865
|
+
`);
|
|
10866
|
+
if (result.rows.length === 0) return [];
|
|
10867
|
+
const row = result.rows[0];
|
|
10868
|
+
return row.roles ?? [];
|
|
10427
10869
|
}
|
|
10428
10870
|
/**
|
|
10429
|
-
* Set roles for a user
|
|
10871
|
+
* Set roles for a user (replaces existing roles)
|
|
10430
10872
|
*/
|
|
10431
10873
|
async setUserRoles(userId, roleIds) {
|
|
10432
|
-
const
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
`);
|
|
10440
|
-
}
|
|
10874
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
10875
|
+
const rolesArray = `{${roleIds.join(",")}}`;
|
|
10876
|
+
await this.db.execute(sql`
|
|
10877
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
10878
|
+
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
10879
|
+
WHERE id = ${userId}
|
|
10880
|
+
`);
|
|
10441
10881
|
}
|
|
10442
10882
|
/**
|
|
10443
|
-
* Assign a specific role to new user
|
|
10883
|
+
* Assign a specific role to new user (appends if not present)
|
|
10444
10884
|
*/
|
|
10445
10885
|
async assignDefaultRole(userId, roleId) {
|
|
10446
|
-
const
|
|
10886
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
10447
10887
|
await this.db.execute(sql`
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10888
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
10889
|
+
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
10890
|
+
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
10451
10891
|
`);
|
|
10452
10892
|
}
|
|
10453
10893
|
/**
|
|
@@ -10456,107 +10896,13 @@ class UserService {
|
|
|
10456
10896
|
async getUserWithRoles(userId) {
|
|
10457
10897
|
const user = await this.getUserById(userId);
|
|
10458
10898
|
if (!user) return null;
|
|
10459
|
-
const
|
|
10899
|
+
const roles = await this.getUserRoles(userId);
|
|
10460
10900
|
return {
|
|
10461
10901
|
user,
|
|
10462
|
-
roles
|
|
10902
|
+
roles
|
|
10463
10903
|
};
|
|
10464
10904
|
}
|
|
10465
10905
|
}
|
|
10466
|
-
class RoleService {
|
|
10467
|
-
constructor(db, tableOrTables) {
|
|
10468
|
-
this.db = db;
|
|
10469
|
-
if (tableOrTables && (tableOrTables.roles || tableOrTables.users)) {
|
|
10470
|
-
this.rolesTable = tableOrTables.roles || roles;
|
|
10471
|
-
} else {
|
|
10472
|
-
this.rolesTable = tableOrTables || roles;
|
|
10473
|
-
}
|
|
10474
|
-
}
|
|
10475
|
-
rolesTable;
|
|
10476
|
-
getQualifiedRolesTableName() {
|
|
10477
|
-
const name = getTableName$1(this.rolesTable);
|
|
10478
|
-
const schema = getTableConfig(this.rolesTable).schema || "public";
|
|
10479
|
-
return `"${schema}"."${name}"`;
|
|
10480
|
-
}
|
|
10481
|
-
async getRoleById(id) {
|
|
10482
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
10483
|
-
const result = await this.db.execute(sql`
|
|
10484
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
10485
|
-
FROM ${sql.raw(tableName)}
|
|
10486
|
-
WHERE id = ${id}
|
|
10487
|
-
`);
|
|
10488
|
-
if (result.rows.length === 0) return null;
|
|
10489
|
-
const row = result.rows[0];
|
|
10490
|
-
return {
|
|
10491
|
-
id: row.id,
|
|
10492
|
-
name: row.name,
|
|
10493
|
-
isAdmin: row.is_admin,
|
|
10494
|
-
defaultPermissions: row.default_permissions,
|
|
10495
|
-
collectionPermissions: row.collection_permissions,
|
|
10496
|
-
config: row.config
|
|
10497
|
-
};
|
|
10498
|
-
}
|
|
10499
|
-
async listRoles() {
|
|
10500
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
10501
|
-
const result = await this.db.execute(sql`
|
|
10502
|
-
SELECT id, name, is_admin, default_permissions, collection_permissions, config
|
|
10503
|
-
FROM ${sql.raw(tableName)}
|
|
10504
|
-
ORDER BY name
|
|
10505
|
-
`);
|
|
10506
|
-
return result.rows.map((row) => ({
|
|
10507
|
-
id: row.id,
|
|
10508
|
-
name: row.name,
|
|
10509
|
-
isAdmin: row.is_admin,
|
|
10510
|
-
defaultPermissions: row.default_permissions,
|
|
10511
|
-
collectionPermissions: row.collection_permissions,
|
|
10512
|
-
config: row.config
|
|
10513
|
-
}));
|
|
10514
|
-
}
|
|
10515
|
-
async createRole(data) {
|
|
10516
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
10517
|
-
const result = await this.db.execute(sql`
|
|
10518
|
-
INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
|
|
10519
|
-
VALUES (
|
|
10520
|
-
${data.id},
|
|
10521
|
-
${data.name},
|
|
10522
|
-
${data.isAdmin ?? false},
|
|
10523
|
-
${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
|
|
10524
|
-
${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
|
|
10525
|
-
${data.config ? JSON.stringify(data.config) : null}::jsonb
|
|
10526
|
-
)
|
|
10527
|
-
RETURNING id, name, is_admin, default_permissions, collection_permissions, config
|
|
10528
|
-
`);
|
|
10529
|
-
const row = result.rows[0];
|
|
10530
|
-
return {
|
|
10531
|
-
id: row.id,
|
|
10532
|
-
name: row.name,
|
|
10533
|
-
isAdmin: row.is_admin,
|
|
10534
|
-
defaultPermissions: row.default_permissions,
|
|
10535
|
-
collectionPermissions: row.collection_permissions,
|
|
10536
|
-
config: row.config
|
|
10537
|
-
};
|
|
10538
|
-
}
|
|
10539
|
-
async updateRole(id, data) {
|
|
10540
|
-
const existing = await this.getRoleById(id);
|
|
10541
|
-
if (!existing) return null;
|
|
10542
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
10543
|
-
await this.db.execute(sql`
|
|
10544
|
-
UPDATE ${sql.raw(tableName)}
|
|
10545
|
-
SET
|
|
10546
|
-
name = ${data.name ?? existing.name},
|
|
10547
|
-
is_admin = ${data.isAdmin ?? existing.isAdmin},
|
|
10548
|
-
default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
|
|
10549
|
-
collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
|
|
10550
|
-
config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
|
|
10551
|
-
WHERE id = ${id}
|
|
10552
|
-
`);
|
|
10553
|
-
return this.getRoleById(id);
|
|
10554
|
-
}
|
|
10555
|
-
async deleteRole(id) {
|
|
10556
|
-
const tableName = this.getQualifiedRolesTableName();
|
|
10557
|
-
await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
|
|
10558
|
-
}
|
|
10559
|
-
}
|
|
10560
10906
|
class RefreshTokenService {
|
|
10561
10907
|
constructor(db, tableOrTables) {
|
|
10562
10908
|
this.db = db;
|
|
@@ -10751,11 +11097,9 @@ class PostgresAuthRepository {
|
|
|
10751
11097
|
constructor(db, tableOrTables) {
|
|
10752
11098
|
this.db = db;
|
|
10753
11099
|
this.userService = new UserService(db, tableOrTables);
|
|
10754
|
-
this.roleService = new RoleService(db, tableOrTables);
|
|
10755
11100
|
this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
|
|
10756
11101
|
}
|
|
10757
11102
|
userService;
|
|
10758
|
-
roleService;
|
|
10759
11103
|
tokenRepository;
|
|
10760
11104
|
// User operations (delegate to UserService)
|
|
10761
11105
|
async createUser(data) {
|
|
@@ -10815,26 +11159,56 @@ class PostgresAuthRepository {
|
|
|
10815
11159
|
async getUserWithRoles(userId) {
|
|
10816
11160
|
return this.userService.getUserWithRoles(userId);
|
|
10817
11161
|
}
|
|
10818
|
-
// Role operations (
|
|
11162
|
+
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
10819
11163
|
async getRoleById(id) {
|
|
10820
|
-
return
|
|
11164
|
+
return {
|
|
11165
|
+
id,
|
|
11166
|
+
name: id,
|
|
11167
|
+
isAdmin: id === "admin",
|
|
11168
|
+
defaultPermissions: null,
|
|
11169
|
+
collectionPermissions: null
|
|
11170
|
+
};
|
|
10821
11171
|
}
|
|
10822
11172
|
async listRoles() {
|
|
10823
|
-
return
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
11173
|
+
return [{
|
|
11174
|
+
id: "admin",
|
|
11175
|
+
name: "Admin",
|
|
11176
|
+
isAdmin: true,
|
|
11177
|
+
defaultPermissions: null,
|
|
11178
|
+
collectionPermissions: null
|
|
11179
|
+
}, {
|
|
11180
|
+
id: "editor",
|
|
11181
|
+
name: "Editor",
|
|
11182
|
+
isAdmin: false,
|
|
11183
|
+
defaultPermissions: null,
|
|
11184
|
+
collectionPermissions: null
|
|
11185
|
+
}, {
|
|
11186
|
+
id: "viewer",
|
|
11187
|
+
name: "Viewer",
|
|
11188
|
+
isAdmin: false,
|
|
11189
|
+
defaultPermissions: null,
|
|
11190
|
+
collectionPermissions: null
|
|
11191
|
+
}];
|
|
11192
|
+
}
|
|
11193
|
+
async createRole(_data) {
|
|
11194
|
+
return {
|
|
11195
|
+
id: _data.id,
|
|
11196
|
+
name: _data.name,
|
|
11197
|
+
isAdmin: _data.isAdmin ?? false,
|
|
11198
|
+
defaultPermissions: _data.defaultPermissions ?? null,
|
|
11199
|
+
collectionPermissions: _data.collectionPermissions ?? null
|
|
11200
|
+
};
|
|
10832
11201
|
}
|
|
10833
11202
|
async updateRole(id, data) {
|
|
10834
|
-
return
|
|
11203
|
+
return {
|
|
11204
|
+
id,
|
|
11205
|
+
name: data.name ?? id,
|
|
11206
|
+
isAdmin: data.isAdmin ?? id === "admin",
|
|
11207
|
+
defaultPermissions: data.defaultPermissions ?? null,
|
|
11208
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
11209
|
+
};
|
|
10835
11210
|
}
|
|
10836
|
-
async deleteRole(
|
|
10837
|
-
await this.roleService.deleteRole(id);
|
|
11211
|
+
async deleteRole(_id) {
|
|
10838
11212
|
}
|
|
10839
11213
|
// Token operations (delegate to PostgresTokenRepository)
|
|
10840
11214
|
async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
@@ -10870,6 +11244,219 @@ class PostgresAuthRepository {
|
|
|
10870
11244
|
async deleteExpiredTokens() {
|
|
10871
11245
|
await this.tokenRepository.deleteExpiredTokens();
|
|
10872
11246
|
}
|
|
11247
|
+
// MFA operations (delegate to MfaService)
|
|
11248
|
+
_mfaService = null;
|
|
11249
|
+
getMfaService() {
|
|
11250
|
+
if (!this._mfaService) {
|
|
11251
|
+
this._mfaService = new MfaService(this.db);
|
|
11252
|
+
}
|
|
11253
|
+
return this._mfaService;
|
|
11254
|
+
}
|
|
11255
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11256
|
+
return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
|
|
11257
|
+
}
|
|
11258
|
+
async getMfaFactors(userId) {
|
|
11259
|
+
return this.getMfaService().getMfaFactors(userId);
|
|
11260
|
+
}
|
|
11261
|
+
async getMfaFactorById(factorId) {
|
|
11262
|
+
return this.getMfaService().getMfaFactorById(factorId);
|
|
11263
|
+
}
|
|
11264
|
+
async verifyMfaFactor(factorId) {
|
|
11265
|
+
return this.getMfaService().verifyMfaFactor(factorId);
|
|
11266
|
+
}
|
|
11267
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11268
|
+
return this.getMfaService().deleteMfaFactor(factorId, userId);
|
|
11269
|
+
}
|
|
11270
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11271
|
+
return this.getMfaService().createMfaChallenge(factorId, ipAddress);
|
|
11272
|
+
}
|
|
11273
|
+
async getMfaChallengeById(challengeId) {
|
|
11274
|
+
return this.getMfaService().getMfaChallengeById(challengeId);
|
|
11275
|
+
}
|
|
11276
|
+
async verifyMfaChallenge(challengeId) {
|
|
11277
|
+
return this.getMfaService().verifyMfaChallenge(challengeId);
|
|
11278
|
+
}
|
|
11279
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11280
|
+
return this.getMfaService().createRecoveryCodes(userId, codeHashes);
|
|
11281
|
+
}
|
|
11282
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11283
|
+
return this.getMfaService().useRecoveryCode(userId, codeHash);
|
|
11284
|
+
}
|
|
11285
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11286
|
+
return this.getMfaService().getUnusedRecoveryCodeCount(userId);
|
|
11287
|
+
}
|
|
11288
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11289
|
+
return this.getMfaService().deleteAllRecoveryCodes(userId);
|
|
11290
|
+
}
|
|
11291
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11292
|
+
return this.getMfaService().hasVerifiedMfaFactors(userId);
|
|
11293
|
+
}
|
|
11294
|
+
}
|
|
11295
|
+
class MfaService {
|
|
11296
|
+
constructor(db, schemaName = "rebase") {
|
|
11297
|
+
this.db = db;
|
|
11298
|
+
this.schemaName = schemaName;
|
|
11299
|
+
}
|
|
11300
|
+
qualify(tableName) {
|
|
11301
|
+
return `"${this.schemaName}"."${tableName}"`;
|
|
11302
|
+
}
|
|
11303
|
+
async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
|
|
11304
|
+
const tableName = this.qualify("mfa_factors");
|
|
11305
|
+
const result = await this.db.execute(sql`
|
|
11306
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
|
|
11307
|
+
VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
|
|
11308
|
+
RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11309
|
+
`);
|
|
11310
|
+
const row = result.rows[0];
|
|
11311
|
+
return {
|
|
11312
|
+
id: row.id,
|
|
11313
|
+
userId: row.user_id,
|
|
11314
|
+
factorType: row.factor_type,
|
|
11315
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11316
|
+
verified: row.verified,
|
|
11317
|
+
createdAt: new Date(row.created_at),
|
|
11318
|
+
updatedAt: new Date(row.updated_at)
|
|
11319
|
+
};
|
|
11320
|
+
}
|
|
11321
|
+
async getMfaFactors(userId) {
|
|
11322
|
+
const tableName = this.qualify("mfa_factors");
|
|
11323
|
+
const result = await this.db.execute(sql`
|
|
11324
|
+
SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
|
|
11325
|
+
FROM ${sql.raw(tableName)}
|
|
11326
|
+
WHERE user_id = ${userId}
|
|
11327
|
+
ORDER BY created_at
|
|
11328
|
+
`);
|
|
11329
|
+
return result.rows.map((row) => ({
|
|
11330
|
+
id: row.id,
|
|
11331
|
+
userId: row.user_id,
|
|
11332
|
+
factorType: row.factor_type,
|
|
11333
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11334
|
+
verified: row.verified,
|
|
11335
|
+
createdAt: new Date(row.created_at),
|
|
11336
|
+
updatedAt: new Date(row.updated_at)
|
|
11337
|
+
}));
|
|
11338
|
+
}
|
|
11339
|
+
async getMfaFactorById(factorId) {
|
|
11340
|
+
const tableName = this.qualify("mfa_factors");
|
|
11341
|
+
const result = await this.db.execute(sql`
|
|
11342
|
+
SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
|
|
11343
|
+
FROM ${sql.raw(tableName)}
|
|
11344
|
+
WHERE id = ${factorId}
|
|
11345
|
+
`);
|
|
11346
|
+
if (result.rows.length === 0) return null;
|
|
11347
|
+
const row = result.rows[0];
|
|
11348
|
+
return {
|
|
11349
|
+
id: row.id,
|
|
11350
|
+
userId: row.user_id,
|
|
11351
|
+
factorType: row.factor_type,
|
|
11352
|
+
secretEncrypted: row.secret_encrypted,
|
|
11353
|
+
friendlyName: row.friendly_name ?? void 0,
|
|
11354
|
+
verified: row.verified,
|
|
11355
|
+
createdAt: new Date(row.created_at),
|
|
11356
|
+
updatedAt: new Date(row.updated_at)
|
|
11357
|
+
};
|
|
11358
|
+
}
|
|
11359
|
+
async verifyMfaFactor(factorId) {
|
|
11360
|
+
const tableName = this.qualify("mfa_factors");
|
|
11361
|
+
await this.db.execute(sql`
|
|
11362
|
+
UPDATE ${sql.raw(tableName)}
|
|
11363
|
+
SET verified = TRUE, updated_at = NOW()
|
|
11364
|
+
WHERE id = ${factorId}
|
|
11365
|
+
`);
|
|
11366
|
+
}
|
|
11367
|
+
async deleteMfaFactor(factorId, userId) {
|
|
11368
|
+
const tableName = this.qualify("mfa_factors");
|
|
11369
|
+
await this.db.execute(sql`
|
|
11370
|
+
DELETE FROM ${sql.raw(tableName)}
|
|
11371
|
+
WHERE id = ${factorId} AND user_id = ${userId}
|
|
11372
|
+
`);
|
|
11373
|
+
}
|
|
11374
|
+
async createMfaChallenge(factorId, ipAddress) {
|
|
11375
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11376
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
|
|
11377
|
+
const result = await this.db.execute(sql`
|
|
11378
|
+
INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
|
|
11379
|
+
VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
|
|
11380
|
+
RETURNING id, factor_id, created_at, verified_at, ip_address
|
|
11381
|
+
`);
|
|
11382
|
+
const row = result.rows[0];
|
|
11383
|
+
return {
|
|
11384
|
+
id: row.id,
|
|
11385
|
+
factorId: row.factor_id,
|
|
11386
|
+
createdAt: new Date(row.created_at),
|
|
11387
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11388
|
+
ipAddress: row.ip_address ?? void 0
|
|
11389
|
+
};
|
|
11390
|
+
}
|
|
11391
|
+
async getMfaChallengeById(challengeId) {
|
|
11392
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11393
|
+
const result = await this.db.execute(sql`
|
|
11394
|
+
SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
|
|
11395
|
+
FROM ${sql.raw(tableName)}
|
|
11396
|
+
WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
|
|
11397
|
+
`);
|
|
11398
|
+
if (result.rows.length === 0) return null;
|
|
11399
|
+
const row = result.rows[0];
|
|
11400
|
+
return {
|
|
11401
|
+
id: row.id,
|
|
11402
|
+
factorId: row.factor_id,
|
|
11403
|
+
createdAt: new Date(row.created_at),
|
|
11404
|
+
verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
|
|
11405
|
+
ipAddress: row.ip_address ?? void 0
|
|
11406
|
+
};
|
|
11407
|
+
}
|
|
11408
|
+
async verifyMfaChallenge(challengeId) {
|
|
11409
|
+
const tableName = this.qualify("mfa_challenges");
|
|
11410
|
+
await this.db.execute(sql`
|
|
11411
|
+
UPDATE ${sql.raw(tableName)}
|
|
11412
|
+
SET verified_at = NOW()
|
|
11413
|
+
WHERE id = ${challengeId}
|
|
11414
|
+
`);
|
|
11415
|
+
}
|
|
11416
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
11417
|
+
const tableName = this.qualify("recovery_codes");
|
|
11418
|
+
await this.db.execute(sql`
|
|
11419
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11420
|
+
`);
|
|
11421
|
+
for (const hash of codeHashes) {
|
|
11422
|
+
await this.db.execute(sql`
|
|
11423
|
+
INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
|
|
11424
|
+
VALUES (${userId}, ${hash})
|
|
11425
|
+
`);
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
async useRecoveryCode(userId, codeHash) {
|
|
11429
|
+
const tableName = this.qualify("recovery_codes");
|
|
11430
|
+
const result = await this.db.execute(sql`
|
|
11431
|
+
UPDATE ${sql.raw(tableName)}
|
|
11432
|
+
SET used_at = NOW()
|
|
11433
|
+
WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
|
|
11434
|
+
RETURNING id
|
|
11435
|
+
`);
|
|
11436
|
+
return result.rows.length > 0;
|
|
11437
|
+
}
|
|
11438
|
+
async getUnusedRecoveryCodeCount(userId) {
|
|
11439
|
+
const tableName = this.qualify("recovery_codes");
|
|
11440
|
+
const result = await this.db.execute(sql`
|
|
11441
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11442
|
+
WHERE user_id = ${userId} AND used_at IS NULL
|
|
11443
|
+
`);
|
|
11444
|
+
return result.rows[0].count;
|
|
11445
|
+
}
|
|
11446
|
+
async deleteAllRecoveryCodes(userId) {
|
|
11447
|
+
const tableName = this.qualify("recovery_codes");
|
|
11448
|
+
await this.db.execute(sql`
|
|
11449
|
+
DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
|
|
11450
|
+
`);
|
|
11451
|
+
}
|
|
11452
|
+
async hasVerifiedMfaFactors(userId) {
|
|
11453
|
+
const tableName = this.qualify("mfa_factors");
|
|
11454
|
+
const result = await this.db.execute(sql`
|
|
11455
|
+
SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
|
|
11456
|
+
WHERE user_id = ${userId} AND verified = TRUE
|
|
11457
|
+
`);
|
|
11458
|
+
return result.rows[0].count > 0;
|
|
11459
|
+
}
|
|
10873
11460
|
}
|
|
10874
11461
|
const DEFAULT_RETENTION = {
|
|
10875
11462
|
maxEntries: 200,
|
|
@@ -11080,7 +11667,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11080
11667
|
const registry = new PostgresCollectionRegistry();
|
|
11081
11668
|
if (collections) {
|
|
11082
11669
|
registry.registerMultiple(collections);
|
|
11083
|
-
|
|
11670
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
11084
11671
|
}
|
|
11085
11672
|
if (pgConfig.schema?.tables) {
|
|
11086
11673
|
Object.values(pgConfig.schema.tables).forEach((table) => {
|
|
@@ -11106,10 +11693,28 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11106
11693
|
try {
|
|
11107
11694
|
await schemaAwareDb.execute(sql`SELECT 1`);
|
|
11108
11695
|
} catch (err) {
|
|
11109
|
-
|
|
11110
|
-
|
|
11696
|
+
logger.error("❌ Failed to connect to PostgreSQL", {
|
|
11697
|
+
error: err
|
|
11698
|
+
});
|
|
11699
|
+
logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
11111
11700
|
}
|
|
11112
11701
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11702
|
+
let readDb;
|
|
11703
|
+
const readUrl = process.env.DATABASE_READ_URL;
|
|
11704
|
+
if (readUrl && readUrl !== pgConfig.connectionString) {
|
|
11705
|
+
try {
|
|
11706
|
+
const {
|
|
11707
|
+
createReadReplicaConnection: createReadReplicaConnection2
|
|
11708
|
+
} = await Promise.resolve().then(() => connection);
|
|
11709
|
+
const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
|
|
11710
|
+
readDb = readResources.db;
|
|
11711
|
+
logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11712
|
+
} catch (err) {
|
|
11713
|
+
logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
|
|
11714
|
+
error: err
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11113
11718
|
const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
|
|
11114
11719
|
const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
|
|
11115
11720
|
realtimeService.setDataDriver(driver);
|
|
@@ -11117,18 +11722,24 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11117
11722
|
try {
|
|
11118
11723
|
await driver.branchService.ensureBranchMetadataTable();
|
|
11119
11724
|
} catch (err) {
|
|
11120
|
-
|
|
11725
|
+
logger.warn("⚠️ Could not initialize branch metadata table", {
|
|
11726
|
+
error: err
|
|
11727
|
+
});
|
|
11121
11728
|
}
|
|
11122
11729
|
}
|
|
11123
|
-
|
|
11730
|
+
const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
|
|
11731
|
+
if (directUrl) {
|
|
11124
11732
|
try {
|
|
11125
|
-
await realtimeService.startListening(
|
|
11733
|
+
await realtimeService.startListening(directUrl);
|
|
11126
11734
|
} catch (err) {
|
|
11127
|
-
|
|
11735
|
+
logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
11736
|
+
error: err
|
|
11737
|
+
});
|
|
11128
11738
|
}
|
|
11129
11739
|
}
|
|
11130
11740
|
const internals = {
|
|
11131
11741
|
db: schemaAwareDb,
|
|
11742
|
+
readDb,
|
|
11132
11743
|
registry,
|
|
11133
11744
|
realtimeService,
|
|
11134
11745
|
driver,
|
|
@@ -11153,28 +11764,19 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11153
11764
|
emailService = createEmailService(authConfig.email);
|
|
11154
11765
|
}
|
|
11155
11766
|
const customUsersTable = registry?.getTable("users");
|
|
11156
|
-
const customRolesTable = registry?.getTable("roles");
|
|
11157
11767
|
let usersSchemaName = "rebase";
|
|
11158
|
-
let rolesSchemaName = "rebase";
|
|
11159
11768
|
if (customUsersTable) {
|
|
11160
11769
|
usersSchemaName = getTableConfig(customUsersTable).schema || "public";
|
|
11161
11770
|
}
|
|
11162
|
-
|
|
11163
|
-
rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
|
|
11164
|
-
}
|
|
11165
|
-
const authTables = createAuthSchema(rolesSchemaName, usersSchemaName);
|
|
11771
|
+
const authTables = createAuthSchema(usersSchemaName);
|
|
11166
11772
|
if (customUsersTable) {
|
|
11167
11773
|
authTables.users = customUsersTable;
|
|
11168
11774
|
}
|
|
11169
|
-
if (customRolesTable) {
|
|
11170
|
-
authTables.roles = customRolesTable;
|
|
11171
|
-
}
|
|
11172
11775
|
const userService = new UserService(db, authTables);
|
|
11173
|
-
const roleService = new RoleService(db, authTables);
|
|
11174
11776
|
const authRepository = new PostgresAuthRepository(db, authTables);
|
|
11175
11777
|
return {
|
|
11176
11778
|
userService,
|
|
11177
|
-
roleService,
|
|
11779
|
+
roleService: userService,
|
|
11178
11780
|
emailService,
|
|
11179
11781
|
authRepository
|
|
11180
11782
|
};
|
|
@@ -11266,22 +11868,25 @@ export {
|
|
|
11266
11868
|
RealtimeService,
|
|
11267
11869
|
appConfig,
|
|
11268
11870
|
createAuthSchema,
|
|
11871
|
+
createDirectDatabaseConnection,
|
|
11269
11872
|
createPostgresAdapter,
|
|
11270
11873
|
createPostgresBootstrapper,
|
|
11271
11874
|
createPostgresDatabaseConnection,
|
|
11272
11875
|
createPostgresWebSocket,
|
|
11876
|
+
createReadReplicaConnection,
|
|
11273
11877
|
generateSchema,
|
|
11878
|
+
mfaChallenges,
|
|
11879
|
+
mfaChallengesRelations,
|
|
11880
|
+
mfaFactors,
|
|
11881
|
+
mfaFactorsRelations,
|
|
11274
11882
|
passwordResetTokens,
|
|
11275
11883
|
passwordResetTokensRelations,
|
|
11276
|
-
|
|
11884
|
+
recoveryCodes,
|
|
11885
|
+
recoveryCodesRelations,
|
|
11277
11886
|
refreshTokens,
|
|
11278
11887
|
refreshTokensRelations,
|
|
11279
|
-
roles,
|
|
11280
|
-
rolesRelations,
|
|
11281
11888
|
userIdentities,
|
|
11282
11889
|
userIdentitiesRelations,
|
|
11283
|
-
userRoles,
|
|
11284
|
-
userRolesRelations,
|
|
11285
11890
|
users,
|
|
11286
11891
|
usersRelations,
|
|
11287
11892
|
usersSchema
|