@rebasepro/server-core 0.7.0 → 0.9.0
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/README.md +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
package/src/auth/routes.ts
DELETED
|
@@ -1,802 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { ApiError, errorHandler } from "../api/errors";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
|
-
import { generateSecureToken, hashToken } from "./admin-user-ops";
|
|
5
|
-
import type { AuthRepository, OAuthProvider, CreateUserData } from "./interfaces";
|
|
6
|
-
import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
|
|
7
|
-
import type { AuthHooks } from "./auth-hooks";
|
|
8
|
-
import { resolveAuthHooks } from "./auth-hooks";
|
|
9
|
-
import { requireAuth } from "./middleware";
|
|
10
|
-
import { EmailService, EmailConfig } from "../email";
|
|
11
|
-
import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate } from "../email/templates";
|
|
12
|
-
import { HonoEnv } from "../api/types";
|
|
13
|
-
import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import { logger } from "../utils/logger";
|
|
16
|
-
import { mountMfaRoutes } from "./mfa-routes";
|
|
17
|
-
import { mountSessionRoutes } from "./session-routes";
|
|
18
|
-
import { mountMagicLinkRoutes } from "./magic-link-routes";
|
|
19
|
-
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Shared configuration for auth and admin route factories.
|
|
23
|
-
*/
|
|
24
|
-
export interface AuthModuleConfig {
|
|
25
|
-
authRepo: AuthRepository;
|
|
26
|
-
emailService?: EmailService;
|
|
27
|
-
emailConfig?: EmailConfig;
|
|
28
|
-
/** Allow new user registration (default: false). */
|
|
29
|
-
allowRegistration?: boolean;
|
|
30
|
-
/** Default role ID to assign to new users (default: none). Must NOT be "admin". */
|
|
31
|
-
defaultRole?: string;
|
|
32
|
-
/** Optional array of OAuth providers */
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
-
oauthProviders?: OAuthProvider<any>[];
|
|
35
|
-
/** When true, blocks all self-registration regardless of `allowRegistration`. */
|
|
36
|
-
disableSelfRegistration?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Auth hooks for customizing password hashing, credential
|
|
39
|
-
* verification, lifecycle hooks, etc.
|
|
40
|
-
*/
|
|
41
|
-
authHooks?: AuthHooks;
|
|
42
|
-
/**
|
|
43
|
-
* Callback that checks if bootstrap has already been completed.
|
|
44
|
-
* Used by GET /auth/config to report `needsSetup` status.
|
|
45
|
-
* When not provided, falls back to checking if any users exist.
|
|
46
|
-
*/
|
|
47
|
-
isBootstrapCompleted?: () => Promise<boolean>;
|
|
48
|
-
/** Enable magic link (passwordless email) login. Requires email service. */
|
|
49
|
-
enableMagicLink?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Helper to build standard auth response output
|
|
54
|
-
*/
|
|
55
|
-
function buildAuthResponse(
|
|
56
|
-
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
|
|
57
|
-
roleIds: string[],
|
|
58
|
-
accessToken: string,
|
|
59
|
-
refreshToken: string
|
|
60
|
-
): AuthResponsePayload {
|
|
61
|
-
return {
|
|
62
|
-
user: {
|
|
63
|
-
uid: user.id,
|
|
64
|
-
email: user.email,
|
|
65
|
-
displayName: user.displayName ?? null,
|
|
66
|
-
photoURL: user.photoUrl ?? null,
|
|
67
|
-
roles: roleIds,
|
|
68
|
-
metadata: user.metadata ?? {}
|
|
69
|
-
},
|
|
70
|
-
tokens: {
|
|
71
|
-
accessToken,
|
|
72
|
-
refreshToken,
|
|
73
|
-
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Get password reset token expiry (1 hour from now)
|
|
81
|
-
*/
|
|
82
|
-
function getPasswordResetExpiry(): Date {
|
|
83
|
-
return new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
87
|
-
if (config.defaultRole === "admin") {
|
|
88
|
-
throw new Error("CRITICAL SECURITY ERROR: defaultRole cannot be 'admin'. Administrative privilege escalation via registration is strictly forbidden. Use the POST /admin/bootstrap endpoint to promote the initial administrator.");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const router = new Hono<HonoEnv>();
|
|
92
|
-
|
|
93
|
-
// Attach Rebase error handler to ensure ApiError exceptions are correctly
|
|
94
|
-
// formatted instead of caught by Hono's default error handler.
|
|
95
|
-
// Hono's onError does NOT propagate from parent to child routers.
|
|
96
|
-
router.onError(errorHandler);
|
|
97
|
-
|
|
98
|
-
const authRepo = config.authRepo;
|
|
99
|
-
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
100
|
-
const ops = resolveAuthHooks(config.authHooks);
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Apply the `transformAuthResponse` hook if provided.
|
|
104
|
-
*
|
|
105
|
-
* Errors are caught and logged — the untransformed response is returned
|
|
106
|
-
* as a graceful fallback so auth never breaks due to a hook failure.
|
|
107
|
-
*/
|
|
108
|
-
async function applyTransformHook(
|
|
109
|
-
response: AuthResponsePayload,
|
|
110
|
-
method: TransformAuthResponseContext["method"],
|
|
111
|
-
request: Request,
|
|
112
|
-
userId: string
|
|
113
|
-
): Promise<AuthResponsePayload> {
|
|
114
|
-
if (!ops.transformAuthResponse) return response;
|
|
115
|
-
try {
|
|
116
|
-
return await ops.transformAuthResponse(response, { userId, method, request });
|
|
117
|
-
} catch (err) {
|
|
118
|
-
logger.error("[AuthHooks] transformAuthResponse error", {
|
|
119
|
-
error: err instanceof Error ? err.message : err
|
|
120
|
-
});
|
|
121
|
-
return response;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Zod input schemas ──────────────────────────────────────────────
|
|
126
|
-
const registerSchema = z.object({
|
|
127
|
-
email: z.string().email("Invalid email address").max(255),
|
|
128
|
-
password: z.string().min(1, "Password is required").max(128),
|
|
129
|
-
displayName: z.string().max(255).optional()
|
|
130
|
-
});
|
|
131
|
-
const loginSchema = z.object({
|
|
132
|
-
email: z.string().email("Invalid email address").max(255),
|
|
133
|
-
password: z.string().min(1, "Password is required").max(128)
|
|
134
|
-
});
|
|
135
|
-
const forgotPasswordSchema = z.object({
|
|
136
|
-
email: z.string().email("Invalid email address").max(255)
|
|
137
|
-
});
|
|
138
|
-
const resetPasswordSchema = z.object({
|
|
139
|
-
token: z.string().min(1, "Token is required"),
|
|
140
|
-
password: z.string().min(1, "Password is required").max(128)
|
|
141
|
-
});
|
|
142
|
-
const changePasswordSchema = z.object({
|
|
143
|
-
oldPassword: z.string().min(1, "Old password is required").max(128),
|
|
144
|
-
newPassword: z.string().min(1, "New password is required").max(128)
|
|
145
|
-
});
|
|
146
|
-
const refreshSchema = z.object({
|
|
147
|
-
refreshToken: z.string().min(1, "Refresh token is required")
|
|
148
|
-
});
|
|
149
|
-
const logoutSchema = z.object({
|
|
150
|
-
refreshToken: z.string().optional()
|
|
151
|
-
});
|
|
152
|
-
const updateProfileSchema = z.object({
|
|
153
|
-
displayName: z.string().max(255).optional(),
|
|
154
|
-
photoURL: z.string().url().max(2048).optional()
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
/** Parse a Zod schema against the request body, throwing ApiError on failure */
|
|
158
|
-
function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {
|
|
159
|
-
const result = schema.safeParse(body);
|
|
160
|
-
if (!result.success) {
|
|
161
|
-
const messages = result.error.issues.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
|
|
162
|
-
throw ApiError.badRequest(messages, "INVALID_INPUT");
|
|
163
|
-
}
|
|
164
|
-
return result.data;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Check if email service is configured
|
|
169
|
-
*/
|
|
170
|
-
function isEmailConfigured(): boolean {
|
|
171
|
-
return !!(emailService && emailService.isConfigured());
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Check if registration is allowed.
|
|
176
|
-
* Registration is only allowed when explicitly enabled via `allowRegistration`.
|
|
177
|
-
* First-user bootstrap must use POST /admin/bootstrap instead.
|
|
178
|
-
*/
|
|
179
|
-
function isRegistrationAllowed(): boolean {
|
|
180
|
-
if (config.disableSelfRegistration) return false;
|
|
181
|
-
return !!allowRegistration;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Send welcome email to a newly registered user (fire-and-forget).
|
|
186
|
-
*/
|
|
187
|
-
function sendWelcomeEmail(user: { email: string; displayName?: string | null }) {
|
|
188
|
-
if (!isEmailConfigured()) return;
|
|
189
|
-
const appName = emailConfig?.appName || "Rebase";
|
|
190
|
-
const loginUrl = emailConfig?.resetPasswordUrl || ""; // reuse base URL → the login / app page
|
|
191
|
-
const templateFn = emailConfig?.templates?.welcomeEmail;
|
|
192
|
-
const emailContent = templateFn
|
|
193
|
-
? templateFn(user, appName)
|
|
194
|
-
: getWelcomeEmailTemplate(user, appName, loginUrl ? `${loginUrl}/app` : undefined);
|
|
195
|
-
|
|
196
|
-
emailService!.send({
|
|
197
|
-
to: user.email,
|
|
198
|
-
subject: emailContent.subject,
|
|
199
|
-
html: emailContent.html,
|
|
200
|
-
text: emailContent.text
|
|
201
|
-
}).catch(err => {
|
|
202
|
-
logger.error("Failed to send welcome email", { error: err instanceof Error ? err.message : err });
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Helper to generate and store session tokens
|
|
208
|
-
*/
|
|
209
|
-
async function createSessionAndTokens(userId: string, userAgent: string, ipAddress: string) {
|
|
210
|
-
const roles = await authRepo.getUserRoles(userId);
|
|
211
|
-
const roleIds = roles.map(r => r.id);
|
|
212
|
-
|
|
213
|
-
// Allow customization of access token claims via hook
|
|
214
|
-
let customClaims: Record<string, unknown> | undefined;
|
|
215
|
-
if (ops.customizeAccessToken) {
|
|
216
|
-
const user = await authRepo.getUserById(userId);
|
|
217
|
-
if (user) {
|
|
218
|
-
const defaultClaims: Record<string, unknown> = { userId,
|
|
219
|
-
roles: roleIds,
|
|
220
|
-
aal: "aal1" };
|
|
221
|
-
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const accessToken = generateAccessToken(userId, roleIds, "aal1", customClaims);
|
|
226
|
-
const refreshToken = generateRefreshToken();
|
|
227
|
-
|
|
228
|
-
await authRepo.createRefreshToken(
|
|
229
|
-
userId,
|
|
230
|
-
hashRefreshToken(refreshToken),
|
|
231
|
-
getRefreshTokenExpiry(),
|
|
232
|
-
userAgent,
|
|
233
|
-
ipAddress
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
return { roleIds,
|
|
237
|
-
accessToken,
|
|
238
|
-
refreshToken };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* POST /auth/register
|
|
243
|
-
* Create a new account with email/password
|
|
244
|
-
*/
|
|
245
|
-
router.post("/register", defaultAuthLimiter, async (c) => {
|
|
246
|
-
const { email, password, displayName } = parseBody(registerSchema, await c.req.json());
|
|
247
|
-
|
|
248
|
-
// Hard kill switch — blocks registration regardless of allowRegistration
|
|
249
|
-
if (config.disableSelfRegistration) {
|
|
250
|
-
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check if registration is allowed (no bypass for empty databases)
|
|
254
|
-
if (!isRegistrationAllowed()) {
|
|
255
|
-
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Validate password strength
|
|
259
|
-
const passwordValidation = ops.validatePasswordStrength(password);
|
|
260
|
-
if (!passwordValidation.valid) {
|
|
261
|
-
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Check if email already exists
|
|
265
|
-
const existingUser = await authRepo.getUserByEmail(email);
|
|
266
|
-
if (existingUser) {
|
|
267
|
-
throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Create user
|
|
271
|
-
const passwordHash = await ops.hashPassword(password);
|
|
272
|
-
let createData: import("./interfaces").CreateUserData = {
|
|
273
|
-
email: email.toLowerCase(),
|
|
274
|
-
passwordHash,
|
|
275
|
-
displayName: displayName || undefined
|
|
276
|
-
};
|
|
277
|
-
if (ops.beforeUserCreate) {
|
|
278
|
-
createData = await ops.beforeUserCreate(createData);
|
|
279
|
-
}
|
|
280
|
-
const user = await authRepo.createUser(createData);
|
|
281
|
-
|
|
282
|
-
// Auto-bootstrap: if this is the very first user in the system, promote to admin.
|
|
283
|
-
// This avoids the chicken-and-egg problem where the first user has no permissions
|
|
284
|
-
// and no way to access the bootstrap endpoint from the UI.
|
|
285
|
-
const existingUsers = await authRepo.listUsers();
|
|
286
|
-
const isFirstUser = existingUsers.length === 1 && existingUsers[0].id === user.id;
|
|
287
|
-
|
|
288
|
-
if (isFirstUser) {
|
|
289
|
-
await authRepo.setUserRoles(user.id, ["admin"]);
|
|
290
|
-
} else if (config.defaultRole) {
|
|
291
|
-
// Assign configured default role (never auto-assign admin via registration)
|
|
292
|
-
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
296
|
-
user.id,
|
|
297
|
-
c.req.header("user-agent") || "unknown",
|
|
298
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
// Send welcome email (fire-and-forget, don't block registration)
|
|
302
|
-
sendWelcomeEmail({ email: user.email,
|
|
303
|
-
displayName: user.displayName });
|
|
304
|
-
|
|
305
|
-
// Fire afterUserCreate hook
|
|
306
|
-
if (ops.afterUserCreate) {
|
|
307
|
-
try {
|
|
308
|
-
await ops.afterUserCreate(user);
|
|
309
|
-
} catch (err) {
|
|
310
|
-
logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Fire onAuthenticated hook (fire-and-forget)
|
|
315
|
-
if (ops.onAuthenticated) {
|
|
316
|
-
ops.onAuthenticated(user, "register").catch(err => {
|
|
317
|
-
logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken);
|
|
322
|
-
const finalResponse = await applyTransformHook(authResponse, "register", c.req.raw, user.id);
|
|
323
|
-
return c.json(finalResponse, 201);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* POST /auth/login
|
|
328
|
-
* Login with email/password
|
|
329
|
-
*/
|
|
330
|
-
router.post("/login", defaultAuthLimiter, async (c) => {
|
|
331
|
-
const { email, password } = parseBody(loginSchema, await c.req.json());
|
|
332
|
-
|
|
333
|
-
// Call beforeLogin hook if provided (throw to reject)
|
|
334
|
-
if (ops.beforeLogin) {
|
|
335
|
-
await ops.beforeLogin(email, "login");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
let user;
|
|
339
|
-
|
|
340
|
-
if (ops.verifyCredentials) {
|
|
341
|
-
// Full credential verification override
|
|
342
|
-
user = await ops.verifyCredentials(email, password, authRepo);
|
|
343
|
-
if (!user) {
|
|
344
|
-
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
345
|
-
}
|
|
346
|
-
} else {
|
|
347
|
-
// Default: email lookup + password hash verification
|
|
348
|
-
user = await authRepo.getUserByEmail(email);
|
|
349
|
-
if (!user) {
|
|
350
|
-
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!user.passwordHash) {
|
|
354
|
-
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const isValidPassword = await ops.verifyPassword(password, user.passwordHash);
|
|
358
|
-
if (!isValidPassword) {
|
|
359
|
-
logger.warn("[Security Audit] Auth login failure", {
|
|
360
|
-
eventType: "auth.login.failure",
|
|
361
|
-
email,
|
|
362
|
-
userId: user.id
|
|
363
|
-
});
|
|
364
|
-
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
369
|
-
user.id,
|
|
370
|
-
c.req.header("user-agent") || "unknown",
|
|
371
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
// Fire onAuthenticated hook (fire-and-forget)
|
|
375
|
-
if (ops.onAuthenticated) {
|
|
376
|
-
ops.onAuthenticated(user, "login").catch(err => {
|
|
377
|
-
logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
logger.info("[Security Audit] Auth login success", {
|
|
382
|
-
eventType: "auth.login.success",
|
|
383
|
-
userId: user.id,
|
|
384
|
-
email
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken);
|
|
388
|
-
const finalResponse = await applyTransformHook(authResponse, "login", c.req.raw, user.id);
|
|
389
|
-
return c.json(finalResponse);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Dynamically mount OAuth provider routes
|
|
394
|
-
*/
|
|
395
|
-
if (config.oauthProviders && config.oauthProviders.length > 0) {
|
|
396
|
-
for (const provider of config.oauthProviders) {
|
|
397
|
-
router.post(`/${provider.id}`, defaultAuthLimiter, async (c) => {
|
|
398
|
-
const payload = parseBody(provider.schema, await c.req.json());
|
|
399
|
-
|
|
400
|
-
let externalUser;
|
|
401
|
-
try {
|
|
402
|
-
externalUser = await provider.verify(payload);
|
|
403
|
-
} catch (err: unknown) {
|
|
404
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
405
|
-
throw ApiError.unauthorized(`${provider.id} login failed: ${msg}`, "OAUTH_ERROR");
|
|
406
|
-
}
|
|
407
|
-
if (!externalUser) {
|
|
408
|
-
throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Find or create user
|
|
412
|
-
let user = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);
|
|
413
|
-
|
|
414
|
-
if (!user) {
|
|
415
|
-
// Check if email exists (link accounts)
|
|
416
|
-
user = await authRepo.getUserByEmail(externalUser.email);
|
|
417
|
-
|
|
418
|
-
if (user) {
|
|
419
|
-
// Only auto-link if the OAuth provider confirmed the email is verified
|
|
420
|
-
if (!externalUser.emailVerified) {
|
|
421
|
-
throw ApiError.forbidden(
|
|
422
|
-
"Cannot auto-link account: email not verified by the OAuth provider. Please log in with your password first and link the provider from your profile.",
|
|
423
|
-
"EMAIL_NOT_VERIFIED"
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
// Link Provider to existing account
|
|
427
|
-
await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
|
|
428
|
-
|
|
429
|
-
// Optional: Update profile info from external provider if empty
|
|
430
|
-
await authRepo.updateUser(user.id, {
|
|
431
|
-
displayName: user.displayName || externalUser.displayName || undefined,
|
|
432
|
-
photoUrl: user.photoUrl || externalUser.photoUrl || undefined
|
|
433
|
-
});
|
|
434
|
-
} else {
|
|
435
|
-
// Create new user
|
|
436
|
-
user = await authRepo.createUser({
|
|
437
|
-
email: externalUser.email.toLowerCase(),
|
|
438
|
-
displayName: externalUser.displayName || undefined,
|
|
439
|
-
photoUrl: externalUser.photoUrl || undefined
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
|
|
443
|
-
|
|
444
|
-
// Fire afterUserCreate hook
|
|
445
|
-
if (ops.afterUserCreate) {
|
|
446
|
-
try {
|
|
447
|
-
await ops.afterUserCreate(user);
|
|
448
|
-
} catch (err) {
|
|
449
|
-
logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Auto-bootstrap: first user in the system gets admin
|
|
454
|
-
const allUsers = await authRepo.listUsers();
|
|
455
|
-
const isFirstUser = allUsers.length === 1 && allUsers[0].id === user.id;
|
|
456
|
-
|
|
457
|
-
if (isFirstUser) {
|
|
458
|
-
await authRepo.setUserRoles(user.id, ["admin"]);
|
|
459
|
-
} else if (config.defaultRole) {
|
|
460
|
-
// Assign configured default role (never auto-assign admin via registration)
|
|
461
|
-
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Send welcome email for new OAuth users (fire-and-forget)
|
|
465
|
-
sendWelcomeEmail({ email: user.email,
|
|
466
|
-
displayName: user.displayName });
|
|
467
|
-
}
|
|
468
|
-
} else {
|
|
469
|
-
// Update profile info from external provider
|
|
470
|
-
await authRepo.updateUser(user.id, {
|
|
471
|
-
displayName: externalUser.displayName || user.displayName || undefined,
|
|
472
|
-
photoUrl: externalUser.photoUrl || user.photoUrl || undefined
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
477
|
-
user.id,
|
|
478
|
-
c.req.header("user-agent") || "unknown",
|
|
479
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken);
|
|
483
|
-
const finalResponse = await applyTransformHook(authResponse, "oauth", c.req.raw, user.id);
|
|
484
|
-
return c.json(finalResponse);
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* POST /auth/forgot-password
|
|
491
|
-
* Request password reset email
|
|
492
|
-
*/
|
|
493
|
-
router.post("/forgot-password", strictAuthLimiter, async (c) => {
|
|
494
|
-
const { email } = parseBody(forgotPasswordSchema, await c.req.json());
|
|
495
|
-
|
|
496
|
-
// Check if email service is configured
|
|
497
|
-
if (!isEmailConfigured()) {
|
|
498
|
-
throw ApiError.serviceUnavailable("Email service not configured. Password reset is not available.", "EMAIL_NOT_CONFIGURED");
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Always return success (security: don't reveal if email exists)
|
|
502
|
-
// But only send email if user exists
|
|
503
|
-
const user = await authRepo.getUserByEmail(email);
|
|
504
|
-
|
|
505
|
-
if (user) {
|
|
506
|
-
// Generate reset token
|
|
507
|
-
const token = generateSecureToken();
|
|
508
|
-
const tokenHash = hashToken(token);
|
|
509
|
-
const expiresAt = getPasswordResetExpiry();
|
|
510
|
-
|
|
511
|
-
await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
|
|
512
|
-
|
|
513
|
-
// Build reset URL
|
|
514
|
-
const baseUrl = emailConfig?.resetPasswordUrl || "";
|
|
515
|
-
const resetUrl = `${baseUrl}/reset-password?token=${token}`;
|
|
516
|
-
|
|
517
|
-
// Get email template
|
|
518
|
-
const appName = emailConfig?.appName || "Rebase";
|
|
519
|
-
const templateFn = emailConfig?.templates?.passwordReset;
|
|
520
|
-
const emailContent = templateFn
|
|
521
|
-
? templateFn(resetUrl, { email: user.email,
|
|
522
|
-
displayName: user.displayName })
|
|
523
|
-
: getPasswordResetTemplate(resetUrl, { email: user.email,
|
|
524
|
-
displayName: user.displayName }, appName);
|
|
525
|
-
|
|
526
|
-
// Send email
|
|
527
|
-
try {
|
|
528
|
-
await emailService!.send({
|
|
529
|
-
to: user.email,
|
|
530
|
-
subject: emailContent.subject,
|
|
531
|
-
html: emailContent.html,
|
|
532
|
-
text: emailContent.text
|
|
533
|
-
});
|
|
534
|
-
} catch (emailError: unknown) {
|
|
535
|
-
logger.error("Failed to send password reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
536
|
-
// Don't reveal email sending failure to client
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Always return success
|
|
541
|
-
return c.json({
|
|
542
|
-
success: true,
|
|
543
|
-
message: "If an account with that email exists, a password reset link has been sent."
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* POST /auth/reset-password
|
|
549
|
-
* Reset password using token
|
|
550
|
-
*/
|
|
551
|
-
router.post("/reset-password", strictAuthLimiter, async (c) => {
|
|
552
|
-
const { token, password } = parseBody(resetPasswordSchema, await c.req.json());
|
|
553
|
-
|
|
554
|
-
// Validate password strength
|
|
555
|
-
const passwordValidation = ops.validatePasswordStrength(password);
|
|
556
|
-
if (!passwordValidation.valid) {
|
|
557
|
-
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Find valid token
|
|
561
|
-
const tokenHash = hashToken(token);
|
|
562
|
-
const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
|
|
563
|
-
|
|
564
|
-
if (!storedToken) {
|
|
565
|
-
throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Update password
|
|
569
|
-
const passwordHash = await ops.hashPassword(password);
|
|
570
|
-
await authRepo.updatePassword(storedToken.userId, passwordHash);
|
|
571
|
-
|
|
572
|
-
// Mark token as used
|
|
573
|
-
await authRepo.markPasswordResetTokenUsed(tokenHash);
|
|
574
|
-
|
|
575
|
-
// Invalidate all refresh tokens (security: log out all sessions)
|
|
576
|
-
await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
|
|
577
|
-
|
|
578
|
-
// Fire onPasswordReset hook (fire-and-forget)
|
|
579
|
-
if (ops.onPasswordReset) {
|
|
580
|
-
ops.onPasswordReset(storedToken.userId).catch(err => {
|
|
581
|
-
logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
return c.json({ success: true,
|
|
586
|
-
message: "Password has been reset successfully" });
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* POST /auth/change-password
|
|
591
|
-
* Change password for authenticated user
|
|
592
|
-
*/
|
|
593
|
-
router.post("/change-password", requireAuth, async (c) => {
|
|
594
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
595
|
-
if (!userCtx) {
|
|
596
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());
|
|
600
|
-
|
|
601
|
-
// Get user
|
|
602
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
603
|
-
if (!user || !user.passwordHash) {
|
|
604
|
-
throw ApiError.badRequest("Cannot change password for this account", "INVALID_ACCOUNT");
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Verify old password
|
|
608
|
-
const isValidOldPassword = await ops.verifyPassword(oldPassword, user.passwordHash);
|
|
609
|
-
if (!isValidOldPassword) {
|
|
610
|
-
throw ApiError.unauthorized("Current password is incorrect", "INVALID_CREDENTIALS");
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Validate new password strength
|
|
614
|
-
const passwordValidation = ops.validatePasswordStrength(newPassword);
|
|
615
|
-
if (!passwordValidation.valid) {
|
|
616
|
-
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Update password
|
|
620
|
-
const passwordHash = await ops.hashPassword(newPassword);
|
|
621
|
-
await authRepo.updatePassword(user.id, passwordHash);
|
|
622
|
-
|
|
623
|
-
// Invalidate all refresh tokens (security: log out all sessions)
|
|
624
|
-
await authRepo.deleteAllRefreshTokensForUser(user.id);
|
|
625
|
-
|
|
626
|
-
return c.json({ success: true,
|
|
627
|
-
message: "Password has been changed successfully" });
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* POST /auth/send-verification
|
|
632
|
-
* Send email verification link (authenticated)
|
|
633
|
-
*/
|
|
634
|
-
router.post("/send-verification", requireAuth, async (c) => {
|
|
635
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
636
|
-
if (!userCtx) {
|
|
637
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Check if email service is configured
|
|
641
|
-
if (!isEmailConfigured()) {
|
|
642
|
-
throw ApiError.serviceUnavailable("Email service not configured. Email verification is not available.", "EMAIL_NOT_CONFIGURED");
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
646
|
-
if (!user) {
|
|
647
|
-
throw ApiError.notFound("User not found");
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (user.emailVerified) {
|
|
651
|
-
throw ApiError.badRequest("Email is already verified", "ALREADY_VERIFIED");
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Generate verification token
|
|
655
|
-
const token = generateSecureToken();
|
|
656
|
-
|
|
657
|
-
// Store hashed token in user record (raw token goes in the email URL)
|
|
658
|
-
await authRepo.setVerificationToken(user.id, hashToken(token));
|
|
659
|
-
|
|
660
|
-
// Build verification URL
|
|
661
|
-
const baseUrl = emailConfig?.verifyEmailUrl || "";
|
|
662
|
-
const verifyUrl = `${baseUrl}/verify-email?token=${token}`;
|
|
663
|
-
|
|
664
|
-
// Get email template
|
|
665
|
-
const appName = emailConfig?.appName || "Rebase";
|
|
666
|
-
const templateFn = emailConfig?.templates?.emailVerification;
|
|
667
|
-
const emailContent = templateFn
|
|
668
|
-
? templateFn(verifyUrl, { email: user.email,
|
|
669
|
-
displayName: user.displayName })
|
|
670
|
-
: getEmailVerificationTemplate(verifyUrl, { email: user.email,
|
|
671
|
-
displayName: user.displayName }, appName);
|
|
672
|
-
|
|
673
|
-
// Send email
|
|
674
|
-
await emailService!.send({
|
|
675
|
-
to: user.email,
|
|
676
|
-
subject: emailContent.subject,
|
|
677
|
-
html: emailContent.html,
|
|
678
|
-
text: emailContent.text
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
return c.json({ success: true,
|
|
682
|
-
message: "Verification email sent" });
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* GET /auth/verify-email
|
|
687
|
-
* Verify email address using token
|
|
688
|
-
*/
|
|
689
|
-
router.get("/verify-email", async (c) => {
|
|
690
|
-
const token = c.req.query("token");
|
|
691
|
-
|
|
692
|
-
if (!token) {
|
|
693
|
-
throw ApiError.badRequest("Verification token is required", "INVALID_INPUT");
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Find user by hashed verification token
|
|
697
|
-
const user = await authRepo.getUserByVerificationToken(hashToken(token));
|
|
698
|
-
if (!user) {
|
|
699
|
-
throw ApiError.badRequest("Invalid or expired verification token", "INVALID_TOKEN");
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Mark email as verified
|
|
703
|
-
await authRepo.setEmailVerified(user.id, true);
|
|
704
|
-
|
|
705
|
-
return c.json({ success: true,
|
|
706
|
-
message: "Email verified successfully" });
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* POST /auth/refresh
|
|
711
|
-
* Refresh access token using refresh token
|
|
712
|
-
*/
|
|
713
|
-
router.post("/refresh", async (c) => {
|
|
714
|
-
const { refreshToken } = parseBody(refreshSchema, await c.req.json());
|
|
715
|
-
|
|
716
|
-
const tokenHash = hashRefreshToken(refreshToken);
|
|
717
|
-
const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);
|
|
718
|
-
|
|
719
|
-
if (!storedToken) {
|
|
720
|
-
throw ApiError.unauthorized("Invalid refresh token", "INVALID_TOKEN");
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (new Date() > storedToken.expiresAt) {
|
|
724
|
-
await authRepo.deleteRefreshToken(tokenHash);
|
|
725
|
-
throw ApiError.unauthorized("Refresh token expired", "TOKEN_EXPIRED");
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Generate new tokens
|
|
729
|
-
const roles = await authRepo.getUserRoles(storedToken.userId);
|
|
730
|
-
const roleIds = roles.map(r => r.id);
|
|
731
|
-
|
|
732
|
-
// Allow customization of access token claims via hook
|
|
733
|
-
let customClaims: Record<string, unknown> | undefined;
|
|
734
|
-
if (ops.customizeAccessToken) {
|
|
735
|
-
const user = await authRepo.getUserById(storedToken.userId);
|
|
736
|
-
if (user) {
|
|
737
|
-
const defaultClaims: Record<string, unknown> = { userId: storedToken.userId,
|
|
738
|
-
roles: roleIds,
|
|
739
|
-
aal: "aal1" };
|
|
740
|
-
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const newAccessToken = generateAccessToken(storedToken.userId, roleIds, "aal1", customClaims);
|
|
745
|
-
const newRefreshToken = generateRefreshToken();
|
|
746
|
-
|
|
747
|
-
// Rotate refresh token (delete old, create new)
|
|
748
|
-
const userAgent = c.req.header("user-agent") || "unknown";
|
|
749
|
-
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
750
|
-
|
|
751
|
-
await authRepo.deleteRefreshToken(tokenHash);
|
|
752
|
-
await authRepo.createRefreshToken(
|
|
753
|
-
storedToken.userId,
|
|
754
|
-
hashRefreshToken(newRefreshToken),
|
|
755
|
-
getRefreshTokenExpiry(),
|
|
756
|
-
userAgent,
|
|
757
|
-
ipAddress
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
const refreshResponse: AuthResponsePayload = {
|
|
761
|
-
tokens: {
|
|
762
|
-
accessToken: newAccessToken,
|
|
763
|
-
refreshToken: newRefreshToken,
|
|
764
|
-
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
const finalResponse = await applyTransformHook(refreshResponse, "refresh", c.req.raw, storedToken.userId);
|
|
768
|
-
return c.json(finalResponse);
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
mountSessionRoutes({
|
|
772
|
-
router,
|
|
773
|
-
config,
|
|
774
|
-
ops,
|
|
775
|
-
parseBody,
|
|
776
|
-
buildAuthResponse,
|
|
777
|
-
createSessionAndTokens,
|
|
778
|
-
applyTransformHook
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
782
|
-
// MFA / TOTP
|
|
783
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
784
|
-
mountMfaRoutes(router, config, ops, parseBody, applyTransformHook);
|
|
785
|
-
|
|
786
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
787
|
-
// Magic Link (passwordless email login)
|
|
788
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
789
|
-
if (config.enableMagicLink) {
|
|
790
|
-
mountMagicLinkRoutes({
|
|
791
|
-
router,
|
|
792
|
-
config,
|
|
793
|
-
ops,
|
|
794
|
-
parseBody,
|
|
795
|
-
buildAuthResponse,
|
|
796
|
-
createSessionAndTokens,
|
|
797
|
-
applyTransformHook
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return router;
|
|
802
|
-
}
|