@rebasepro/server-core 0.1.2 → 0.2.1
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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
- package/dist/index-BZoAtuqi.js.map +1 -0
- package/dist/index.es.js +15851 -15065
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15825 -15035
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
- package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
- package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
- package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/server-core/src/auth/index.d.ts +7 -0
- package/dist/server-core/src/auth/interfaces.d.ts +2 -0
- package/dist/server-core/src/auth/middleware.d.ts +18 -0
- package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +7 -1
- package/dist/server-core/src/env.d.ts +131 -0
- package/dist/server-core/src/index.d.ts +2 -0
- package/dist/server-core/src/init.d.ts +62 -3
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +26 -26
- package/src/api/errors.ts +1 -1
- package/src/api/graphql/graphql-schema-generator.ts +7 -0
- package/src/api/openapi-generator.ts +13 -1
- package/src/api/rest/api-generator-count.test.ts +14 -12
- package/src/api/rest/query-parser.ts +2 -20
- package/src/auth/adapter-middleware.ts +83 -0
- package/src/auth/admin-routes.ts +36 -43
- package/src/auth/auth-overrides.ts +172 -0
- package/src/auth/builtin-auth-adapter.ts +384 -0
- package/src/auth/crypto-utils.ts +31 -0
- package/src/auth/custom-auth-adapter.ts +85 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/interfaces.ts +2 -0
- package/src/auth/jwt.ts +3 -1
- package/src/auth/middleware.ts +2 -46
- package/src/auth/rls-scope.ts +58 -0
- package/src/auth/routes.ts +74 -32
- package/src/cron/cron-scheduler.test.ts +9 -9
- package/src/cron/cron-scheduler.ts +1 -1
- package/src/env.ts +224 -0
- package/src/index.ts +4 -0
- package/src/init.ts +355 -135
- package/src/storage/routes.ts +1 -19
- package/src/utils/logging.ts +3 -3
- package/test/admin-routes.test.ts +10 -4
- package/test/auth-routes.test.ts +2 -2
- package/test/backend-hooks-admin.test.ts +32 -12
- package/test/custom-auth-adapter.test.ts +177 -0
- package/test/env.test.ts +138 -0
- package/test/query-parser.test.ts +0 -29
- package/tsconfig.json +3 -0
- package/dist/index-DXVBFp5V.js.map +0 -1
|
@@ -250,7 +250,7 @@ export interface PluginFormActionProps<USER extends User = User, EC extends Enti
|
|
|
250
250
|
disabled: boolean;
|
|
251
251
|
formContext?: FormContext;
|
|
252
252
|
context: RebaseContext<USER>;
|
|
253
|
-
openEntityMode: "side_panel" | "full_screen" | "split";
|
|
253
|
+
openEntityMode: "side_panel" | "full_screen" | "split" | "dialog";
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
256
|
* Parameters passed to the field builder wrap function.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentRef } from "./component_ref";
|
|
2
|
-
import type { EntityReference, EntityRelation, EntityValues, GeoPoint, Entity } from "./entities";
|
|
2
|
+
import type { EntityReference, EntityRelation, EntityValues, GeoPoint, Entity, Vector } from "./entities";
|
|
3
3
|
import type { Relation, JoinStep, OnAction } from "./relations";
|
|
4
4
|
import type { EntityCollection, FilterValues } from "./collections";
|
|
5
5
|
import type { ColorKey, ColorScheme } from "./chips";
|
|
@@ -31,8 +31,8 @@ export type PropertyCallbacks<T = unknown, M extends Record<string, unknown> = R
|
|
|
31
31
|
/**
|
|
32
32
|
* @group Entity properties
|
|
33
33
|
*/
|
|
34
|
-
export type DataType = "string" | "number" | "boolean" | "date" | "geopoint" | "reference" | "relation" | "array" | "map";
|
|
35
|
-
export type Property = StringProperty | NumberProperty | BooleanProperty | DateProperty | GeopointProperty | ReferenceProperty | RelationProperty | ArrayProperty | MapProperty;
|
|
34
|
+
export type DataType = "string" | "number" | "boolean" | "date" | "geopoint" | "reference" | "relation" | "array" | "map" | "vector" | "binary";
|
|
35
|
+
export type Property = StringProperty | NumberProperty | BooleanProperty | DateProperty | GeopointProperty | ReferenceProperty | RelationProperty | ArrayProperty | MapProperty | VectorProperty | BinaryProperty;
|
|
36
36
|
export type Properties = {
|
|
37
37
|
[key: string]: Property;
|
|
38
38
|
};
|
|
@@ -48,7 +48,7 @@ export type FirebaseProperties = {
|
|
|
48
48
|
* A helper type to infer the underlying data type from a Property definition.
|
|
49
49
|
* This is the core of the type inference system.
|
|
50
50
|
*/
|
|
51
|
-
export type InferPropertyType<P extends Property> = P extends StringProperty ? string : P extends NumberProperty ? number : P extends BooleanProperty ? boolean : P extends DateProperty ? Date : P extends GeopointProperty ? GeoPoint : P extends ReferenceProperty ? EntityReference : P extends RelationProperty ? EntityRelation | EntityRelation[] : P extends ArrayProperty ? (P["of"] extends Property ? InferPropertyType<P["of"]>[] : unknown[]) : P extends MapProperty ? (P["properties"] extends Properties ? InferEntityType<P["properties"]> : Record<string, unknown>) : never;
|
|
51
|
+
export type InferPropertyType<P extends Property> = P extends StringProperty ? string : P extends NumberProperty ? number : P extends BooleanProperty ? boolean : P extends DateProperty ? Date : P extends GeopointProperty ? GeoPoint : P extends ReferenceProperty ? EntityReference : P extends RelationProperty ? EntityRelation | EntityRelation[] : P extends ArrayProperty ? (P["of"] extends Property ? InferPropertyType<P["of"]>[] : unknown[]) : P extends MapProperty ? (P["properties"] extends Properties ? InferEntityType<P["properties"]> : Record<string, unknown>) : P extends VectorProperty ? Vector : P extends BinaryProperty ? string : never;
|
|
52
52
|
/**
|
|
53
53
|
* Helper type that determines whether a property is required.
|
|
54
54
|
* Uses direct structural matching against `{ validation: { required: true } }`
|
|
@@ -309,6 +309,25 @@ export interface BooleanProperty extends BaseProperty {
|
|
|
309
309
|
*/
|
|
310
310
|
validation?: PropertyValidationSchema;
|
|
311
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* @group Entity properties
|
|
314
|
+
*/
|
|
315
|
+
export interface VectorUIConfig extends BaseUIConfig {
|
|
316
|
+
clearable?: boolean;
|
|
317
|
+
}
|
|
318
|
+
export interface VectorProperty extends BaseProperty {
|
|
319
|
+
ui?: VectorUIConfig;
|
|
320
|
+
type: "vector";
|
|
321
|
+
dimensions: number;
|
|
322
|
+
validation?: PropertyValidationSchema;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @group Entity properties
|
|
326
|
+
*/
|
|
327
|
+
export interface BinaryProperty extends BaseProperty {
|
|
328
|
+
type: "binary";
|
|
329
|
+
validation?: PropertyValidationSchema;
|
|
330
|
+
}
|
|
312
331
|
/**
|
|
313
332
|
* @group Entity properties
|
|
314
333
|
*/
|
|
@@ -421,7 +440,7 @@ export interface RelationProperty extends BaseProperty {
|
|
|
421
440
|
* When set, the framework treats this property as a self-contained relation
|
|
422
441
|
* definition and no separate `relations[]` entry is needed.
|
|
423
442
|
*/
|
|
424
|
-
target?: () => EntityCollection;
|
|
443
|
+
target?: string | (() => EntityCollection | string);
|
|
425
444
|
/**
|
|
426
445
|
* Whether this property references one or many records.
|
|
427
446
|
* Defaults to `"one"`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { ArrayProperty, MapProperty, StringProperty, NumberProperty, BooleanProperty, DateProperty, GeopointProperty, ReferenceProperty, RelationProperty } from "./properties";
|
|
2
|
+
import { ArrayProperty, MapProperty, StringProperty, NumberProperty, BooleanProperty, DateProperty, GeopointProperty, ReferenceProperty, RelationProperty, VectorProperty, BinaryProperty } from "./properties";
|
|
3
3
|
import { BaseProperty } from "./properties";
|
|
4
4
|
type CMSBasePropertyNoName = Omit<BaseProperty, "name">;
|
|
5
5
|
export type ConfigProperty = (Omit<StringProperty, "name"> & {
|
|
@@ -28,6 +28,10 @@ export type ConfigProperty = (Omit<StringProperty, "name"> & {
|
|
|
28
28
|
} & CMSBasePropertyNoName) | (Omit<MapProperty, "name" | "properties"> & {
|
|
29
29
|
name?: string;
|
|
30
30
|
properties?: Record<string, ConfigProperty>;
|
|
31
|
+
} & CMSBasePropertyNoName) | (Omit<VectorProperty, "name"> & {
|
|
32
|
+
name?: string;
|
|
33
|
+
} & CMSBasePropertyNoName) | (Omit<BinaryProperty, "name"> & {
|
|
34
|
+
name?: string;
|
|
31
35
|
} & CMSBasePropertyNoName);
|
|
32
36
|
/**
|
|
33
37
|
* This is the configuration object for a property.
|
|
@@ -66,5 +70,5 @@ export type PropertyConfig = {
|
|
|
66
70
|
*/
|
|
67
71
|
description?: string;
|
|
68
72
|
};
|
|
69
|
-
export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "user_select" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "reference_as_string" | "multi_references" | "relation" | "switch" | "date_time" | "repeat" | "custom_array" | "block";
|
|
73
|
+
export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "user_select" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "reference_as_string" | "multi_references" | "relation" | "switch" | "date_time" | "repeat" | "custom_array" | "block" | "vector_input";
|
|
70
74
|
export {};
|
|
@@ -17,7 +17,7 @@ export interface Relation {
|
|
|
17
17
|
/**
|
|
18
18
|
* The final collection you want to retrieve records from.
|
|
19
19
|
*/
|
|
20
|
-
target: () => EntityCollection;
|
|
20
|
+
target: (() => EntityCollection) | any;
|
|
21
21
|
/**
|
|
22
22
|
* The nature of the relationship, determining if one or many records are returned.
|
|
23
23
|
*/
|
|
@@ -30,6 +30,8 @@ export interface RebaseTranslations {
|
|
|
30
30
|
copy: string;
|
|
31
31
|
delete: string;
|
|
32
32
|
delete_not_allowed: string;
|
|
33
|
+
edit_entity?: string;
|
|
34
|
+
back_to_detail?: string;
|
|
33
35
|
delete_confirmation_title: string;
|
|
34
36
|
delete_confirmation_body: string;
|
|
35
37
|
delete_multiple_confirmation_body: string;
|
|
@@ -392,6 +394,12 @@ export interface RebaseTranslations {
|
|
|
392
394
|
submit: string;
|
|
393
395
|
no_filterable_properties: string;
|
|
394
396
|
apply_filters: string;
|
|
397
|
+
/** Label shown on the filter presets dropdown trigger */
|
|
398
|
+
filter_presets?: string;
|
|
399
|
+
/** Tooltip shown when hovering over a preset entry */
|
|
400
|
+
filter_preset_apply?: string;
|
|
401
|
+
/** Shown when a preset is active, with {{label}} interpolation */
|
|
402
|
+
filter_preset_active?: string;
|
|
395
403
|
list: string;
|
|
396
404
|
table_view_mode: string;
|
|
397
405
|
cards: string;
|
|
@@ -42,5 +42,10 @@ export type User = {
|
|
|
42
42
|
* The date and time when the user was created.
|
|
43
43
|
*/
|
|
44
44
|
createdAt?: Date | string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Additional metadata/custom claims associated with the user.
|
|
47
|
+
* Accessible by the frontend, but only writable by the backend.
|
|
48
|
+
*/
|
|
49
|
+
readonly metadata?: Record<string, any>;
|
|
45
50
|
getIdToken?: (forceRefresh?: boolean) => Promise<string>;
|
|
46
51
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -33,44 +33,44 @@
|
|
|
33
33
|
"exports": {
|
|
34
34
|
".": {
|
|
35
35
|
"types": "./dist/server-core/src/index.d.ts",
|
|
36
|
-
"development": "./
|
|
36
|
+
"development": "./dist/index.es.js",
|
|
37
37
|
"import": "./dist/index.es.js",
|
|
38
38
|
"require": "./dist/index.umd.js"
|
|
39
39
|
},
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@aws-sdk/client-s3": "^3.
|
|
44
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
43
|
+
"@aws-sdk/client-s3": "^3.1050.0",
|
|
44
|
+
"@aws-sdk/s3-request-presigner": "^3.1050.0",
|
|
45
45
|
"@hono/graphql-server": "0.7.0",
|
|
46
46
|
"@hono/node-server": "1.19.12",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
47
|
+
"drizzle-orm": "^0.44.7",
|
|
48
|
+
"google-auth-library": "^9.15.1",
|
|
49
|
+
"graphql": "^16.14.0",
|
|
50
|
+
"hono": "^4.12.21",
|
|
51
|
+
"jsonwebtoken": "^9.0.3",
|
|
52
|
+
"nodemailer": "^6.10.1",
|
|
52
53
|
"ts-morph": "27.0.2",
|
|
53
|
-
"ws": "^8.
|
|
54
|
-
"zod": "^3.
|
|
55
|
-
"@rebasepro/
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
54
|
+
"ws": "^8.20.1",
|
|
55
|
+
"zod": "^3.25.76",
|
|
56
|
+
"@rebasepro/common": "0.2.1",
|
|
57
|
+
"@rebasepro/client": "0.2.1",
|
|
58
|
+
"@rebasepro/types": "0.2.1",
|
|
59
|
+
"@rebasepro/utils": "0.2.1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@types/jest": "^29.5.14",
|
|
62
|
-
"@types/jsonwebtoken": "^9.0.
|
|
63
|
-
"@types/node": "^20.
|
|
64
|
-
"@types/nodemailer": "^6.4.
|
|
65
|
-
"@types/react": "^19.
|
|
66
|
-
"@types/react-dom": "^19.
|
|
67
|
-
"@types/ws": "^8.
|
|
63
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
64
|
+
"@types/node": "^20.19.41",
|
|
65
|
+
"@types/nodemailer": "^6.4.23",
|
|
66
|
+
"@types/react": "^19.2.15",
|
|
67
|
+
"@types/react-dom": "^19.2.3",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
69
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
68
70
|
"jest": "^29.7.0",
|
|
69
71
|
"ts-jest": "29.4.1",
|
|
70
|
-
"typescript": "^5.
|
|
71
|
-
"vite": "^5.
|
|
72
|
-
"@rebasepro/common": "0.1.2",
|
|
73
|
-
"@rebasepro/types": "0.1.2"
|
|
72
|
+
"typescript": "^5.9.3",
|
|
73
|
+
"vite": "^5.4.21"
|
|
74
74
|
},
|
|
75
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
76
76
|
"publishConfig": {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"watch": "vite build --watch",
|
|
81
81
|
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
82
82
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
83
|
-
"test": "jest --passWithNoTests",
|
|
83
|
+
"test": "jest --passWithNoTests --forceExit",
|
|
84
84
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/api/errors.ts
CHANGED
|
@@ -108,7 +108,7 @@ export const errorHandler: ErrorHandler = (err, c) => {
|
|
|
108
108
|
logMessage = `Database schema mismatch (${issue} missing): ${cause.message}. Did you forget to run migrations ('pnpm db:push' or 'pnpm db:migrate')?`;
|
|
109
109
|
}
|
|
110
110
|
} else if ("code" in error && error.code === "ENETUNREACH") {
|
|
111
|
-
const netErr = error as
|
|
111
|
+
const netErr = error as PgLikeError;
|
|
112
112
|
logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;
|
|
113
113
|
} else if ("code" in error && (error.code === "42703" || error.code === "42P01")) {
|
|
114
114
|
const issue = error.code === "42703" ? "column" : "table";
|
|
@@ -89,6 +89,7 @@ export class GraphQLSchemaGenerator {
|
|
|
89
89
|
let type;
|
|
90
90
|
|
|
91
91
|
switch (property.type) {
|
|
92
|
+
case "binary":
|
|
92
93
|
case "string":
|
|
93
94
|
type = GraphQLString;
|
|
94
95
|
break;
|
|
@@ -104,6 +105,9 @@ export class GraphQLSchemaGenerator {
|
|
|
104
105
|
case "array":
|
|
105
106
|
type = new GraphQLList(GraphQLString);
|
|
106
107
|
break;
|
|
108
|
+
case "vector":
|
|
109
|
+
type = new GraphQLList(GraphQLFloat);
|
|
110
|
+
break;
|
|
107
111
|
default:
|
|
108
112
|
type = GraphQLString;
|
|
109
113
|
}
|
|
@@ -143,6 +147,7 @@ export class GraphQLSchemaGenerator {
|
|
|
143
147
|
|
|
144
148
|
private convertPropertyToInputType(property: Property) {
|
|
145
149
|
switch (property.type) {
|
|
150
|
+
case "binary":
|
|
146
151
|
case "string":
|
|
147
152
|
return GraphQLString;
|
|
148
153
|
case "number":
|
|
@@ -153,6 +158,8 @@ export class GraphQLSchemaGenerator {
|
|
|
153
158
|
return GraphQLString;
|
|
154
159
|
case "array":
|
|
155
160
|
return new GraphQLList(GraphQLString);
|
|
161
|
+
case "vector":
|
|
162
|
+
return new GraphQLList(GraphQLFloat);
|
|
156
163
|
default:
|
|
157
164
|
return GraphQLString;
|
|
158
165
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityCollection, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, Relation } from "@rebasepro/types";
|
|
1
|
+
import { EntityCollection, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, Relation, VectorProperty } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* OpenAPI 3.0.3 specification generator.
|
|
@@ -624,6 +624,18 @@ enum: [variantKey] },
|
|
|
624
624
|
return base;
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
case "vector": {
|
|
628
|
+
const vp = property as VectorProperty;
|
|
629
|
+
base.type = "array";
|
|
630
|
+
base.items = { type: "number" };
|
|
631
|
+
base.description = (base.description || "") + ` (Vector(${vp.dimensions}))`;
|
|
632
|
+
return base;
|
|
633
|
+
}
|
|
634
|
+
case "binary": {
|
|
635
|
+
base.type = "string";
|
|
636
|
+
base.description = (base.description || "") + " (Binary/Base64)";
|
|
637
|
+
return base;
|
|
638
|
+
}
|
|
627
639
|
default:
|
|
628
640
|
base.type = "string";
|
|
629
641
|
return base;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
2
|
import { RestApiGenerator } from "./api-generator";
|
|
3
|
-
import type { DataDriver, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
3
|
+
import type { DataDriver, Entity, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Minimal mock DataDriver for testing.
|
|
7
7
|
*/
|
|
8
8
|
function createMockDriver(overrides?: Partial<DataDriver>): DataDriver {
|
|
9
9
|
return {
|
|
10
|
-
fetchCollection: jest.fn().mockResolvedValue([]),
|
|
11
|
-
fetchEntity: jest.fn().mockResolvedValue(
|
|
12
|
-
saveEntity: jest.fn().mockResolvedValue({ id: "1",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
fetchCollection: jest.fn<DataDriver["fetchCollection"]>().mockResolvedValue([]),
|
|
11
|
+
fetchEntity: jest.fn<DataDriver["fetchEntity"]>().mockResolvedValue(undefined),
|
|
12
|
+
saveEntity: jest.fn<DataDriver["saveEntity"]>().mockResolvedValue({ id: "1",
|
|
13
|
+
path: "test",
|
|
14
|
+
values: {} } as Entity),
|
|
15
|
+
deleteEntity: jest.fn<DataDriver["deleteEntity"]>().mockResolvedValue(undefined),
|
|
16
|
+
countEntities: jest.fn<NonNullable<DataDriver["countEntities"]>>().mockResolvedValue(0),
|
|
17
|
+
...overrides
|
|
16
18
|
} as unknown as DataDriver;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -21,7 +23,7 @@ function createTestCollection(slug: string): EntityCollection {
|
|
|
21
23
|
slug,
|
|
22
24
|
name: slug.charAt(0).toUpperCase() + slug.slice(1),
|
|
23
25
|
path: slug,
|
|
24
|
-
properties: {}
|
|
26
|
+
properties: {}
|
|
25
27
|
} as unknown as EntityCollection;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -31,7 +33,7 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
31
33
|
|
|
32
34
|
beforeEach(() => {
|
|
33
35
|
driver = createMockDriver({
|
|
34
|
-
countEntities: jest.fn().mockResolvedValue(42)
|
|
36
|
+
countEntities: jest.fn<NonNullable<DataDriver["countEntities"]>>().mockResolvedValue(42)
|
|
35
37
|
});
|
|
36
38
|
collection = createTestCollection("products");
|
|
37
39
|
});
|
|
@@ -93,10 +95,10 @@ describe("RestApiGenerator - Count Endpoint", () => {
|
|
|
93
95
|
|
|
94
96
|
it("GET /products/count should not be confused with GET /products/:id", async () => {
|
|
95
97
|
// Ensure the count route is registered before the :id route
|
|
96
|
-
const fetchEntity = jest.fn().mockResolvedValue(
|
|
98
|
+
const fetchEntity = jest.fn<DataDriver["fetchEntity"]>().mockResolvedValue(undefined);
|
|
97
99
|
const driverCustom = createMockDriver({
|
|
98
|
-
countEntities: jest.fn().mockResolvedValue(99),
|
|
99
|
-
fetchEntity
|
|
100
|
+
countEntities: jest.fn<NonNullable<DataDriver["countEntities"]>>().mockResolvedValue(99),
|
|
101
|
+
fetchEntity: fetchEntity as unknown as DataDriver["fetchEntity"]
|
|
100
102
|
});
|
|
101
103
|
const generator = new RestApiGenerator([collection], driverCustom);
|
|
102
104
|
const app = generator.generateRoutes();
|
|
@@ -37,27 +37,9 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
37
37
|
// Filtering
|
|
38
38
|
options.where = {};
|
|
39
39
|
|
|
40
|
-
// Legacy JSON where clause
|
|
41
|
-
if (query.where) {
|
|
42
|
-
try {
|
|
43
|
-
const parsedWhere = typeof query.where === "string"
|
|
44
|
-
? JSON.parse(query.where)
|
|
45
|
-
: query.where;
|
|
46
|
-
if (typeof parsedWhere !== "object" || parsedWhere === null || Array.isArray(parsedWhere)) {
|
|
47
|
-
throw new Error("Filter must be a JSON object");
|
|
48
|
-
}
|
|
49
|
-
Object.assign(options.where, parsedWhere);
|
|
50
|
-
} catch (e) {
|
|
51
|
-
const message = e instanceof Error ? e.message : "malformed JSON";
|
|
52
|
-
const err = new Error(`Invalid 'where' filter: ${message}`) as Error & { code?: string; statusCode?: number };
|
|
53
|
-
err.code = "BAD_REQUEST";
|
|
54
|
-
err.statusCode = 400;
|
|
55
|
-
throw err;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
40
|
|
|
59
|
-
// PostgREST
|
|
60
|
-
const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "
|
|
41
|
+
// PostgREST-style filtering: ?field=op.value
|
|
42
|
+
const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString"];
|
|
61
43
|
for (const [key, rawValue] of Object.entries(query)) {
|
|
62
44
|
if (reservedQueryKeys.includes(key)) continue;
|
|
63
45
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Auth Middleware
|
|
3
|
+
*
|
|
4
|
+
* Creates a Hono middleware that delegates authentication to an `AuthAdapter`
|
|
5
|
+
* instead of hardcoded JWT verification. This is used when the user passes
|
|
6
|
+
* an `AuthAdapter` to `initializeRebaseBackend()`.
|
|
7
|
+
*
|
|
8
|
+
* The middleware:
|
|
9
|
+
* 1. Calls `adapter.verifyRequest(request)` to resolve the user
|
|
10
|
+
* 2. Scopes the DataDriver via `withAuth()` for RLS
|
|
11
|
+
* 3. Enforces auth (401) when `requireAuth` is true and no user is found
|
|
12
|
+
*
|
|
13
|
+
* The behavior is identical to `createAuthMiddleware()` — only the
|
|
14
|
+
* token verification strategy is pluggable.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { MiddlewareHandler } from "hono";
|
|
18
|
+
import type { DataDriver, AuthAdapter } from "@rebasepro/types";
|
|
19
|
+
import type { HonoEnv } from "../api/types";
|
|
20
|
+
import { scopeDataDriver } from "./rls-scope";
|
|
21
|
+
|
|
22
|
+
export interface AdapterAuthMiddlewareOptions {
|
|
23
|
+
/** The auth adapter to delegate verification to. */
|
|
24
|
+
adapter: AuthAdapter;
|
|
25
|
+
/** The DataDriver to scope via withAuth() for RLS. */
|
|
26
|
+
driver: DataDriver;
|
|
27
|
+
/**
|
|
28
|
+
* If true, return 401 when no valid user is resolved.
|
|
29
|
+
* Defaults to `true` (secure by default).
|
|
30
|
+
*/
|
|
31
|
+
requireAuth?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a Hono middleware that uses an `AuthAdapter` for request verification.
|
|
36
|
+
*/
|
|
37
|
+
export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
38
|
+
const { adapter, driver, requireAuth: enforceAuth = true } = options;
|
|
39
|
+
|
|
40
|
+
return async (c, next) => {
|
|
41
|
+
let authenticatedUser = null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
authenticatedUser = await adapter.verifyRequest(c.req.raw);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// adapter.verifyRequest() threw — reject the request (fail closed)
|
|
47
|
+
return c.json({ error: { message: "Unauthorized", code: "UNAUTHORIZED" } }, 401);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (authenticatedUser) {
|
|
51
|
+
// Authenticated — set user context and scope driver
|
|
52
|
+
c.set("user", {
|
|
53
|
+
userId: authenticatedUser.uid,
|
|
54
|
+
email: authenticatedUser.email,
|
|
55
|
+
roles: authenticatedUser.roles,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
c.set("driver", await scopeDataDriver(driver, {
|
|
59
|
+
uid: authenticatedUser.uid,
|
|
60
|
+
roles: authenticatedUser.roles,
|
|
61
|
+
}));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("[AUTH-ADAPTER] RLS scoping failed for authenticated user:", error);
|
|
64
|
+
return c.json({ error: { message: "Internal authentication error", code: "INTERNAL_ERROR" } }, 500);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Not authenticated — scope as anon for RLS evaluation
|
|
68
|
+
try {
|
|
69
|
+
c.set("driver", await scopeDataDriver(driver, { uid: "anon", roles: ["anon"] }));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("[AUTH-ADAPTER] Failed to create anon-scoped driver:", error);
|
|
72
|
+
return c.json({ error: { message: "Server configuration error", code: "INTERNAL_ERROR" } }, 500);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Enforce auth if required
|
|
77
|
+
if (enforceAuth && !c.get("user")) {
|
|
78
|
+
return c.json({ error: { message: "Unauthorized: Authentication required", code: "UNAUTHORIZED" } }, 401);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return next();
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/auth/admin-routes.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { ApiError, errorHandler } from "../api/errors";
|
|
3
3
|
import type { AuthRepository } from "./interfaces";
|
|
4
4
|
import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
|
|
5
|
-
import {
|
|
5
|
+
import type { AuthOverrides } from "./auth-overrides";
|
|
6
|
+
import { resolveAuthOverrides } from "./auth-overrides";
|
|
6
7
|
import { AuthModuleConfig } from "./routes";
|
|
7
8
|
import type { BackendHooks, AdminUser, AdminRole, BackendHookContext } from "@rebasepro/types";
|
|
8
9
|
|
|
@@ -17,6 +18,11 @@ interface AdminRouteOptions extends AuthModuleConfig {
|
|
|
17
18
|
* Backend-level hooks for intercepting admin data.
|
|
18
19
|
*/
|
|
19
20
|
hooks?: BackendHooks;
|
|
21
|
+
/**
|
|
22
|
+
* Auth overrides for customizing password hashing, credential
|
|
23
|
+
* verification, lifecycle hooks, etc.
|
|
24
|
+
*/
|
|
25
|
+
overrides?: AuthOverrides;
|
|
20
26
|
}
|
|
21
27
|
import { HonoEnv } from "../api/types";
|
|
22
28
|
import { randomBytes, createHash } from "crypto";
|
|
@@ -68,7 +74,8 @@ function hashToken(token: string): string {
|
|
|
68
74
|
export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
69
75
|
const router = new Hono<HonoEnv>();
|
|
70
76
|
const authRepo = config.authRepo;
|
|
71
|
-
const { emailService, emailConfig, hooks } = config;
|
|
77
|
+
const { emailService, emailConfig, hooks, overrides } = config;
|
|
78
|
+
const ops = resolveAuthOverrides(overrides);
|
|
72
79
|
|
|
73
80
|
/** Build a BackendHookContext from Hono's context object */
|
|
74
81
|
function buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
@@ -192,41 +199,20 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
192
199
|
const orderDir = c.req.query("orderDir") as "asc" | "desc" | undefined;
|
|
193
200
|
const hookCtx = buildHookContext(c, "GET");
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const limit = limitParam ? parseInt(limitParam, 10) : 25;
|
|
198
|
-
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
|
199
|
-
|
|
200
|
-
const result = await authRepo.listUsersPaginated({
|
|
201
|
-
limit,
|
|
202
|
-
offset,
|
|
203
|
-
search: search || undefined,
|
|
204
|
-
orderBy: orderBy || undefined,
|
|
205
|
-
orderDir: orderDir || undefined,
|
|
206
|
-
roleId: c.req.query("role") || undefined
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
let usersWithRoles: AdminUser[] = await Promise.all(
|
|
210
|
-
result.users.map(async (u) => {
|
|
211
|
-
const roles = await authRepo.getUserRoleIds(u.id);
|
|
212
|
-
return toAdminUser(u, roles);
|
|
213
|
-
})
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
usersWithRoles = await applyUserAfterReadBatch(usersWithRoles, hookCtx);
|
|
202
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 25;
|
|
203
|
+
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
|
217
204
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
const result = await authRepo.listUsersPaginated({
|
|
206
|
+
limit,
|
|
207
|
+
offset,
|
|
208
|
+
search: search || undefined,
|
|
209
|
+
orderBy: orderBy || undefined,
|
|
210
|
+
orderDir: orderDir || undefined,
|
|
211
|
+
roleId: c.req.query("role") || undefined
|
|
212
|
+
});
|
|
225
213
|
|
|
226
|
-
// Legacy: return all users (no pagination)
|
|
227
|
-
const users = await authRepo.listUsers();
|
|
228
214
|
let usersWithRoles: AdminUser[] = await Promise.all(
|
|
229
|
-
users.map(async (u) => {
|
|
215
|
+
result.users.map(async (u) => {
|
|
230
216
|
const roles = await authRepo.getUserRoleIds(u.id);
|
|
231
217
|
return toAdminUser(u, roles);
|
|
232
218
|
})
|
|
@@ -234,7 +220,12 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
234
220
|
|
|
235
221
|
usersWithRoles = await applyUserAfterReadBatch(usersWithRoles, hookCtx);
|
|
236
222
|
|
|
237
|
-
return c.json({
|
|
223
|
+
return c.json({
|
|
224
|
+
users: usersWithRoles,
|
|
225
|
+
total: result.total,
|
|
226
|
+
limit: result.limit,
|
|
227
|
+
offset: result.offset
|
|
228
|
+
});
|
|
238
229
|
});
|
|
239
230
|
|
|
240
231
|
router.get("/users/:userId", requireAdmin, async (c) => {
|
|
@@ -258,7 +249,8 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
258
249
|
|
|
259
250
|
router.post("/users", requireAdmin, async (c) => {
|
|
260
251
|
const body = await c.req.json();
|
|
261
|
-
|
|
252
|
+
const { password } = body;
|
|
253
|
+
let { email, displayName, roles } = body;
|
|
262
254
|
|
|
263
255
|
if (!email) {
|
|
264
256
|
throw ApiError.badRequest("Email is required", "INVALID_INPUT");
|
|
@@ -281,11 +273,11 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
281
273
|
// Use provided password or auto-generate one
|
|
282
274
|
const clearPassword = password || generateSecurePassword();
|
|
283
275
|
|
|
284
|
-
const validation = validatePasswordStrength(clearPassword);
|
|
276
|
+
const validation = ops.validatePasswordStrength(clearPassword);
|
|
285
277
|
if (!validation.valid) {
|
|
286
278
|
throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
|
|
287
279
|
}
|
|
288
|
-
const passwordHash = await hashPassword(clearPassword);
|
|
280
|
+
const passwordHash = await ops.hashPassword(clearPassword);
|
|
289
281
|
|
|
290
282
|
const user = await authRepo.createUser({
|
|
291
283
|
email: email.toLowerCase(),
|
|
@@ -401,14 +393,14 @@ displayName: existing.displayName }, appName);
|
|
|
401
393
|
console.error("Failed to send reset email:", emailError instanceof Error ? emailError.message : emailError);
|
|
402
394
|
// Fall back to returning the temporary password
|
|
403
395
|
const clearPassword = generateSecurePassword();
|
|
404
|
-
const passwordHash = await hashPassword(clearPassword);
|
|
396
|
+
const passwordHash = await ops.hashPassword(clearPassword);
|
|
405
397
|
await authRepo.updatePassword(existing.id, passwordHash);
|
|
406
398
|
temporaryPassword = clearPassword;
|
|
407
399
|
}
|
|
408
400
|
} else {
|
|
409
401
|
// No email service — generate password, set it, and return one-time
|
|
410
402
|
const clearPassword = generateSecurePassword();
|
|
411
|
-
const passwordHash = await hashPassword(clearPassword);
|
|
403
|
+
const passwordHash = await ops.hashPassword(clearPassword);
|
|
412
404
|
await authRepo.updatePassword(existing.id, passwordHash);
|
|
413
405
|
temporaryPassword = clearPassword;
|
|
414
406
|
}
|
|
@@ -430,7 +422,8 @@ displayName: existing.displayName }, appName);
|
|
|
430
422
|
router.put("/users/:userId", requireAdmin, async (c) => {
|
|
431
423
|
const userId = c.req.param("userId");
|
|
432
424
|
const body = await c.req.json();
|
|
433
|
-
|
|
425
|
+
const { password } = body;
|
|
426
|
+
let { email, displayName, roles } = body;
|
|
434
427
|
|
|
435
428
|
const existing = await authRepo.getUserById(userId);
|
|
436
429
|
if (!existing) {
|
|
@@ -451,11 +444,11 @@ displayName: existing.displayName }, appName);
|
|
|
451
444
|
if (displayName !== undefined) updates.displayName = displayName;
|
|
452
445
|
|
|
453
446
|
if (password) {
|
|
454
|
-
const validation = validatePasswordStrength(password);
|
|
447
|
+
const validation = ops.validatePasswordStrength(password);
|
|
455
448
|
if (!validation.valid) {
|
|
456
449
|
throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
|
|
457
450
|
}
|
|
458
|
-
updates.passwordHash = await hashPassword(password);
|
|
451
|
+
updates.passwordHash = await ops.hashPassword(password);
|
|
459
452
|
}
|
|
460
453
|
|
|
461
454
|
if (Object.keys(updates).length > 0) {
|