@rebasepro/server-core 0.4.0 → 0.6.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 +62 -25
- package/dist/{server-core/src/api → api}/errors.d.ts +20 -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/{server-core/src/auth → auth}/jwt.d.ts +10 -0
- 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 +60871 -50424
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +73151 -51256
- 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 +95 -20
- package/src/api/graphql/graphql-schema-generator.ts +15 -10
- package/src/api/logs-routes.ts +5 -2
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator-count.test.ts +21 -10
- package/src/api/rest/api-generator.ts +151 -36
- package/src/api/rest/query-parser.ts +18 -12
- package/src/api/server.ts +13 -2
- 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 +64 -54
- 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 +15 -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 -600
- 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 +93 -295
- 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 +30 -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 +5 -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 -6
- 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 -536
- 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 -941
- 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 -1181
- 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/test.ts +0 -6
- /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}/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/index.ts
CHANGED
|
@@ -7,9 +7,12 @@ export type { JwtConfig, AccessTokenPayload } from "./jwt";
|
|
|
7
7
|
export { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
|
|
8
8
|
export type { PasswordValidationResult } from "./password";
|
|
9
9
|
|
|
10
|
-
export type { AuthHooks, AuthMethod,
|
|
10
|
+
export type { AuthHooks, AuthMethod, ResolvedAuthHooks } from "./auth-hooks";
|
|
11
11
|
export { resolveAuthHooks } from "./auth-hooks";
|
|
12
12
|
|
|
13
|
+
export { generateSecurePassword, generateSecureToken, hashToken, prepareAdminUserValues, finalizeAdminUserCreation } from "./admin-user-ops";
|
|
14
|
+
export type { AdminUserContext, AdminUserPrepareResult } from "./admin-user-ops";
|
|
15
|
+
|
|
13
16
|
// OAuth Providers
|
|
14
17
|
export { createGoogleProvider } from "./google-oauth";
|
|
15
18
|
export type { GoogleProviderConfig } from "./google-oauth";
|
|
@@ -32,7 +35,8 @@ export type { AuthMiddlewareOptions, AuthResult } from "./middleware";
|
|
|
32
35
|
export { createAuthRoutes } from "./routes";
|
|
33
36
|
export type { AuthModuleConfig } from "./routes";
|
|
34
37
|
|
|
35
|
-
export {
|
|
38
|
+
export { createResetPasswordRoute } from "./reset-password-admin";
|
|
39
|
+
export type { ResetPasswordRouteConfig } from "./reset-password-admin";
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createApiKeyRateLimiter, apiKeyKeyGenerator } from "./rate-limiter";
|
package/src/auth/jwt.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
2
|
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
3
4
|
|
|
4
5
|
export interface JwtConfig {
|
|
5
6
|
secret: string;
|
|
@@ -13,6 +14,16 @@ export interface AccessTokenPayload {
|
|
|
13
14
|
uid?: string;
|
|
14
15
|
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
15
16
|
aal?: "aal1" | "aal2";
|
|
17
|
+
/** Email claim from the JWT, if present */
|
|
18
|
+
email?: string;
|
|
19
|
+
/** Display name claim from the JWT, if present */
|
|
20
|
+
displayName?: string;
|
|
21
|
+
/** Photo URL claim from the JWT, if present */
|
|
22
|
+
photoURL?: string;
|
|
23
|
+
/** Whether MFA has been verified for this session */
|
|
24
|
+
mfa_verified?: boolean;
|
|
25
|
+
/** Authentication Methods Reference — list of methods used (e.g. 'pwd', 'otp') */
|
|
26
|
+
amr?: string[];
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
let jwtConfig: JwtConfig = {
|
|
@@ -52,14 +63,14 @@ export function configureJwt(config: JwtConfig): void {
|
|
|
52
63
|
if (!config.secret || config.secret.length < 32) {
|
|
53
64
|
throw new Error(
|
|
54
65
|
"JWT secret is too short. Must be at least 32 characters. " +
|
|
55
|
-
"Generate one with: node -e \"
|
|
66
|
+
"Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\""
|
|
56
67
|
);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
if (weakSecrets.has(config.secret.toLowerCase())) {
|
|
60
71
|
throw new Error(
|
|
61
72
|
"JWT secret is a known default/weak value. Please use a strong, randomly generated secret. " +
|
|
62
|
-
"Generate one with: node -e \"
|
|
73
|
+
"Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\""
|
|
63
74
|
);
|
|
64
75
|
}
|
|
65
76
|
|
|
@@ -138,7 +149,7 @@ export function verifyAccessToken(token: string): AccessTokenPayload | null {
|
|
|
138
149
|
const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[]; aal?: string };
|
|
139
150
|
const id = decoded.userId || decoded.uid || decoded.sub;
|
|
140
151
|
if (!id) {
|
|
141
|
-
|
|
152
|
+
logger.error("[JWT] Verification failed: missing id in payload", { detail: decoded });
|
|
142
153
|
return null;
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -150,7 +161,7 @@ export function verifyAccessToken(token: string): AccessTokenPayload | null {
|
|
|
150
161
|
aal
|
|
151
162
|
};
|
|
152
163
|
} catch (error) {
|
|
153
|
-
|
|
164
|
+
logger.error("[JWT] Verification failed", { error: error, detail: token.substring(0, 15) });
|
|
154
165
|
return null;
|
|
155
166
|
}
|
|
156
167
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
3
4
|
|
|
4
5
|
export interface LinkedinUserInfo {
|
|
5
6
|
linkedinId: string;
|
|
@@ -38,7 +39,7 @@ export function createLinkedinProvider(config: { clientId: string, clientSecret:
|
|
|
38
39
|
|
|
39
40
|
if (!tokenResponse.ok) {
|
|
40
41
|
const errBody = await tokenResponse.text();
|
|
41
|
-
|
|
42
|
+
logger.error("Failed to get LinkedIn access token", { detail: errBody });
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -54,7 +55,7 @@ export function createLinkedinProvider(config: { clientId: string, clientSecret:
|
|
|
54
55
|
|
|
55
56
|
if (!profileResponse.ok) {
|
|
56
57
|
const errBody = await profileResponse.text();
|
|
57
|
-
|
|
58
|
+
logger.error("Failed to get LinkedIn user info", { detail: errBody });
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -73,7 +74,7 @@ export function createLinkedinProvider(config: { clientId: string, clientSecret:
|
|
|
73
74
|
photoUrl: profileData.picture || null
|
|
74
75
|
};
|
|
75
76
|
} catch (error) {
|
|
76
|
-
|
|
77
|
+
logger.error("LinkedIn OAuth error", { error: error });
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ApiError } from "../api/errors";
|
|
4
|
+
import { HonoEnv } from "../api/types";
|
|
5
|
+
import { requireAuth } from "./middleware";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import {
|
|
8
|
+
generateTotpSecret,
|
|
9
|
+
verifyTotp,
|
|
10
|
+
base32Decode,
|
|
11
|
+
generateRecoveryCodes,
|
|
12
|
+
hashRecoveryCode
|
|
13
|
+
} from "./mfa";
|
|
14
|
+
import {
|
|
15
|
+
generateAccessToken,
|
|
16
|
+
generateRefreshToken,
|
|
17
|
+
hashRefreshToken,
|
|
18
|
+
getRefreshTokenExpiry,
|
|
19
|
+
getAccessTokenExpiry
|
|
20
|
+
} from "./jwt";
|
|
21
|
+
import type { AuthModuleConfig } from "./routes";
|
|
22
|
+
import { resolveAuthHooks } from "./auth-hooks";
|
|
23
|
+
|
|
24
|
+
export function mountMfaRoutes(
|
|
25
|
+
router: Hono<HonoEnv>,
|
|
26
|
+
config: AuthModuleConfig,
|
|
27
|
+
ops: ReturnType<typeof resolveAuthHooks>,
|
|
28
|
+
parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T
|
|
29
|
+
): void {
|
|
30
|
+
const authRepo = config.authRepo;
|
|
31
|
+
const emailConfig = config.emailConfig;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* POST /auth/mfa/enroll
|
|
35
|
+
* Start MFA enrollment: generate TOTP secret and recovery codes
|
|
36
|
+
*/
|
|
37
|
+
router.post("/mfa/enroll", requireAuth, async (c) => {
|
|
38
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
39
|
+
if (!userCtx) {
|
|
40
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
|
|
44
|
+
const friendlyName = typeof body.friendlyName === "string" ? body.friendlyName : undefined;
|
|
45
|
+
const issuer = typeof body.issuer === "string" ? body.issuer : emailConfig?.appName || "Rebase";
|
|
46
|
+
|
|
47
|
+
// Get user for account name
|
|
48
|
+
const user = await authRepo.getUserById(userCtx.userId);
|
|
49
|
+
if (!user) {
|
|
50
|
+
throw ApiError.notFound("User not found");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Generate TOTP secret
|
|
54
|
+
const { secret, uri } = generateTotpSecret(issuer, user.email);
|
|
55
|
+
|
|
56
|
+
// Store the factor (unverified until user confirms with a valid code)
|
|
57
|
+
const factor = await authRepo.createMfaFactor(
|
|
58
|
+
user.id,
|
|
59
|
+
"totp",
|
|
60
|
+
secret, // In production, encrypt this before storage
|
|
61
|
+
friendlyName
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Generate recovery codes
|
|
65
|
+
const codes = generateRecoveryCodes(10);
|
|
66
|
+
const codeHashes = codes.map(hashRecoveryCode);
|
|
67
|
+
await authRepo.createRecoveryCodes(user.id, codeHashes);
|
|
68
|
+
|
|
69
|
+
return c.json(
|
|
70
|
+
{
|
|
71
|
+
factor: {
|
|
72
|
+
id: factor.id,
|
|
73
|
+
factorType: factor.factorType,
|
|
74
|
+
friendlyName: factor.friendlyName
|
|
75
|
+
},
|
|
76
|
+
totp: {
|
|
77
|
+
secret,
|
|
78
|
+
uri,
|
|
79
|
+
qrUri: uri
|
|
80
|
+
},
|
|
81
|
+
recoveryCodes: codes
|
|
82
|
+
},
|
|
83
|
+
201
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* POST /auth/mfa/verify
|
|
89
|
+
* Verify TOTP code to complete MFA enrollment
|
|
90
|
+
*/
|
|
91
|
+
router.post("/mfa/verify", requireAuth, async (c) => {
|
|
92
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
93
|
+
if (!userCtx) {
|
|
94
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const verifySchema = z.object({
|
|
98
|
+
factorId: z.string().min(1, "Factor ID is required"),
|
|
99
|
+
code: z.string().length(6, "Code must be 6 digits")
|
|
100
|
+
});
|
|
101
|
+
const { factorId, code } = parseBody(verifySchema, await c.req.json());
|
|
102
|
+
|
|
103
|
+
// Get the factor
|
|
104
|
+
const factor = await authRepo.getMfaFactorById(factorId);
|
|
105
|
+
if (!factor || factor.userId !== userCtx.userId) {
|
|
106
|
+
throw ApiError.notFound("MFA factor not found");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (factor.verified) {
|
|
110
|
+
throw ApiError.badRequest("Factor is already verified", "ALREADY_VERIFIED");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Verify the TOTP code
|
|
114
|
+
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
115
|
+
const isValid = verifyTotp(secretBuffer, code);
|
|
116
|
+
|
|
117
|
+
if (!isValid) {
|
|
118
|
+
throw ApiError.unauthorized("Invalid TOTP code", "INVALID_CODE");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Mark factor as verified
|
|
122
|
+
await authRepo.verifyMfaFactor(factorId);
|
|
123
|
+
|
|
124
|
+
return c.json({
|
|
125
|
+
success: true,
|
|
126
|
+
message: "MFA factor verified and enrolled"
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* POST /auth/mfa/challenge
|
|
132
|
+
* Create an MFA challenge during login (user has MFA enrolled)
|
|
133
|
+
*/
|
|
134
|
+
router.post("/mfa/challenge", requireAuth, async (c) => {
|
|
135
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
136
|
+
if (!userCtx) {
|
|
137
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const challengeSchema = z.object({
|
|
141
|
+
factorId: z.string().min(1, "Factor ID is required")
|
|
142
|
+
});
|
|
143
|
+
const { factorId } = parseBody(challengeSchema, await c.req.json());
|
|
144
|
+
|
|
145
|
+
// Verify the factor belongs to this user and is verified
|
|
146
|
+
const factor = await authRepo.getMfaFactorById(factorId);
|
|
147
|
+
if (!factor || factor.userId !== userCtx.userId) {
|
|
148
|
+
throw ApiError.notFound("MFA factor not found");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!factor.verified) {
|
|
152
|
+
throw ApiError.badRequest("MFA factor is not yet verified", "FACTOR_NOT_VERIFIED");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
156
|
+
const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);
|
|
157
|
+
|
|
158
|
+
return c.json({
|
|
159
|
+
challengeId: challenge.id,
|
|
160
|
+
factorId: challenge.factorId,
|
|
161
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* POST /auth/mfa/challenge/verify
|
|
167
|
+
* Verify a TOTP code for an active challenge, upgrade aal1 → aal2
|
|
168
|
+
*/
|
|
169
|
+
router.post("/mfa/challenge/verify", requireAuth, async (c) => {
|
|
170
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
171
|
+
if (!userCtx) {
|
|
172
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const challengeVerifySchema = z.object({
|
|
176
|
+
challengeId: z.string().min(1, "Challenge ID is required"),
|
|
177
|
+
code: z.string().min(1, "Code is required")
|
|
178
|
+
});
|
|
179
|
+
const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());
|
|
180
|
+
|
|
181
|
+
// Find the challenge
|
|
182
|
+
const challenge = await authRepo.getMfaChallengeById(challengeId);
|
|
183
|
+
if (!challenge) {
|
|
184
|
+
throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get the factor and verify ownership
|
|
188
|
+
const factor = await authRepo.getMfaFactorById(challenge.factorId);
|
|
189
|
+
if (!factor || factor.userId !== userCtx.userId) {
|
|
190
|
+
throw ApiError.notFound("MFA factor not found");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Try TOTP verification first (standard 6-digit codes)
|
|
194
|
+
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
195
|
+
let isValid = verifyTotp(secretBuffer, code);
|
|
196
|
+
|
|
197
|
+
// Fall back to recovery code verification if TOTP didn't match
|
|
198
|
+
if (!isValid) {
|
|
199
|
+
const codeHash = hashRecoveryCode(code);
|
|
200
|
+
isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!isValid) {
|
|
204
|
+
throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Mark challenge as verified
|
|
208
|
+
await authRepo.verifyMfaChallenge(challengeId);
|
|
209
|
+
|
|
210
|
+
// Generate new access token with aal2
|
|
211
|
+
const roles = await authRepo.getUserRoles(userCtx.userId);
|
|
212
|
+
const roleIds = roles.map((r) => r.id);
|
|
213
|
+
const accessToken = generateAccessToken(userCtx.userId, roleIds, "aal2");
|
|
214
|
+
const refreshToken = generateRefreshToken();
|
|
215
|
+
|
|
216
|
+
// Create new refresh token
|
|
217
|
+
await authRepo.createRefreshToken(
|
|
218
|
+
userCtx.userId,
|
|
219
|
+
hashRefreshToken(refreshToken),
|
|
220
|
+
getRefreshTokenExpiry(),
|
|
221
|
+
c.req.header("user-agent") || "unknown",
|
|
222
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Fire onMfaVerified hook
|
|
226
|
+
if (ops.onMfaVerified) {
|
|
227
|
+
ops.onMfaVerified(userCtx.userId, factor.id).catch((err) => {
|
|
228
|
+
logger.error("[AuthHooks] onMfaVerified error", {
|
|
229
|
+
error: err instanceof Error ? err.message : err
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return c.json({
|
|
235
|
+
tokens: {
|
|
236
|
+
accessToken,
|
|
237
|
+
refreshToken,
|
|
238
|
+
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* GET /auth/mfa/factors
|
|
245
|
+
* List enrolled MFA factors for the current user
|
|
246
|
+
*/
|
|
247
|
+
router.get("/mfa/factors", requireAuth, async (c) => {
|
|
248
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
249
|
+
if (!userCtx) {
|
|
250
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const factors = await authRepo.getMfaFactors(userCtx.userId);
|
|
254
|
+
return c.json({
|
|
255
|
+
factors: factors.map((f) => ({
|
|
256
|
+
id: f.id,
|
|
257
|
+
factorType: f.factorType,
|
|
258
|
+
friendlyName: f.friendlyName,
|
|
259
|
+
verified: f.verified,
|
|
260
|
+
createdAt: f.createdAt
|
|
261
|
+
}))
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* DELETE /auth/mfa/unenroll
|
|
267
|
+
* Remove an MFA factor
|
|
268
|
+
*/
|
|
269
|
+
router.delete("/mfa/unenroll", requireAuth, async (c) => {
|
|
270
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
271
|
+
if (!userCtx) {
|
|
272
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const unenrollSchema = z.object({
|
|
276
|
+
factorId: z.string().min(1, "Factor ID is required")
|
|
277
|
+
});
|
|
278
|
+
const { factorId } = parseBody(unenrollSchema, await c.req.json());
|
|
279
|
+
|
|
280
|
+
// Verify ownership
|
|
281
|
+
const factor = await authRepo.getMfaFactorById(factorId);
|
|
282
|
+
if (!factor || factor.userId !== userCtx.userId) {
|
|
283
|
+
throw ApiError.notFound("MFA factor not found");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await authRepo.deleteMfaFactor(factorId, userCtx.userId);
|
|
287
|
+
|
|
288
|
+
// If no more verified factors, clean up recovery codes
|
|
289
|
+
const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.userId);
|
|
290
|
+
if (!hasFactors) {
|
|
291
|
+
await authRepo.deleteAllRecoveryCodes(userCtx.userId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return c.json({
|
|
295
|
+
success: true,
|
|
296
|
+
message: "MFA factor removed"
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
package/src/auth/mfa.ts
CHANGED
|
@@ -133,7 +133,8 @@ export function generateTotpSecret(issuer: string, accountName: string): {
|
|
|
133
133
|
const encodedAccount = encodeURIComponent(accountName);
|
|
134
134
|
const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
135
135
|
|
|
136
|
-
return { secret,
|
|
136
|
+
return { secret,
|
|
137
|
+
uri };
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// ─── Recovery Codes ──────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Creates a Microsoft / Entra ID (Azure AD) OAuth Provider integration.
|
|
@@ -41,7 +42,7 @@ export function createMicrosoftProvider(config: {
|
|
|
41
42
|
);
|
|
42
43
|
|
|
43
44
|
if (!tokenResponse.ok) {
|
|
44
|
-
|
|
45
|
+
logger.error("Failed to get Microsoft access token", { detail: await tokenResponse.text() });
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -54,7 +55,7 @@ export function createMicrosoftProvider(config: {
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
if (!profileResponse.ok) {
|
|
57
|
-
|
|
58
|
+
logger.error("Failed to get Microsoft user info", { detail: await profileResponse.text() });
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -67,7 +68,7 @@ export function createMicrosoftProvider(config: {
|
|
|
67
68
|
|
|
68
69
|
const email = profileData.mail || profileData.userPrincipalName;
|
|
69
70
|
if (!email) {
|
|
70
|
-
|
|
71
|
+
logger.error("Microsoft user has no email");
|
|
71
72
|
return null;
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -80,7 +81,7 @@ export function createMicrosoftProvider(config: {
|
|
|
80
81
|
photoUrl: null
|
|
81
82
|
};
|
|
82
83
|
} catch (error) {
|
|
83
|
-
|
|
84
|
+
logger.error("Microsoft OAuth error", { error: error });
|
|
84
85
|
return null;
|
|
85
86
|
}
|
|
86
87
|
}
|
package/src/auth/middleware.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { scopeDataDriver } from "./rls-scope";
|
|
|
6
6
|
import { safeCompare } from "./crypto-utils";
|
|
7
7
|
import { isApiKeyToken, validateApiKey } from "./api-keys/api-key-middleware";
|
|
8
8
|
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
9
|
+
import { logger } from "../utils/logger";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Result from a custom auth validator.
|
|
@@ -295,7 +296,7 @@ code: "UNAUTHORIZED" } }, 401);
|
|
|
295
296
|
roles: ["admin"]
|
|
296
297
|
}));
|
|
297
298
|
} catch (error) {
|
|
298
|
-
|
|
299
|
+
logger.error("[AUTH] RLS scoping failed for service key", { error: error });
|
|
299
300
|
return c.json({ error: { message: "Internal authentication error",
|
|
300
301
|
code: "INTERNAL_ERROR" } }, 500);
|
|
301
302
|
}
|
|
@@ -305,7 +306,7 @@ code: "INTERNAL_ERROR" } }, 500);
|
|
|
305
306
|
// api_keys table instead of JWT verification.
|
|
306
307
|
const result = await validateApiKey(c, token, {
|
|
307
308
|
store: apiKeyStore,
|
|
308
|
-
driver
|
|
309
|
+
driver
|
|
309
310
|
});
|
|
310
311
|
if (result !== true) {
|
|
311
312
|
return result;
|
|
@@ -322,7 +323,7 @@ roles: payload.roles };
|
|
|
322
323
|
c.set("driver", await scopeDataDriver(driver, user));
|
|
323
324
|
} catch (error) {
|
|
324
325
|
// withAuth() failed for a valid token — reject (fail closed)
|
|
325
|
-
|
|
326
|
+
logger.error("[AUTH] RLS scoping failed for authenticated user", { error: error });
|
|
326
327
|
return c.json({ error: { message: "Internal authentication error",
|
|
327
328
|
code: "INTERNAL_ERROR" } }, 500);
|
|
328
329
|
}
|
|
@@ -342,7 +343,7 @@ code: "UNAUTHORIZED" } }, 401);
|
|
|
342
343
|
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
343
344
|
roles: ["anon"] }));
|
|
344
345
|
} catch (error) {
|
|
345
|
-
|
|
346
|
+
logger.error("[AUTH] Failed to create anon-scoped driver", { error: error });
|
|
346
347
|
return c.json({ error: { message: "Server configuration error",
|
|
347
348
|
code: "INTERNAL_ERROR" } }, 500);
|
|
348
349
|
}
|
package/src/auth/rate-limiter.ts
CHANGED
|
@@ -159,7 +159,7 @@ export function apiKeyKeyGenerator(c: Parameters<MiddlewareHandler<HonoEnv>>[0])
|
|
|
159
159
|
*/
|
|
160
160
|
export function createApiKeyRateLimiter(
|
|
161
161
|
defaultLimit = 1000,
|
|
162
|
-
windowMs = 15 * 60 * 1000
|
|
162
|
+
windowMs = 15 * 60 * 1000
|
|
163
163
|
): MiddlewareHandler<HonoEnv> {
|
|
164
164
|
// We maintain a single shared store keyed by API key ID.
|
|
165
165
|
// The actual limit is resolved per-request from the key's metadata.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone admin endpoint for resetting a user's password.
|
|
3
|
+
*
|
|
4
|
+
* Hook resolution order:
|
|
5
|
+
* 1. Collection-level hook (`auth.onResetPassword` on the collection)
|
|
6
|
+
* 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
|
|
7
|
+
* 3. Built-in default (send reset email, or generate temp password)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import { ApiError, errorHandler } from "../api/errors";
|
|
12
|
+
import type { AuthRepository } from "./interfaces";
|
|
13
|
+
import { createRequireAuth, requireAdmin } from "./middleware";
|
|
14
|
+
import type { AuthHooks } from "./auth-hooks";
|
|
15
|
+
import { resolveAuthHooks } from "./auth-hooks";
|
|
16
|
+
import { generateSecurePassword, generateSecureToken, hashToken } from "./admin-user-ops";
|
|
17
|
+
import { getPasswordResetTemplate } from "../email/templates";
|
|
18
|
+
import type { EmailService, EmailConfig } from "../email";
|
|
19
|
+
import type { HonoEnv } from "../api/types";
|
|
20
|
+
import type { AuthCollectionConfig } from "@rebasepro/types";
|
|
21
|
+
import { logger } from "../utils/logger";
|
|
22
|
+
|
|
23
|
+
export interface ResetPasswordRouteConfig {
|
|
24
|
+
authRepo: AuthRepository;
|
|
25
|
+
emailService?: EmailService;
|
|
26
|
+
emailConfig?: EmailConfig;
|
|
27
|
+
serviceKey?: string;
|
|
28
|
+
authHooks?: AuthHooks;
|
|
29
|
+
/** The parsed auth config from the collection, if available. */
|
|
30
|
+
collectionAuthConfig?: AuthCollectionConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a standalone admin route for resetting user passwords.
|
|
35
|
+
*
|
|
36
|
+
* Mounts: POST /users/:userId/reset-password
|
|
37
|
+
*/
|
|
38
|
+
export function createResetPasswordRoute(config: ResetPasswordRouteConfig): Hono<HonoEnv> {
|
|
39
|
+
const router = new Hono<HonoEnv>();
|
|
40
|
+
const authRepo = config.authRepo;
|
|
41
|
+
const { emailService, emailConfig, collectionAuthConfig } = config;
|
|
42
|
+
const ops = resolveAuthHooks(config.authHooks);
|
|
43
|
+
|
|
44
|
+
router.onError(errorHandler);
|
|
45
|
+
router.use("/*", createRequireAuth({ serviceKey: config.serviceKey }));
|
|
46
|
+
|
|
47
|
+
router.post("/users/:userId/reset-password", requireAdmin, async (c) => {
|
|
48
|
+
const userId = c.req.param("userId");
|
|
49
|
+
const existing = await authRepo.getUserById(userId);
|
|
50
|
+
if (!existing) {
|
|
51
|
+
throw ApiError.notFound("User not found");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let invitationSent = false;
|
|
55
|
+
let temporaryPassword: string | undefined;
|
|
56
|
+
|
|
57
|
+
// 1. Collection-level hook (closest to the data)
|
|
58
|
+
if (collectionAuthConfig?.onResetPassword) {
|
|
59
|
+
const isEmailConfigured = !!(emailService && emailService.isConfigured());
|
|
60
|
+
const hookResult = await collectionAuthConfig.onResetPassword(existing.id, {
|
|
61
|
+
hashPassword: (password: string) => ops.hashPassword(password),
|
|
62
|
+
sendEmail: isEmailConfigured
|
|
63
|
+
? (options) => emailService!.send(options)
|
|
64
|
+
: undefined,
|
|
65
|
+
emailConfigured: isEmailConfigured,
|
|
66
|
+
appName: emailConfig?.appName || "Rebase",
|
|
67
|
+
resetPasswordUrl: emailConfig?.resetPasswordUrl || ""
|
|
68
|
+
});
|
|
69
|
+
temporaryPassword = hookResult.temporaryPassword;
|
|
70
|
+
invitationSent = hookResult.invitationSent ?? false;
|
|
71
|
+
}
|
|
72
|
+
// 2. Backend-level hook (global override)
|
|
73
|
+
else if (ops.onAdminResetPassword) {
|
|
74
|
+
const hookResult = await ops.onAdminResetPassword(existing.id, {
|
|
75
|
+
authRepo,
|
|
76
|
+
emailService,
|
|
77
|
+
emailConfig
|
|
78
|
+
});
|
|
79
|
+
temporaryPassword = hookResult.temporaryPassword;
|
|
80
|
+
invitationSent = hookResult.invitationSent ?? false;
|
|
81
|
+
}
|
|
82
|
+
// 3. Built-in default
|
|
83
|
+
else {
|
|
84
|
+
const isEmailConfigured = !!(emailService && emailService.isConfigured());
|
|
85
|
+
|
|
86
|
+
if (isEmailConfigured) {
|
|
87
|
+
try {
|
|
88
|
+
const token = generateSecureToken();
|
|
89
|
+
const tokenHash = hashToken(token);
|
|
90
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
91
|
+
|
|
92
|
+
await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
|
|
93
|
+
|
|
94
|
+
const baseUrl = emailConfig?.resetPasswordUrl || "";
|
|
95
|
+
const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
|
|
96
|
+
|
|
97
|
+
const appName = emailConfig?.appName || "Rebase";
|
|
98
|
+
const templateFn = emailConfig?.templates?.passwordReset;
|
|
99
|
+
const emailContent = templateFn
|
|
100
|
+
? templateFn(setPasswordUrl, { email: existing.email,
|
|
101
|
+
displayName: existing.displayName })
|
|
102
|
+
: getPasswordResetTemplate(setPasswordUrl, { email: existing.email,
|
|
103
|
+
displayName: existing.displayName }, appName);
|
|
104
|
+
|
|
105
|
+
await emailService!.send({
|
|
106
|
+
to: existing.email,
|
|
107
|
+
subject: emailContent.subject,
|
|
108
|
+
html: emailContent.html,
|
|
109
|
+
text: emailContent.text
|
|
110
|
+
});
|
|
111
|
+
invitationSent = true;
|
|
112
|
+
} catch (emailError: unknown) {
|
|
113
|
+
logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
114
|
+
// Fall back to returning the temporary password
|
|
115
|
+
const clearPassword = generateSecurePassword();
|
|
116
|
+
const passwordHash = await ops.hashPassword(clearPassword);
|
|
117
|
+
await authRepo.updatePassword(existing.id, passwordHash);
|
|
118
|
+
temporaryPassword = clearPassword;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// No email service — generate password, set it, and return one-time
|
|
122
|
+
const clearPassword = generateSecurePassword();
|
|
123
|
+
const passwordHash = await ops.hashPassword(clearPassword);
|
|
124
|
+
await authRepo.updatePassword(existing.id, passwordHash);
|
|
125
|
+
temporaryPassword = clearPassword;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const userRoles = await authRepo.getUserRoleIds(existing.id);
|
|
130
|
+
|
|
131
|
+
return c.json({
|
|
132
|
+
user: {
|
|
133
|
+
uid: existing.id,
|
|
134
|
+
email: existing.email,
|
|
135
|
+
displayName: existing.displayName,
|
|
136
|
+
roles: userRoles
|
|
137
|
+
},
|
|
138
|
+
invitationSent,
|
|
139
|
+
...(temporaryPassword ? { temporaryPassword } : {})
|
|
140
|
+
}, 200);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return router;
|
|
144
|
+
}
|
package/src/auth/rls-scope.ts
CHANGED
|
@@ -47,7 +47,7 @@ function isRLSScopedDriver(driver: DataDriver): driver is RLSScopedDriver {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function scopeDataDriver(
|
|
49
49
|
driver: DataDriver,
|
|
50
|
-
user: { uid: string; roles?: string[] }
|
|
50
|
+
user: { uid: string; roles?: string[] }
|
|
51
51
|
): Promise<DataDriver> {
|
|
52
52
|
if (isRLSScopedDriver(driver)) {
|
|
53
53
|
// Fail closed — do NOT catch and swallow errors here.
|