@rebasepro/server-core 0.1.0 → 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 +15909 -16083
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15847 -16017
- 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/app/frontend/node_modules/esbuild/LICENSE.md +0 -21
- package/app/frontend/node_modules/esbuild/README.md +0 -3
- package/app/frontend/node_modules/esbuild/bin/esbuild +0 -220
- package/app/frontend/node_modules/esbuild/install.js +0 -285
- package/app/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
- package/app/frontend/node_modules/esbuild/lib/main.js +0 -2239
- package/app/frontend/node_modules/esbuild/package.json +0 -46
- package/dist/index-DXVBFp5V.js.map +0 -1
- package/examples/firebase/node_modules/esbuild/LICENSE.md +0 -21
- package/examples/firebase/node_modules/esbuild/README.md +0 -3
- package/examples/firebase/node_modules/esbuild/bin/esbuild +0 -220
- package/examples/firebase/node_modules/esbuild/install.js +0 -285
- package/examples/firebase/node_modules/esbuild/lib/main.d.ts +0 -705
- package/examples/firebase/node_modules/esbuild/lib/main.js +0 -2239
- package/examples/firebase/node_modules/esbuild/package.json +0 -46
- package/examples/medmot-staging/frontend/node_modules/esbuild/LICENSE.md +0 -21
- package/examples/medmot-staging/frontend/node_modules/esbuild/README.md +0 -3
- package/examples/medmot-staging/frontend/node_modules/esbuild/bin/esbuild +0 -220
- package/examples/medmot-staging/frontend/node_modules/esbuild/install.js +0 -285
- package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
- package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.js +0 -2239
- package/examples/medmot-staging/frontend/node_modules/esbuild/package.json +0 -46
- package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
- package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
- package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
- package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
- package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
- package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
- package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
- package/packages/client/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/client/node_modules/esbuild/README.md +0 -3
- package/packages/client/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/client/node_modules/esbuild/install.js +0 -285
- package/packages/client/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/client/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/client/node_modules/esbuild/package.json +0 -46
- package/packages/client-postgresql/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/client-postgresql/node_modules/esbuild/README.md +0 -3
- package/packages/client-postgresql/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/client-postgresql/node_modules/esbuild/install.js +0 -285
- package/packages/client-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/client-postgresql/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/client-postgresql/node_modules/esbuild/package.json +0 -46
- package/packages/common/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/common/node_modules/esbuild/README.md +0 -3
- package/packages/common/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/common/node_modules/esbuild/install.js +0 -285
- package/packages/common/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/common/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/common/node_modules/esbuild/package.json +0 -46
- package/packages/server-mongodb/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/server-mongodb/node_modules/esbuild/README.md +0 -3
- package/packages/server-mongodb/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/server-mongodb/node_modules/esbuild/install.js +0 -285
- package/packages/server-mongodb/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/server-mongodb/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/server-mongodb/node_modules/esbuild/package.json +0 -46
- package/packages/server-postgresql/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/server-postgresql/node_modules/esbuild/README.md +0 -3
- package/packages/server-postgresql/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/server-postgresql/node_modules/esbuild/install.js +0 -285
- package/packages/server-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/server-postgresql/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/server-postgresql/node_modules/esbuild/package.json +0 -46
- package/packages/types/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/types/node_modules/esbuild/README.md +0 -3
- package/packages/types/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/types/node_modules/esbuild/install.js +0 -285
- package/packages/types/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/types/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/types/node_modules/esbuild/package.json +0 -46
- package/packages/utils/node_modules/esbuild/LICENSE.md +0 -21
- package/packages/utils/node_modules/esbuild/README.md +0 -3
- package/packages/utils/node_modules/esbuild/bin/esbuild +0 -220
- package/packages/utils/node_modules/esbuild/install.js +0 -285
- package/packages/utils/node_modules/esbuild/lib/main.d.ts +0 -705
- package/packages/utils/node_modules/esbuild/lib/main.js +0 -2239
- package/packages/utils/node_modules/esbuild/package.json +0 -46
package/src/auth/interfaces.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface UserData {
|
|
|
20
20
|
emailVerified: boolean;
|
|
21
21
|
emailVerificationToken?: string | null;
|
|
22
22
|
emailVerificationSentAt?: Date | null;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
23
24
|
createdAt: Date;
|
|
24
25
|
updatedAt: Date;
|
|
25
26
|
}
|
|
@@ -33,6 +34,7 @@ export interface CreateUserData {
|
|
|
33
34
|
displayName?: string;
|
|
34
35
|
photoUrl?: string;
|
|
35
36
|
emailVerified?: boolean;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
package/src/auth/jwt.ts
CHANGED
|
@@ -42,7 +42,9 @@ export function configureJwt(config: JwtConfig): void {
|
|
|
42
42
|
"example-secret",
|
|
43
43
|
"please-change-me",
|
|
44
44
|
"replace-this-with-a-real-secret",
|
|
45
|
-
"default-secret"
|
|
45
|
+
"default-secret",
|
|
46
|
+
"rebase_saas_jwt_secret_must_be_long_long_long_long",
|
|
47
|
+
"rebase_saas_service_key_must_be_long_long_long_long"
|
|
46
48
|
]);
|
|
47
49
|
|
|
48
50
|
if (!config.secret || config.secret.length < 32) {
|
package/src/auth/middleware.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { MiddlewareHandler, Context } from "hono";
|
|
|
2
2
|
import { DataDriver } from "@rebasepro/types";
|
|
3
3
|
import { verifyAccessToken, AccessTokenPayload } from "./jwt";
|
|
4
4
|
import { HonoEnv } from "../api/types";
|
|
5
|
-
import {
|
|
5
|
+
import { scopeDataDriver } from "./rls-scope";
|
|
6
|
+
import { safeCompare } from "./crypto-utils";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Result from a custom auth validator.
|
|
@@ -202,23 +203,6 @@ export function extractUserFromToken(token: string): AccessTokenPayload | null {
|
|
|
202
203
|
return verifyAccessToken(token);
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
/**
|
|
206
|
-
* Helper to scope a DataDriver via withAuth() for RLS.
|
|
207
|
-
* SECURITY: If withAuth() is available but fails, the error is re-thrown
|
|
208
|
-
* so the request is denied rather than proceeding with unscoped access.
|
|
209
|
-
*/
|
|
210
|
-
async function scopeDataDriver(
|
|
211
|
-
driver: DataDriver,
|
|
212
|
-
user: { uid: string; roles?: string[] }
|
|
213
|
-
): Promise<DataDriver> {
|
|
214
|
-
if ("withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function") {
|
|
215
|
-
// Fail closed — do NOT catch and swallow errors here.
|
|
216
|
-
// If RLS scoping fails the request must be rejected.
|
|
217
|
-
return await (driver as unknown as { withAuth: (user: Record<string, unknown>) => Promise<DataDriver> }).withAuth(user);
|
|
218
|
-
}
|
|
219
|
-
return driver;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
206
|
/**
|
|
223
207
|
* Create a configurable auth middleware that handles:
|
|
224
208
|
* 1. Token extraction (via custom validator or JWT Bearer token)
|
|
@@ -237,34 +221,6 @@ async function scopeDataDriver(
|
|
|
237
221
|
* This is the single source of truth for HTTP auth in Rebase.
|
|
238
222
|
* Use this instead of manually parsing tokens in route handlers.
|
|
239
223
|
*/
|
|
240
|
-
/**
|
|
241
|
-
* Constant-time string comparison to prevent timing attacks on service keys.
|
|
242
|
-
*
|
|
243
|
-
* We intentionally avoid early-returning on length mismatch because that
|
|
244
|
-
* would leak the key's length through timing differences. Instead, both
|
|
245
|
-
* inputs are padded to the same length so `timingSafeEqual` always runs
|
|
246
|
-
* over equal-length buffers.
|
|
247
|
-
*/
|
|
248
|
-
function safeCompare(a: string, b: string): boolean {
|
|
249
|
-
const maxLen = Math.max(a.length, b.length);
|
|
250
|
-
// Pad both to maxLen so timingSafeEqual always compares equal-length buffers.
|
|
251
|
-
// If the original lengths differ the result will be false due to the padding
|
|
252
|
-
// difference, but the comparison still takes constant time.
|
|
253
|
-
const bufA = Buffer.alloc(maxLen);
|
|
254
|
-
const bufB = Buffer.alloc(maxLen);
|
|
255
|
-
bufA.write(a);
|
|
256
|
-
bufB.write(b);
|
|
257
|
-
try {
|
|
258
|
-
const isEqual = timingSafeEqual(bufA, bufB);
|
|
259
|
-
// Even though padding makes mismatched-length strings compare as
|
|
260
|
-
// different bytes, we still need to verify lengths match to avoid
|
|
261
|
-
// a padded shorter string accidentally equaling a longer one that
|
|
262
|
-
// has trailing null bytes.
|
|
263
|
-
return isEqual && a.length === b.length;
|
|
264
|
-
} catch {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
224
|
|
|
269
225
|
export function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
270
226
|
const { driver, requireAuth: enforceAuth = true, validator, serviceKey } = options;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RLS (Row-Level Security) scoping helper.
|
|
3
|
+
*
|
|
4
|
+
* DataDrivers may implement a `withAuth()` method that returns a scoped
|
|
5
|
+
* clone of the driver with RLS policies applied for the given user.
|
|
6
|
+
* This is database-specific (e.g. Postgres SET LOCAL ROLE) and is not
|
|
7
|
+
* part of the core DataDriver interface.
|
|
8
|
+
*
|
|
9
|
+
* This module provides the shared duck-typing logic used by the
|
|
10
|
+
* adapter-aware middleware.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DataDriver } from "@rebasepro/types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A DataDriver that supports RLS scoping via `withAuth()`.
|
|
19
|
+
*
|
|
20
|
+
* This is not part of the public DataDriver interface because not all
|
|
21
|
+
* database implementations support RLS. Drivers that do (e.g. Postgres)
|
|
22
|
+
* extend DataDriver with this method.
|
|
23
|
+
*/
|
|
24
|
+
interface RLSScopedDriver extends DataDriver {
|
|
25
|
+
withAuth(user: { uid: string; roles?: string[] }): Promise<DataDriver>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if the driver supports RLS scoping via `withAuth()`.
|
|
30
|
+
*/
|
|
31
|
+
function isRLSScopedDriver(driver: DataDriver): driver is RLSScopedDriver {
|
|
32
|
+
return "withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Scope a DataDriver via `withAuth()` for RLS.
|
|
37
|
+
*
|
|
38
|
+
* SECURITY: If `withAuth()` is available but fails, the error is re-thrown
|
|
39
|
+
* so the request is **denied** rather than proceeding with unscoped access
|
|
40
|
+
* (fail-closed behavior).
|
|
41
|
+
*
|
|
42
|
+
* If the driver does not support RLS, the original driver is returned.
|
|
43
|
+
*
|
|
44
|
+
* @param driver - The DataDriver to scope.
|
|
45
|
+
* @param user - The authenticated user identity for RLS.
|
|
46
|
+
* @returns The RLS-scoped DataDriver (or the original if RLS is unsupported).
|
|
47
|
+
*/
|
|
48
|
+
export async function scopeDataDriver(
|
|
49
|
+
driver: DataDriver,
|
|
50
|
+
user: { uid: string; roles?: string[] },
|
|
51
|
+
): Promise<DataDriver> {
|
|
52
|
+
if (isRLSScopedDriver(driver)) {
|
|
53
|
+
// Fail closed — do NOT catch and swallow errors here.
|
|
54
|
+
// If RLS scoping fails the request must be rejected.
|
|
55
|
+
return await driver.withAuth(user);
|
|
56
|
+
}
|
|
57
|
+
return driver;
|
|
58
|
+
}
|
package/src/auth/routes.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { ApiError, errorHandler } from "../api/errors";
|
|
|
3
3
|
import { randomBytes, createHash } from "crypto";
|
|
4
4
|
import type { AuthRepository, OAuthProvider } from "./interfaces";
|
|
5
5
|
import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
|
|
6
|
-
import {
|
|
6
|
+
import type { AuthOverrides } from "./auth-overrides";
|
|
7
|
+
import { resolveAuthOverrides } from "./auth-overrides";
|
|
7
8
|
import { requireAuth } from "./middleware";
|
|
8
9
|
import { EmailService, EmailConfig } from "../email";
|
|
9
10
|
import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate } from "../email/templates";
|
|
@@ -23,9 +24,15 @@ export interface AuthModuleConfig {
|
|
|
23
24
|
/** Default role ID to assign to new users (default: none). Must NOT be "admin". */
|
|
24
25
|
defaultRole?: string;
|
|
25
26
|
/** Optional array of OAuth providers */
|
|
26
|
-
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
oauthProviders?: OAuthProvider<any>[];
|
|
27
29
|
/** When true, blocks all self-registration regardless of `allowRegistration`. */
|
|
28
30
|
disableSelfRegistration?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Auth overrides for customizing password hashing, credential
|
|
33
|
+
* verification, lifecycle hooks, etc.
|
|
34
|
+
*/
|
|
35
|
+
overrides?: AuthOverrides;
|
|
29
36
|
/**
|
|
30
37
|
* Callback that checks if bootstrap has already been completed.
|
|
31
38
|
* Used by GET /auth/config to report `needsSetup` status.
|
|
@@ -38,7 +45,7 @@ export interface AuthModuleConfig {
|
|
|
38
45
|
* Helper to build standard auth response output
|
|
39
46
|
*/
|
|
40
47
|
function buildAuthResponse(
|
|
41
|
-
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null },
|
|
48
|
+
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
|
|
42
49
|
roleIds: string[],
|
|
43
50
|
accessToken: string,
|
|
44
51
|
refreshToken: string
|
|
@@ -49,7 +56,8 @@ function buildAuthResponse(
|
|
|
49
56
|
email: user.email,
|
|
50
57
|
displayName: user.displayName ?? null,
|
|
51
58
|
photoURL: user.photoUrl ?? null,
|
|
52
|
-
roles: roleIds
|
|
59
|
+
roles: roleIds,
|
|
60
|
+
metadata: user.metadata ?? {}
|
|
53
61
|
},
|
|
54
62
|
tokens: {
|
|
55
63
|
accessToken,
|
|
@@ -93,7 +101,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
93
101
|
router.onError(errorHandler);
|
|
94
102
|
|
|
95
103
|
const authRepo = config.authRepo;
|
|
96
|
-
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
104
|
+
const { emailService, emailConfig, allowRegistration = false, overrides } = config;
|
|
105
|
+
const ops = resolveAuthOverrides(overrides);
|
|
97
106
|
|
|
98
107
|
// ── Zod input schemas ──────────────────────────────────────────────
|
|
99
108
|
const registerSchema = z.object({
|
|
@@ -214,7 +223,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
// Validate password strength
|
|
217
|
-
const passwordValidation = validatePasswordStrength(password);
|
|
226
|
+
const passwordValidation = ops.validatePasswordStrength(password);
|
|
218
227
|
if (!passwordValidation.valid) {
|
|
219
228
|
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
220
229
|
}
|
|
@@ -226,12 +235,16 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
// Create user
|
|
229
|
-
const passwordHash = await hashPassword(password);
|
|
230
|
-
|
|
238
|
+
const passwordHash = await ops.hashPassword(password);
|
|
239
|
+
let createData: import("./interfaces").CreateUserData = {
|
|
231
240
|
email: email.toLowerCase(),
|
|
232
241
|
passwordHash,
|
|
233
242
|
displayName: displayName || undefined
|
|
234
|
-
}
|
|
243
|
+
};
|
|
244
|
+
if (overrides?.beforeUserCreate) {
|
|
245
|
+
createData = await overrides.beforeUserCreate(createData);
|
|
246
|
+
}
|
|
247
|
+
const user = await authRepo.createUser(createData);
|
|
235
248
|
|
|
236
249
|
// Auto-bootstrap: if this is the very first user in the system, promote to admin.
|
|
237
250
|
// This avoids the chicken-and-egg problem where the first user has no permissions
|
|
@@ -256,6 +269,20 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
256
269
|
sendWelcomeEmail({ email: user.email,
|
|
257
270
|
displayName: user.displayName });
|
|
258
271
|
|
|
272
|
+
// Fire afterUserCreate hook (fire-and-forget)
|
|
273
|
+
if (overrides?.afterUserCreate) {
|
|
274
|
+
overrides.afterUserCreate(user).catch(err => {
|
|
275
|
+
console.error("[AuthOverrides] afterUserCreate error:", err instanceof Error ? err.message : err);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Fire onAuthenticated hook (fire-and-forget)
|
|
280
|
+
if (overrides?.onAuthenticated) {
|
|
281
|
+
overrides.onAuthenticated(user, "register").catch(err => {
|
|
282
|
+
console.error("[AuthOverrides] onAuthenticated error:", err instanceof Error ? err.message : err);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
259
286
|
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
|
|
260
287
|
});
|
|
261
288
|
|
|
@@ -266,18 +293,29 @@ displayName: user.displayName });
|
|
|
266
293
|
router.post("/login", defaultAuthLimiter, async (c) => {
|
|
267
294
|
const { email, password } = parseBody(loginSchema, await c.req.json());
|
|
268
295
|
|
|
269
|
-
|
|
270
|
-
if (!user) {
|
|
271
|
-
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
272
|
-
}
|
|
296
|
+
let user;
|
|
273
297
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
if (overrides?.verifyCredentials) {
|
|
299
|
+
// Full credential verification override
|
|
300
|
+
user = await overrides.verifyCredentials(email, password, authRepo);
|
|
301
|
+
if (!user) {
|
|
302
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
// Default: email lookup + password hash verification
|
|
306
|
+
user = await authRepo.getUserByEmail(email);
|
|
307
|
+
if (!user) {
|
|
308
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
309
|
+
}
|
|
277
310
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
if (!user.passwordHash) {
|
|
312
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const isValidPassword = await ops.verifyPassword(password, user.passwordHash);
|
|
316
|
+
if (!isValidPassword) {
|
|
317
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
318
|
+
}
|
|
281
319
|
}
|
|
282
320
|
|
|
283
321
|
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
@@ -286,6 +324,13 @@ displayName: user.displayName });
|
|
|
286
324
|
c.req.header("x-forwarded-for") || "unknown"
|
|
287
325
|
);
|
|
288
326
|
|
|
327
|
+
// Fire onAuthenticated hook (fire-and-forget)
|
|
328
|
+
if (overrides?.onAuthenticated) {
|
|
329
|
+
overrides.onAuthenticated(user, "login").catch(err => {
|
|
330
|
+
console.error("[AuthOverrides] onAuthenticated error:", err instanceof Error ? err.message : err);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
289
334
|
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
|
|
290
335
|
});
|
|
291
336
|
|
|
@@ -434,7 +479,7 @@ displayName: user.displayName }, appName);
|
|
|
434
479
|
const { token, password } = parseBody(resetPasswordSchema, await c.req.json());
|
|
435
480
|
|
|
436
481
|
// Validate password strength
|
|
437
|
-
const passwordValidation = validatePasswordStrength(password);
|
|
482
|
+
const passwordValidation = ops.validatePasswordStrength(password);
|
|
438
483
|
if (!passwordValidation.valid) {
|
|
439
484
|
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
440
485
|
}
|
|
@@ -448,7 +493,7 @@ displayName: user.displayName }, appName);
|
|
|
448
493
|
}
|
|
449
494
|
|
|
450
495
|
// Update password
|
|
451
|
-
const passwordHash = await hashPassword(password);
|
|
496
|
+
const passwordHash = await ops.hashPassword(password);
|
|
452
497
|
await authRepo.updatePassword(storedToken.userId, passwordHash);
|
|
453
498
|
|
|
454
499
|
// Mark token as used
|
|
@@ -480,19 +525,19 @@ message: "Password has been reset successfully" });
|
|
|
480
525
|
}
|
|
481
526
|
|
|
482
527
|
// Verify old password
|
|
483
|
-
const isValidOldPassword = await verifyPassword(oldPassword, user.passwordHash);
|
|
528
|
+
const isValidOldPassword = await ops.verifyPassword(oldPassword, user.passwordHash);
|
|
484
529
|
if (!isValidOldPassword) {
|
|
485
530
|
throw ApiError.unauthorized("Current password is incorrect", "INVALID_CREDENTIALS");
|
|
486
531
|
}
|
|
487
532
|
|
|
488
533
|
// Validate new password strength
|
|
489
|
-
const passwordValidation = validatePasswordStrength(newPassword);
|
|
534
|
+
const passwordValidation = ops.validatePasswordStrength(newPassword);
|
|
490
535
|
if (!passwordValidation.valid) {
|
|
491
536
|
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
492
537
|
}
|
|
493
538
|
|
|
494
539
|
// Update password
|
|
495
|
-
const passwordHash = await hashPassword(newPassword);
|
|
540
|
+
const passwordHash = await ops.hashPassword(newPassword);
|
|
496
541
|
await authRepo.updatePassword(user.id, passwordHash);
|
|
497
542
|
|
|
498
543
|
// Invalidate all refresh tokens (security: log out all sessions)
|
|
@@ -727,7 +772,8 @@ message: "Session revoked successfully" });
|
|
|
727
772
|
displayName: result.user.displayName,
|
|
728
773
|
photoURL: result.user.photoUrl,
|
|
729
774
|
emailVerified: result.user.emailVerified,
|
|
730
|
-
roles: result.roles.map(r => r.id)
|
|
775
|
+
roles: result.roles.map(r => r.id),
|
|
776
|
+
metadata: result.user.metadata ?? {}
|
|
731
777
|
}
|
|
732
778
|
});
|
|
733
779
|
});
|
|
@@ -765,7 +811,8 @@ message: "Session revoked successfully" });
|
|
|
765
811
|
displayName: result.user.displayName,
|
|
766
812
|
photoURL: result.user.photoUrl,
|
|
767
813
|
emailVerified: result.user.emailVerified,
|
|
768
|
-
roles: result.roles.map(r => r.id)
|
|
814
|
+
roles: result.roles.map(r => r.id),
|
|
815
|
+
metadata: result.user.metadata ?? {}
|
|
769
816
|
}
|
|
770
817
|
});
|
|
771
818
|
});
|
|
@@ -788,18 +835,13 @@ message: "Session revoked successfully" });
|
|
|
788
835
|
// Registration is allowed when explicitly enabled OR during initial setup
|
|
789
836
|
const registrationAllowed = needsSetup || !!allowRegistration;
|
|
790
837
|
|
|
791
|
-
// Build
|
|
792
|
-
// Also maintain legacy boolean fields for backward compatibility.
|
|
838
|
+
// Build the list of enabled OAuth providers for frontend discovery.
|
|
793
839
|
const enabledProviders = (config.oauthProviders || []).map(p => p.id);
|
|
794
840
|
|
|
795
841
|
return c.json({
|
|
796
842
|
needsSetup,
|
|
797
843
|
registrationEnabled: registrationAllowed,
|
|
798
|
-
// Legacy fields (kept for backward compat)
|
|
799
|
-
googleEnabled: enabledProviders.includes("google"),
|
|
800
|
-
linkedinEnabled: enabledProviders.includes("linkedin"),
|
|
801
844
|
emailServiceEnabled: isEmailConfigured(),
|
|
802
|
-
// New: complete list of available OAuth providers
|
|
803
845
|
enabledProviders
|
|
804
846
|
});
|
|
805
847
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
|
|
2
2
|
import { CronScheduler, validateCronExpression } from "./cron-scheduler";
|
|
3
|
-
import type { CronJobDefinition } from "@rebasepro/types";
|
|
3
|
+
import type { CronJobDefinition, CronJobLogEntry } from "@rebasepro/types";
|
|
4
4
|
import type { LoadedCronJob } from "./cron-loader";
|
|
5
5
|
|
|
6
6
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
@@ -475,12 +475,12 @@ describe("CronScheduler", () => {
|
|
|
475
475
|
beforeEach(() => { jest.useRealTimers(); });
|
|
476
476
|
|
|
477
477
|
it("persists logs to store after execution", async () => {
|
|
478
|
-
const insertLog = jest.fn<(entry:
|
|
478
|
+
const insertLog = jest.fn<(entry: CronJobLogEntry) => Promise<void>>().mockResolvedValue(undefined);
|
|
479
479
|
const mockStore = {
|
|
480
480
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
481
481
|
insertLog,
|
|
482
|
-
fetchLogs: jest.fn<() => Promise<
|
|
483
|
-
fetchJobStats: jest.fn<() => Promise<Map<string,
|
|
482
|
+
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
483
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
|
|
484
484
|
};
|
|
485
485
|
scheduler.setStore(mockStore);
|
|
486
486
|
scheduler.registerJobs([makeJob("persisted")]);
|
|
@@ -493,8 +493,8 @@ describe("CronScheduler", () => {
|
|
|
493
493
|
const mockStore = {
|
|
494
494
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
495
495
|
insertLog: jest.fn<() => Promise<void>>().mockRejectedValue(new Error("DB down")),
|
|
496
|
-
fetchLogs: jest.fn<() => Promise<
|
|
497
|
-
fetchJobStats: jest.fn<() => Promise<Map<string,
|
|
496
|
+
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
497
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
|
|
498
498
|
};
|
|
499
499
|
scheduler.setStore(mockStore);
|
|
500
500
|
scheduler.registerJobs([makeJob("resilient")]);
|
|
@@ -504,13 +504,13 @@ describe("CronScheduler", () => {
|
|
|
504
504
|
});
|
|
505
505
|
|
|
506
506
|
it("seeds counters from store on start", async () => {
|
|
507
|
-
const stats = new Map<string,
|
|
507
|
+
const stats = new Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>();
|
|
508
508
|
stats.set("seeded", { totalRuns: 42, totalFailures: 3, lastRunAt: "2026-01-01T00:00:00Z" });
|
|
509
509
|
const mockStore = {
|
|
510
510
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
511
511
|
insertLog: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
512
|
-
fetchLogs: jest.fn<() => Promise<
|
|
513
|
-
fetchJobStats: jest.fn<() => Promise<Map<string,
|
|
512
|
+
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
513
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats),
|
|
514
514
|
};
|
|
515
515
|
scheduler.setStore(mockStore);
|
|
516
516
|
scheduler.registerJobs([makeJob("seeded")]);
|
|
@@ -15,7 +15,7 @@ import type { CronStore } from "./cron-store";
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Expand a single cron field into an ordered array of allowed values.
|
|
18
|
-
* Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`,
|
|
18
|
+
* Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*\/S`, and comma-separated combinations.
|
|
19
19
|
*/
|
|
20
20
|
function expandCronField(field: string, min: number, max: number): number[] {
|
|
21
21
|
const results = new Set<number>();
|