@rebasepro/server-core 0.1.2 → 0.2.3
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/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- 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 +16038 -15240
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15980 -15178
- 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/controllers/data.d.ts +21 -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/jest.config.cjs +4 -1
- package/package.json +27 -27
- 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
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) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthOverrides
|
|
3
|
+
*
|
|
4
|
+
* Override specific behaviors of the built-in Rebase auth system.
|
|
5
|
+
*
|
|
6
|
+
* Each method replaces one piece of the default implementation.
|
|
7
|
+
* Unset methods fall through to the built-in defaults (scrypt passwords,
|
|
8
|
+
* standard validation rules, etc.).
|
|
9
|
+
*
|
|
10
|
+
* This interface is intentionally open for extension — new overrides
|
|
11
|
+
* can be added as optional methods without breaking existing configurations.
|
|
12
|
+
*
|
|
13
|
+
* @example bcrypt password support
|
|
14
|
+
* ```ts
|
|
15
|
+
* import bcrypt from "bcrypt";
|
|
16
|
+
*
|
|
17
|
+
* const overrides: AuthOverrides = {
|
|
18
|
+
* hashPassword: (pw) => bcrypt.hash(pw, 12),
|
|
19
|
+
* verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
|
|
20
|
+
* validatePasswordStrength: (pw) => ({
|
|
21
|
+
* valid: pw.length >= 6,
|
|
22
|
+
* errors: pw.length < 6 ? ["Password must be at least 6 characters"] : []
|
|
23
|
+
* })
|
|
24
|
+
* };
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example Override the entire login credential check
|
|
28
|
+
* ```ts
|
|
29
|
+
* const overrides: AuthOverrides = {
|
|
30
|
+
* verifyCredentials: async (email, password, repo) => {
|
|
31
|
+
* const user = await repo.getUserByEmail(email);
|
|
32
|
+
* if (!user || !user.passwordHash) return null;
|
|
33
|
+
* const valid = await myCustomVerify(password, user.passwordHash);
|
|
34
|
+
* return valid ? user : null;
|
|
35
|
+
* }
|
|
36
|
+
* };
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
hashPassword as defaultHashPassword,
|
|
42
|
+
verifyPassword as defaultVerifyPassword,
|
|
43
|
+
validatePasswordStrength as defaultValidatePasswordStrength
|
|
44
|
+
} from "./password";
|
|
45
|
+
import type { PasswordValidationResult } from "./password";
|
|
46
|
+
import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Authentication method identifier for lifecycle hooks.
|
|
50
|
+
*/
|
|
51
|
+
export type AuthMethod = "login" | "register" | "oauth" | "refresh" | "password-reset";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Override specific parts of the built-in Rebase auth implementation.
|
|
55
|
+
*
|
|
56
|
+
* Every method is optional. The built-in defaults apply for any method
|
|
57
|
+
* that is not provided.
|
|
58
|
+
*/
|
|
59
|
+
export interface AuthOverrides {
|
|
60
|
+
// ─── Password Operations ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Hash a cleartext password for storage.
|
|
64
|
+
*
|
|
65
|
+
* Default: scrypt (Node.js crypto, 64-byte key, random 32-byte salt).
|
|
66
|
+
*
|
|
67
|
+
* @param password - The cleartext password.
|
|
68
|
+
* @returns The hashed password string (format is implementation-defined).
|
|
69
|
+
*/
|
|
70
|
+
hashPassword?(password: string): Promise<string>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verify a cleartext password against a stored hash.
|
|
74
|
+
*
|
|
75
|
+
* Default: scrypt verification with timing-safe comparison.
|
|
76
|
+
*
|
|
77
|
+
* @param password - The cleartext password to check.
|
|
78
|
+
* @param storedHash - The hash string retrieved from the database.
|
|
79
|
+
* @returns `true` if the password matches the hash.
|
|
80
|
+
*/
|
|
81
|
+
verifyPassword?(password: string, storedHash: string): Promise<boolean>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate password strength before hashing.
|
|
85
|
+
*
|
|
86
|
+
* Default: minimum 8 characters, at least one uppercase, one lowercase, one digit.
|
|
87
|
+
*
|
|
88
|
+
* @param password - The cleartext password to validate.
|
|
89
|
+
* @returns Validation result with `valid` flag and error messages.
|
|
90
|
+
*/
|
|
91
|
+
validatePasswordStrength?(password: string): PasswordValidationResult;
|
|
92
|
+
|
|
93
|
+
// ─── Credential Resolution ────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Override the complete credential verification during email/password login.
|
|
97
|
+
*
|
|
98
|
+
* When set, this replaces the default flow:
|
|
99
|
+
* 1. Look up user by email
|
|
100
|
+
* 2. Verify password hash
|
|
101
|
+
*
|
|
102
|
+
* The auth repository is provided for database access. Return the user
|
|
103
|
+
* data if credentials are valid, or `null` to reject the login.
|
|
104
|
+
*
|
|
105
|
+
* Default: `getUserByEmail(email)` + `verifyPassword(password, user.passwordHash)`.
|
|
106
|
+
*/
|
|
107
|
+
verifyCredentials?(email: string, password: string, repo: AuthRepository): Promise<UserData | null>;
|
|
108
|
+
|
|
109
|
+
// ─── Lifecycle Hooks ──────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Called after any successful authentication event (login, register,
|
|
113
|
+
* OAuth, token refresh, password reset).
|
|
114
|
+
*
|
|
115
|
+
* Use for audit logging, syncing external state, updating
|
|
116
|
+
* last-login timestamps, etc.
|
|
117
|
+
*
|
|
118
|
+
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
119
|
+
*/
|
|
120
|
+
onAuthenticated?(user: UserData, method: AuthMethod): Promise<void>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Called before a new user is created (registration or admin creation).
|
|
124
|
+
*
|
|
125
|
+
* Return modified data to alter what gets stored, or throw an error
|
|
126
|
+
* to reject the creation entirely.
|
|
127
|
+
*
|
|
128
|
+
* Default: passthrough (returns data unchanged).
|
|
129
|
+
*/
|
|
130
|
+
beforeUserCreate?(data: CreateUserData): Promise<CreateUserData>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Called after a new user is created.
|
|
134
|
+
*
|
|
135
|
+
* Use for provisioning external resources, sending notifications
|
|
136
|
+
* to third-party systems, etc.
|
|
137
|
+
*
|
|
138
|
+
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
139
|
+
*/
|
|
140
|
+
afterUserCreate?(user: UserData): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolved auth operations — every method is guaranteed to exist.
|
|
145
|
+
* Created by `resolveAuthOverrides()` which merges user overrides
|
|
146
|
+
* with built-in defaults.
|
|
147
|
+
*/
|
|
148
|
+
export interface ResolvedAuthOperations {
|
|
149
|
+
hashPassword(password: string): Promise<string>;
|
|
150
|
+
verifyPassword(password: string, storedHash: string): Promise<boolean>;
|
|
151
|
+
validatePasswordStrength(password: string): PasswordValidationResult;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Merge user-provided overrides with the built-in defaults to produce
|
|
156
|
+
* a complete set of resolved operations.
|
|
157
|
+
*
|
|
158
|
+
* This is the single point where defaults are applied — all consumers
|
|
159
|
+
* call this once and use the resolved operations throughout.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveAuthOverrides(overrides?: AuthOverrides): ResolvedAuthOperations {
|
|
162
|
+
return {
|
|
163
|
+
hashPassword: overrides?.hashPassword
|
|
164
|
+
?? defaultHashPassword,
|
|
165
|
+
|
|
166
|
+
verifyPassword: overrides?.verifyPassword
|
|
167
|
+
?? defaultVerifyPassword,
|
|
168
|
+
|
|
169
|
+
validatePasswordStrength: overrides?.validatePasswordStrength
|
|
170
|
+
?? defaultValidatePasswordStrength,
|
|
171
|
+
};
|
|
172
|
+
}
|