@rebasepro/server-core 0.5.0 → 0.6.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/dist/{server-core/src/api → api}/errors.d.ts +5 -2
- package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
- package/dist/{server-core/src/api → api}/types.d.ts +2 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
- package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
- package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
- package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/session-routes.d.ts +25 -0
- package/dist/backend-CIxN4FVm.js +15 -0
- package/dist/backend-CIxN4FVm.js.map +1 -0
- package/dist/chunk-Dze3rakg.js +42 -0
- package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
- package/dist/dist-CZKP-Xz4.js +832 -0
- package/dist/dist-CZKP-Xz4.js.map +1 -0
- package/dist/env.d.ts +102 -0
- package/dist/from-VbwD7xRf.js +3849 -0
- package/dist/from-VbwD7xRf.js.map +1 -0
- package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
- package/dist/index.es.js +60872 -50462
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +73156 -51298
- package/dist/index.umd.js.map +1 -1
- 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 +11 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
- package/dist/jwt-DHcQRGC3.js +4168 -0
- package/dist/jwt-DHcQRGC3.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-BeBSuOXN.js +125 -0
- package/dist/ms-BeBSuOXN.js.map +1 -0
- package/dist/multipart-parser-CedBDOeC.js +299 -0
- package/dist/multipart-parser-CedBDOeC.js.map +1 -0
- package/dist/src-COaj0G3P.js +1182 -0
- package/dist/src-COaj0G3P.js.map +1 -0
- package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
- package/dist/utils/request-id.d.ts +4 -0
- package/package.json +29 -29
- package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
- package/src/api/errors.ts +75 -19
- package/src/api/graphql/graphql-schema-generator.ts +15 -10
- package/src/api/logs-routes.ts +5 -2
- package/src/api/rest/api-generator-count.test.ts +21 -10
- package/src/api/rest/api-generator.ts +102 -26
- package/src/api/rest/query-parser.ts +18 -12
- package/src/api/server.ts +12 -1
- package/src/api/types.ts +2 -0
- package/src/auth/adapter-middleware.ts +17 -10
- package/src/auth/admin-user-ops.ts +236 -0
- package/src/auth/api-keys/api-key-middleware.ts +15 -9
- package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
- package/src/auth/api-keys/api-key-routes.ts +3 -3
- package/src/auth/api-keys/api-key-store.ts +14 -8
- package/src/auth/api-keys/index.ts +2 -2
- package/src/auth/apple-oauth.ts +4 -3
- package/src/auth/auth-hooks.ts +50 -10
- package/src/auth/bitbucket-oauth.ts +5 -4
- package/src/auth/builtin-auth-adapter.ts +60 -39
- package/src/auth/custom-auth-adapter.ts +4 -5
- package/src/auth/discord-oauth.ts +5 -4
- package/src/auth/facebook-oauth.ts +5 -4
- package/src/auth/github-oauth.ts +6 -5
- package/src/auth/gitlab-oauth.ts +5 -4
- package/src/auth/google-oauth.ts +2 -1
- package/src/auth/index.ts +6 -2
- package/src/auth/jwt.ts +5 -4
- package/src/auth/linkedin-oauth.ts +4 -3
- package/src/auth/mfa-routes.ts +299 -0
- package/src/auth/mfa.ts +2 -1
- package/src/auth/microsoft-oauth.ts +5 -4
- package/src/auth/middleware.ts +5 -4
- package/src/auth/rate-limiter.ts +1 -1
- package/src/auth/reset-password-admin.ts +144 -0
- package/src/auth/rls-scope.ts +1 -1
- package/src/auth/routes.ts +52 -596
- package/src/auth/session-routes.ts +341 -0
- package/src/auth/slack-oauth.ts +6 -5
- package/src/auth/spotify-oauth.ts +5 -4
- package/src/auth/twitter-oauth.ts +4 -3
- package/src/collections/loader.ts +5 -4
- package/src/cron/cron-scheduler.test.ts +23 -11
- package/src/cron/cron-scheduler.ts +14 -8
- package/src/cron/cron-store.ts +1 -1
- package/src/email/smtp-email-service.ts +3 -2
- package/src/env.ts +11 -10
- package/src/index.ts +1 -0
- package/src/init/docs.ts +47 -0
- package/src/init/health.ts +37 -0
- package/src/init/middlewares.ts +61 -0
- package/src/init/shutdown.ts +56 -0
- package/src/init/storage.ts +57 -0
- package/src/init.ts +85 -279
- package/src/serve-spa.ts +12 -5
- package/src/services/driver-registry.ts +4 -3
- package/src/storage/S3StorageController.ts +1 -2
- package/src/storage/image-transform.ts +25 -10
- package/src/storage/routes.ts +34 -11
- package/src/storage/storage-registry.ts +4 -3
- package/src/storage/tus-handler.ts +12 -12
- package/src/utils/request-id.ts +40 -0
- package/src/utils/request-logger.ts +6 -0
- package/test/api-generator.test.ts +90 -2
- package/test/api-key-permission-guard.test.ts +24 -12
- package/test/auth-routes.test.ts +5 -3
- package/test/backend-hooks-data.test.ts +99 -30
- package/test/custom-auth-adapter.test.ts +16 -15
- package/test/email-templates.test.ts +10 -5
- package/test/env.test.ts +10 -10
- package/test/function-loader.test.ts +7 -4
- package/test/graphql-schema-generator.test.ts +554 -0
- package/test/jwt-security.test.ts +1 -1
- package/test/query-parser.test.ts +0 -1
- package/test/reset-password-admin.test.ts +113 -0
- package/test/singleton.test.ts +5 -5
- package/test/smtp-email-service.test.ts +21 -21
- package/test/webhook-service.test.ts +22 -11
- package/tsconfig.json +2 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +10 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -14
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/index-Cr1D21av.js +0 -49
- package/dist/index-Cr1D21av.js.map +0 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
- package/dist/server-core/src/env.d.ts +0 -137
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -571
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -961
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1173
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/src/auth/admin-routes.ts +0 -534
- package/test/admin-routes.test.ts +0 -544
- package/test/backend-hooks-admin.test.ts +0 -388
- /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/jwt.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
- /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
- /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
- /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
- /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
- /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
- /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
- /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
package/src/auth/routes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { ApiError, errorHandler } from "../api/errors";
|
|
3
|
-
import { randomBytes
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { generateSecureToken, hashToken } from "./admin-user-ops";
|
|
4
5
|
import type { AuthRepository, OAuthProvider, CreateUserData } from "./interfaces";
|
|
5
6
|
import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
|
|
6
7
|
import type { AuthHooks } from "./auth-hooks";
|
|
@@ -11,7 +12,9 @@ import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmail
|
|
|
11
12
|
import { HonoEnv } from "../api/types";
|
|
12
13
|
import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
|
|
13
14
|
import { z } from "zod";
|
|
14
|
-
import {
|
|
15
|
+
import { logger } from "../utils/logger";
|
|
16
|
+
import { mountMfaRoutes } from "./mfa-routes";
|
|
17
|
+
import { mountSessionRoutes } from "./session-routes";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Shared configuration for auth and admin route factories.
|
|
@@ -68,19 +71,6 @@ function buildAuthResponse(
|
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
/**
|
|
72
|
-
* Generate a secure random token
|
|
73
|
-
*/
|
|
74
|
-
function generateSecureToken(): string {
|
|
75
|
-
return randomBytes(40).toString("hex");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Hash a token for database storage
|
|
80
|
-
*/
|
|
81
|
-
function hashToken(token: string): string {
|
|
82
|
-
return createHash("sha256").update(token).digest("hex");
|
|
83
|
-
}
|
|
84
74
|
|
|
85
75
|
/**
|
|
86
76
|
* Get password reset token expiry (1 hour from now)
|
|
@@ -102,8 +92,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
102
92
|
router.onError(errorHandler);
|
|
103
93
|
|
|
104
94
|
const authRepo = config.authRepo;
|
|
105
|
-
const { emailService, emailConfig, allowRegistration = false
|
|
106
|
-
const ops = resolveAuthHooks(authHooks);
|
|
95
|
+
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
96
|
+
const ops = resolveAuthHooks(config.authHooks);
|
|
107
97
|
|
|
108
98
|
// ── Zod input schemas ──────────────────────────────────────────────
|
|
109
99
|
const registerSchema = z.object({
|
|
@@ -141,7 +131,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
141
131
|
function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {
|
|
142
132
|
const result = schema.safeParse(body);
|
|
143
133
|
if (!result.success) {
|
|
144
|
-
const messages = result.error.
|
|
134
|
+
const messages = result.error.issues.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
|
|
145
135
|
throw ApiError.badRequest(messages, "INVALID_INPUT");
|
|
146
136
|
}
|
|
147
137
|
return result.data;
|
|
@@ -182,7 +172,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
182
172
|
html: emailContent.html,
|
|
183
173
|
text: emailContent.text
|
|
184
174
|
}).catch(err => {
|
|
185
|
-
|
|
175
|
+
logger.error("Failed to send welcome email", { error: err instanceof Error ? err.message : err });
|
|
186
176
|
});
|
|
187
177
|
}
|
|
188
178
|
|
|
@@ -195,11 +185,13 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
195
185
|
|
|
196
186
|
// Allow customization of access token claims via hook
|
|
197
187
|
let customClaims: Record<string, unknown> | undefined;
|
|
198
|
-
if (
|
|
188
|
+
if (ops.customizeAccessToken) {
|
|
199
189
|
const user = await authRepo.getUserById(userId);
|
|
200
190
|
if (user) {
|
|
201
|
-
const defaultClaims: Record<string, unknown> = { userId,
|
|
202
|
-
|
|
191
|
+
const defaultClaims: Record<string, unknown> = { userId,
|
|
192
|
+
roles: roleIds,
|
|
193
|
+
aal: "aal1" };
|
|
194
|
+
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
203
195
|
}
|
|
204
196
|
}
|
|
205
197
|
|
|
@@ -214,7 +206,9 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
214
206
|
ipAddress
|
|
215
207
|
);
|
|
216
208
|
|
|
217
|
-
return { roleIds,
|
|
209
|
+
return { roleIds,
|
|
210
|
+
accessToken,
|
|
211
|
+
refreshToken };
|
|
218
212
|
}
|
|
219
213
|
|
|
220
214
|
/**
|
|
@@ -253,8 +247,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
253
247
|
passwordHash,
|
|
254
248
|
displayName: displayName || undefined
|
|
255
249
|
};
|
|
256
|
-
if (
|
|
257
|
-
createData = await
|
|
250
|
+
if (ops.beforeUserCreate) {
|
|
251
|
+
createData = await ops.beforeUserCreate(createData);
|
|
258
252
|
}
|
|
259
253
|
const user = await authRepo.createUser(createData);
|
|
260
254
|
|
|
@@ -282,18 +276,18 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
282
276
|
displayName: user.displayName });
|
|
283
277
|
|
|
284
278
|
// Fire afterUserCreate hook
|
|
285
|
-
if (
|
|
279
|
+
if (ops.afterUserCreate) {
|
|
286
280
|
try {
|
|
287
|
-
await
|
|
281
|
+
await ops.afterUserCreate(user);
|
|
288
282
|
} catch (err) {
|
|
289
|
-
|
|
283
|
+
logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
|
|
290
284
|
}
|
|
291
285
|
}
|
|
292
286
|
|
|
293
287
|
// Fire onAuthenticated hook (fire-and-forget)
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
288
|
+
if (ops.onAuthenticated) {
|
|
289
|
+
ops.onAuthenticated(user, "register").catch(err => {
|
|
290
|
+
logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
|
|
297
291
|
});
|
|
298
292
|
}
|
|
299
293
|
|
|
@@ -308,15 +302,15 @@ displayName: user.displayName });
|
|
|
308
302
|
const { email, password } = parseBody(loginSchema, await c.req.json());
|
|
309
303
|
|
|
310
304
|
// Call beforeLogin hook if provided (throw to reject)
|
|
311
|
-
if (
|
|
312
|
-
await
|
|
305
|
+
if (ops.beforeLogin) {
|
|
306
|
+
await ops.beforeLogin(email, "login");
|
|
313
307
|
}
|
|
314
308
|
|
|
315
309
|
let user;
|
|
316
310
|
|
|
317
|
-
if (
|
|
311
|
+
if (ops.verifyCredentials) {
|
|
318
312
|
// Full credential verification override
|
|
319
|
-
user = await
|
|
313
|
+
user = await ops.verifyCredentials(email, password, authRepo);
|
|
320
314
|
if (!user) {
|
|
321
315
|
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
322
316
|
}
|
|
@@ -344,9 +338,9 @@ displayName: user.displayName });
|
|
|
344
338
|
);
|
|
345
339
|
|
|
346
340
|
// Fire onAuthenticated hook (fire-and-forget)
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
341
|
+
if (ops.onAuthenticated) {
|
|
342
|
+
ops.onAuthenticated(user, "login").catch(err => {
|
|
343
|
+
logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
|
|
350
344
|
});
|
|
351
345
|
}
|
|
352
346
|
|
|
@@ -399,11 +393,11 @@ displayName: user.displayName });
|
|
|
399
393
|
await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
|
|
400
394
|
|
|
401
395
|
// Fire afterUserCreate hook
|
|
402
|
-
if (
|
|
396
|
+
if (ops.afterUserCreate) {
|
|
403
397
|
try {
|
|
404
|
-
await
|
|
398
|
+
await ops.afterUserCreate(user);
|
|
405
399
|
} catch (err) {
|
|
406
|
-
|
|
400
|
+
logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
|
|
407
401
|
}
|
|
408
402
|
}
|
|
409
403
|
|
|
@@ -487,7 +481,7 @@ displayName: user.displayName }, appName);
|
|
|
487
481
|
text: emailContent.text
|
|
488
482
|
});
|
|
489
483
|
} catch (emailError: unknown) {
|
|
490
|
-
|
|
484
|
+
logger.error("Failed to send password reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
491
485
|
// Don't reveal email sending failure to client
|
|
492
486
|
}
|
|
493
487
|
}
|
|
@@ -531,9 +525,9 @@ displayName: user.displayName }, appName);
|
|
|
531
525
|
await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
|
|
532
526
|
|
|
533
527
|
// Fire onPasswordReset hook (fire-and-forget)
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
528
|
+
if (ops.onPasswordReset) {
|
|
529
|
+
ops.onPasswordReset(storedToken.userId).catch(err => {
|
|
530
|
+
logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
|
|
537
531
|
});
|
|
538
532
|
}
|
|
539
533
|
|
|
@@ -686,11 +680,13 @@ message: "Email verified successfully" });
|
|
|
686
680
|
|
|
687
681
|
// Allow customization of access token claims via hook
|
|
688
682
|
let customClaims: Record<string, unknown> | undefined;
|
|
689
|
-
if (
|
|
683
|
+
if (ops.customizeAccessToken) {
|
|
690
684
|
const user = await authRepo.getUserById(storedToken.userId);
|
|
691
685
|
if (user) {
|
|
692
|
-
const defaultClaims: Record<string, unknown> = { userId: storedToken.userId,
|
|
693
|
-
|
|
686
|
+
const defaultClaims: Record<string, unknown> = { userId: storedToken.userId,
|
|
687
|
+
roles: roleIds,
|
|
688
|
+
aal: "aal1" };
|
|
689
|
+
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
694
690
|
}
|
|
695
691
|
}
|
|
696
692
|
|
|
@@ -719,559 +715,19 @@ message: "Email verified successfully" });
|
|
|
719
715
|
});
|
|
720
716
|
});
|
|
721
717
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (refreshToken) {
|
|
730
|
-
const tokenHash = hashRefreshToken(refreshToken);
|
|
731
|
-
await authRepo.deleteRefreshToken(tokenHash);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Call afterLogout hook (fire-and-forget)
|
|
735
|
-
// Extract userId from the access token if present
|
|
736
|
-
const authHeader = c.req.header("authorization");
|
|
737
|
-
if (authHooks?.afterLogout && authHeader?.startsWith("Bearer ")) {
|
|
738
|
-
const { verifyAccessToken } = await import("./jwt");
|
|
739
|
-
const payload = verifyAccessToken(authHeader.substring(7));
|
|
740
|
-
if (payload) {
|
|
741
|
-
authHooks.afterLogout(payload.userId).catch(err => {
|
|
742
|
-
console.error("[AuthHooks] afterLogout error:", err instanceof Error ? err.message : err);
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
return c.json({ success: true });
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* GET /auth/sessions
|
|
752
|
-
* Get active refresh tokens (sessions) for the current user
|
|
753
|
-
*/
|
|
754
|
-
router.get("/sessions", requireAuth, async (c) => {
|
|
755
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
756
|
-
if (!userCtx) {
|
|
757
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const currentRefreshToken = c.req.header("x-refresh-token") as string;
|
|
761
|
-
const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
|
|
762
|
-
|
|
763
|
-
const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
|
|
764
|
-
|
|
765
|
-
const mappedSessions = sessions.map(s => ({
|
|
766
|
-
id: s.id,
|
|
767
|
-
userAgent: s.userAgent,
|
|
768
|
-
ipAddress: s.ipAddress,
|
|
769
|
-
createdAt: s.createdAt,
|
|
770
|
-
isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
|
|
771
|
-
}));
|
|
772
|
-
|
|
773
|
-
return c.json({ sessions: mappedSessions });
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* DELETE /auth/sessions
|
|
778
|
-
* Delete all refresh tokens for the current user (remote logout every device)
|
|
779
|
-
*/
|
|
780
|
-
router.delete("/sessions", requireAuth, async (c) => {
|
|
781
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
782
|
-
if (!userCtx) {
|
|
783
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
|
|
787
|
-
return c.json({ success: true,
|
|
788
|
-
message: "All sessions revoked successfully" });
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* DELETE /auth/sessions/:id
|
|
793
|
-
* Delete a specific refresh token (remote logout)
|
|
794
|
-
*/
|
|
795
|
-
router.delete("/sessions/:id", requireAuth, async (c) => {
|
|
796
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
797
|
-
if (!userCtx) {
|
|
798
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const id = c.req.param("id");
|
|
802
|
-
if (!id) {
|
|
803
|
-
throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
await authRepo.deleteRefreshTokenById(id, userCtx.userId);
|
|
807
|
-
return c.json({ success: true,
|
|
808
|
-
message: "Session revoked successfully" });
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* GET /auth/me
|
|
813
|
-
* Get current authenticated user
|
|
814
|
-
*/
|
|
815
|
-
router.get("/me", requireAuth, async (c) => {
|
|
816
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
817
|
-
if (!userCtx) {
|
|
818
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
822
|
-
if (!result) {
|
|
823
|
-
throw ApiError.notFound("User not found");
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return c.json({
|
|
827
|
-
user: {
|
|
828
|
-
uid: result.user.id,
|
|
829
|
-
email: result.user.email,
|
|
830
|
-
displayName: result.user.displayName,
|
|
831
|
-
photoURL: result.user.photoUrl,
|
|
832
|
-
emailVerified: result.user.emailVerified,
|
|
833
|
-
roles: result.roles.map(r => r.id),
|
|
834
|
-
metadata: result.user.metadata ?? {}
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* PATCH /auth/me
|
|
841
|
-
* Update current authenticated user profile
|
|
842
|
-
*/
|
|
843
|
-
router.patch("/me", requireAuth, async (c) => {
|
|
844
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
845
|
-
if (!userCtx) {
|
|
846
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
|
|
850
|
-
|
|
851
|
-
const updatedUser = await authRepo.updateUser(userCtx.userId, {
|
|
852
|
-
displayName: displayName !== undefined ? displayName : undefined,
|
|
853
|
-
photoUrl: photoURL !== undefined ? photoURL : undefined
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
if (!updatedUser) {
|
|
857
|
-
throw ApiError.notFound("User not found");
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
861
|
-
if (!result) {
|
|
862
|
-
throw ApiError.notFound("User not found");
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return c.json({
|
|
866
|
-
user: {
|
|
867
|
-
uid: result.user.id,
|
|
868
|
-
email: result.user.email,
|
|
869
|
-
displayName: result.user.displayName,
|
|
870
|
-
photoURL: result.user.photoUrl,
|
|
871
|
-
emailVerified: result.user.emailVerified,
|
|
872
|
-
roles: result.roles.map(r => r.id),
|
|
873
|
-
metadata: result.user.metadata ?? {}
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* GET /auth/config
|
|
880
|
-
* Get public auth configuration (for frontend to know what's available)
|
|
881
|
-
*/
|
|
882
|
-
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
883
|
-
// Determine if setup is needed using the persistent bootstrap flag
|
|
884
|
-
// when available, falling back to user-count check for backward compat.
|
|
885
|
-
let needsSetup: boolean;
|
|
886
|
-
if (config.isBootstrapCompleted) {
|
|
887
|
-
needsSetup = !(await config.isBootstrapCompleted());
|
|
888
|
-
} else {
|
|
889
|
-
const allUsers = await authRepo.listUsers();
|
|
890
|
-
needsSetup = allUsers.length === 0;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Registration is allowed when explicitly enabled OR during initial setup
|
|
894
|
-
const registrationAllowed = needsSetup || !!allowRegistration;
|
|
895
|
-
|
|
896
|
-
// Build the list of enabled OAuth providers for frontend discovery.
|
|
897
|
-
const enabledProviders = (config.oauthProviders || []).map(p => p.id);
|
|
898
|
-
|
|
899
|
-
return c.json({
|
|
900
|
-
needsSetup,
|
|
901
|
-
registrationEnabled: registrationAllowed,
|
|
902
|
-
emailServiceEnabled: isEmailConfigured(),
|
|
903
|
-
enabledProviders
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
908
|
-
// ANONYMOUS SIGN-IN
|
|
909
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
910
|
-
|
|
911
|
-
/**
|
|
912
|
-
* POST /auth/anonymous
|
|
913
|
-
* Create an anonymous user with temporary credentials
|
|
914
|
-
*/
|
|
915
|
-
router.post("/anonymous", strictAuthLimiter, async (c) => {
|
|
916
|
-
const anonId = randomBytes(16).toString("hex");
|
|
917
|
-
const anonEmail = `anon_${anonId.slice(0, 8)}@anonymous.local`;
|
|
918
|
-
|
|
919
|
-
let createData: CreateUserData = {
|
|
920
|
-
email: anonEmail,
|
|
921
|
-
emailVerified: false,
|
|
922
|
-
isAnonymous: true
|
|
923
|
-
};
|
|
924
|
-
|
|
925
|
-
if (authHooks?.beforeUserCreate) {
|
|
926
|
-
createData = await authHooks.beforeUserCreate(createData);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const user = await authRepo.createUser(createData);
|
|
930
|
-
|
|
931
|
-
// Assign default role (follow register route pattern, but never auto-admin)
|
|
932
|
-
if (config.defaultRole) {
|
|
933
|
-
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
937
|
-
user.id,
|
|
938
|
-
c.req.header("user-agent") || "unknown",
|
|
939
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
940
|
-
);
|
|
941
|
-
|
|
942
|
-
// Fire afterUserCreate hook
|
|
943
|
-
if (authHooks?.afterUserCreate) {
|
|
944
|
-
authHooks.afterUserCreate(user).catch(err => {
|
|
945
|
-
console.error("[AuthHooks] afterUserCreate error:", err instanceof Error ? err.message : err);
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Fire onAuthenticated hook
|
|
950
|
-
if (authHooks?.onAuthenticated) {
|
|
951
|
-
authHooks.onAuthenticated(user, "anonymous").catch(err => {
|
|
952
|
-
console.error("[AuthHooks] onAuthenticated error:", err instanceof Error ? err.message : err);
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* POST /auth/anonymous/link
|
|
961
|
-
* Upgrade an anonymous user to a permanent account with email/password
|
|
962
|
-
*/
|
|
963
|
-
router.post("/anonymous/link", requireAuth, async (c) => {
|
|
964
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
965
|
-
if (!userCtx) {
|
|
966
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
970
|
-
if (!user?.isAnonymous) {
|
|
971
|
-
throw ApiError.badRequest("User is not anonymous", "NOT_ANONYMOUS");
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
const linkSchema = z.object({
|
|
975
|
-
email: z.string().email("Invalid email address").max(255),
|
|
976
|
-
password: z.string().min(1, "Password is required").max(128)
|
|
977
|
-
});
|
|
978
|
-
const { email, password } = parseBody(linkSchema, await c.req.json());
|
|
979
|
-
|
|
980
|
-
// Validate password strength
|
|
981
|
-
const passwordValidation = ops.validatePasswordStrength(password);
|
|
982
|
-
if (!passwordValidation.valid) {
|
|
983
|
-
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Check if email is already taken
|
|
987
|
-
const existingUser = await authRepo.getUserByEmail(email.toLowerCase());
|
|
988
|
-
if (existingUser) {
|
|
989
|
-
throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// Hash password
|
|
993
|
-
const passwordHash = await ops.hashPassword(password);
|
|
994
|
-
|
|
995
|
-
// Update user: set email, password, remove anonymous flag
|
|
996
|
-
const updatedUser = await authRepo.updateUser(user.id, {
|
|
997
|
-
email: email.toLowerCase(),
|
|
998
|
-
passwordHash,
|
|
999
|
-
isAnonymous: false
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
if (!updatedUser) {
|
|
1003
|
-
throw ApiError.notFound("User not found");
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// Generate new tokens with updated identity
|
|
1007
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
1008
|
-
user.id,
|
|
1009
|
-
c.req.header("user-agent") || "unknown",
|
|
1010
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
1011
|
-
);
|
|
1012
|
-
|
|
1013
|
-
return c.json(buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken));
|
|
718
|
+
mountSessionRoutes({
|
|
719
|
+
router,
|
|
720
|
+
config,
|
|
721
|
+
ops,
|
|
722
|
+
parseBody,
|
|
723
|
+
buildAuthResponse,
|
|
724
|
+
createSessionAndTokens
|
|
1014
725
|
});
|
|
1015
726
|
|
|
1016
727
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1017
728
|
// MFA / TOTP
|
|
1018
729
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* POST /auth/mfa/enroll
|
|
1022
|
-
* Start MFA enrollment: generate TOTP secret and recovery codes
|
|
1023
|
-
*/
|
|
1024
|
-
router.post("/mfa/enroll", requireAuth, async (c) => {
|
|
1025
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1026
|
-
if (!userCtx) {
|
|
1027
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
|
|
1031
|
-
const friendlyName = typeof body.friendlyName === "string" ? body.friendlyName : undefined;
|
|
1032
|
-
const issuer = typeof body.issuer === "string" ? body.issuer : (emailConfig?.appName || "Rebase");
|
|
1033
|
-
|
|
1034
|
-
// Get user for account name
|
|
1035
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
1036
|
-
if (!user) {
|
|
1037
|
-
throw ApiError.notFound("User not found");
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// Generate TOTP secret
|
|
1041
|
-
const { secret, uri } = generateTotpSecret(issuer, user.email);
|
|
1042
|
-
|
|
1043
|
-
// Store the factor (unverified until user confirms with a valid code)
|
|
1044
|
-
const factor = await authRepo.createMfaFactor(
|
|
1045
|
-
user.id,
|
|
1046
|
-
"totp",
|
|
1047
|
-
secret, // In production, encrypt this before storage
|
|
1048
|
-
friendlyName
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
// Generate recovery codes
|
|
1052
|
-
const codes = generateRecoveryCodes(10);
|
|
1053
|
-
const codeHashes = codes.map(hashRecoveryCode);
|
|
1054
|
-
await authRepo.createRecoveryCodes(user.id, codeHashes);
|
|
1055
|
-
|
|
1056
|
-
return c.json({
|
|
1057
|
-
factor: {
|
|
1058
|
-
id: factor.id,
|
|
1059
|
-
factorType: factor.factorType,
|
|
1060
|
-
friendlyName: factor.friendlyName
|
|
1061
|
-
},
|
|
1062
|
-
totp: {
|
|
1063
|
-
secret,
|
|
1064
|
-
uri,
|
|
1065
|
-
qrUri: uri // Client can use a QR library to render this
|
|
1066
|
-
},
|
|
1067
|
-
recoveryCodes: codes
|
|
1068
|
-
}, 201);
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* POST /auth/mfa/verify
|
|
1073
|
-
* Verify TOTP code to complete MFA enrollment
|
|
1074
|
-
*/
|
|
1075
|
-
router.post("/mfa/verify", requireAuth, async (c) => {
|
|
1076
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1077
|
-
if (!userCtx) {
|
|
1078
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const verifySchema = z.object({
|
|
1082
|
-
factorId: z.string().min(1, "Factor ID is required"),
|
|
1083
|
-
code: z.string().length(6, "Code must be 6 digits")
|
|
1084
|
-
});
|
|
1085
|
-
const { factorId, code } = parseBody(verifySchema, await c.req.json());
|
|
1086
|
-
|
|
1087
|
-
// Get the factor
|
|
1088
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
1089
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
1090
|
-
throw ApiError.notFound("MFA factor not found");
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
if (factor.verified) {
|
|
1094
|
-
throw ApiError.badRequest("Factor is already verified", "ALREADY_VERIFIED");
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// Verify the TOTP code
|
|
1098
|
-
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
1099
|
-
const isValid = verifyTotp(secretBuffer, code);
|
|
1100
|
-
|
|
1101
|
-
if (!isValid) {
|
|
1102
|
-
throw ApiError.unauthorized("Invalid TOTP code", "INVALID_CODE");
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Mark factor as verified
|
|
1106
|
-
await authRepo.verifyMfaFactor(factorId);
|
|
1107
|
-
|
|
1108
|
-
return c.json({ success: true, message: "MFA factor verified and enrolled" });
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
/**
|
|
1112
|
-
* POST /auth/mfa/challenge
|
|
1113
|
-
* Create an MFA challenge during login (user has MFA enrolled)
|
|
1114
|
-
*/
|
|
1115
|
-
router.post("/mfa/challenge", requireAuth, async (c) => {
|
|
1116
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1117
|
-
if (!userCtx) {
|
|
1118
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const challengeSchema = z.object({
|
|
1122
|
-
factorId: z.string().min(1, "Factor ID is required")
|
|
1123
|
-
});
|
|
1124
|
-
const { factorId } = parseBody(challengeSchema, await c.req.json());
|
|
1125
|
-
|
|
1126
|
-
// Verify the factor belongs to this user and is verified
|
|
1127
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
1128
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
1129
|
-
throw ApiError.notFound("MFA factor not found");
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
if (!factor.verified) {
|
|
1133
|
-
throw ApiError.badRequest("MFA factor is not yet verified", "FACTOR_NOT_VERIFIED");
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
1137
|
-
const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);
|
|
1138
|
-
|
|
1139
|
-
return c.json({
|
|
1140
|
-
challengeId: challenge.id,
|
|
1141
|
-
factorId: challenge.factorId,
|
|
1142
|
-
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
|
|
1143
|
-
});
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
/**
|
|
1147
|
-
* POST /auth/mfa/challenge/verify
|
|
1148
|
-
* Verify a TOTP code for an active challenge, upgrade aal1 → aal2
|
|
1149
|
-
*/
|
|
1150
|
-
router.post("/mfa/challenge/verify", requireAuth, async (c) => {
|
|
1151
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1152
|
-
if (!userCtx) {
|
|
1153
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
const challengeVerifySchema = z.object({
|
|
1157
|
-
challengeId: z.string().min(1, "Challenge ID is required"),
|
|
1158
|
-
code: z.string().min(1, "Code is required")
|
|
1159
|
-
});
|
|
1160
|
-
const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());
|
|
1161
|
-
|
|
1162
|
-
// Find the challenge
|
|
1163
|
-
const challenge = await authRepo.getMfaChallengeById(challengeId);
|
|
1164
|
-
if (!challenge) {
|
|
1165
|
-
throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Get the factor and verify ownership
|
|
1169
|
-
const factor = await authRepo.getMfaFactorById(challenge.factorId);
|
|
1170
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
1171
|
-
throw ApiError.notFound("MFA factor not found");
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Try TOTP verification first (standard 6-digit codes)
|
|
1175
|
-
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
1176
|
-
let isValid = verifyTotp(secretBuffer, code);
|
|
1177
|
-
|
|
1178
|
-
// Fall back to recovery code verification if TOTP didn't match
|
|
1179
|
-
if (!isValid) {
|
|
1180
|
-
const codeHash = hashRecoveryCode(code);
|
|
1181
|
-
isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
if (!isValid) {
|
|
1185
|
-
throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Mark challenge as verified
|
|
1189
|
-
await authRepo.verifyMfaChallenge(challengeId);
|
|
1190
|
-
|
|
1191
|
-
// Generate new access token with aal2
|
|
1192
|
-
const roles = await authRepo.getUserRoles(userCtx.userId);
|
|
1193
|
-
const roleIds = roles.map(r => r.id);
|
|
1194
|
-
const accessToken = generateAccessToken(userCtx.userId, roleIds, "aal2");
|
|
1195
|
-
const refreshToken = generateRefreshToken();
|
|
1196
|
-
|
|
1197
|
-
// Create new refresh token
|
|
1198
|
-
await authRepo.createRefreshToken(
|
|
1199
|
-
userCtx.userId,
|
|
1200
|
-
hashRefreshToken(refreshToken),
|
|
1201
|
-
getRefreshTokenExpiry(),
|
|
1202
|
-
c.req.header("user-agent") || "unknown",
|
|
1203
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
// Fire onMfaVerified hook
|
|
1207
|
-
if (authHooks?.onMfaVerified) {
|
|
1208
|
-
authHooks.onMfaVerified(userCtx.userId, factor.id).catch(err => {
|
|
1209
|
-
console.error("[AuthHooks] onMfaVerified error:", err instanceof Error ? err.message : err);
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
return c.json({
|
|
1214
|
-
tokens: {
|
|
1215
|
-
accessToken,
|
|
1216
|
-
refreshToken,
|
|
1217
|
-
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* GET /auth/mfa/factors
|
|
1224
|
-
* List enrolled MFA factors for the current user
|
|
1225
|
-
*/
|
|
1226
|
-
router.get("/mfa/factors", requireAuth, async (c) => {
|
|
1227
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1228
|
-
if (!userCtx) {
|
|
1229
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
const factors = await authRepo.getMfaFactors(userCtx.userId);
|
|
1233
|
-
return c.json({
|
|
1234
|
-
factors: factors.map(f => ({
|
|
1235
|
-
id: f.id,
|
|
1236
|
-
factorType: f.factorType,
|
|
1237
|
-
friendlyName: f.friendlyName,
|
|
1238
|
-
verified: f.verified,
|
|
1239
|
-
createdAt: f.createdAt
|
|
1240
|
-
}))
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* DELETE /auth/mfa/unenroll
|
|
1246
|
-
* Remove an MFA factor
|
|
1247
|
-
*/
|
|
1248
|
-
router.delete("/mfa/unenroll", requireAuth, async (c) => {
|
|
1249
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
1250
|
-
if (!userCtx) {
|
|
1251
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
const unenrollSchema = z.object({
|
|
1255
|
-
factorId: z.string().min(1, "Factor ID is required")
|
|
1256
|
-
});
|
|
1257
|
-
const { factorId } = parseBody(unenrollSchema, await c.req.json());
|
|
1258
|
-
|
|
1259
|
-
// Verify ownership
|
|
1260
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
1261
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
1262
|
-
throw ApiError.notFound("MFA factor not found");
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
await authRepo.deleteMfaFactor(factorId, userCtx.userId);
|
|
1266
|
-
|
|
1267
|
-
// If no more verified factors, clean up recovery codes
|
|
1268
|
-
const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.userId);
|
|
1269
|
-
if (!hasFactors) {
|
|
1270
|
-
await authRepo.deleteAllRecoveryCodes(userCtx.userId);
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
return c.json({ success: true, message: "MFA factor removed" });
|
|
1274
|
-
});
|
|
730
|
+
mountMfaRoutes(router, config, ops, parseBody);
|
|
1275
731
|
|
|
1276
732
|
return router;
|
|
1277
733
|
}
|