@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
|
@@ -21,6 +21,7 @@ import type { HonoEnv } from "../api/types";
|
|
|
21
21
|
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
22
22
|
import { scopeDataDriver } from "./rls-scope";
|
|
23
23
|
import { validateApiKey } from "./api-keys/api-key-middleware";
|
|
24
|
+
import { logger } from "../utils/logger";
|
|
24
25
|
|
|
25
26
|
export interface AdapterAuthMiddlewareOptions {
|
|
26
27
|
/** The auth adapter to delegate verification to. */
|
|
@@ -48,7 +49,8 @@ export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOption
|
|
|
48
49
|
const authHeader = c.req.header("authorization") || "";
|
|
49
50
|
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
50
51
|
if (token.startsWith("rk_")) {
|
|
51
|
-
const result = await validateApiKey(c, token, { store: apiKeyStore,
|
|
52
|
+
const result = await validateApiKey(c, token, { store: apiKeyStore,
|
|
53
|
+
driver });
|
|
52
54
|
if (result === true) return next();
|
|
53
55
|
return result;
|
|
54
56
|
}
|
|
@@ -60,7 +62,8 @@ export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOption
|
|
|
60
62
|
authenticatedUser = await adapter.verifyRequest(c.req.raw);
|
|
61
63
|
} catch (error) {
|
|
62
64
|
// adapter.verifyRequest() threw — reject the request (fail closed)
|
|
63
|
-
return c.json({ error: { message: "Unauthorized",
|
|
65
|
+
return c.json({ error: { message: "Unauthorized",
|
|
66
|
+
code: "UNAUTHORIZED" } }, 401);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
if (authenticatedUser) {
|
|
@@ -68,30 +71,34 @@ export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOption
|
|
|
68
71
|
c.set("user", {
|
|
69
72
|
userId: authenticatedUser.uid,
|
|
70
73
|
email: authenticatedUser.email,
|
|
71
|
-
roles: authenticatedUser.roles
|
|
74
|
+
roles: authenticatedUser.roles
|
|
72
75
|
});
|
|
73
76
|
try {
|
|
74
77
|
c.set("driver", await scopeDataDriver(driver, {
|
|
75
78
|
uid: authenticatedUser.uid,
|
|
76
|
-
roles: authenticatedUser.roles
|
|
79
|
+
roles: authenticatedUser.roles
|
|
77
80
|
}));
|
|
78
81
|
} catch (error) {
|
|
79
|
-
|
|
80
|
-
return c.json({ error: { message: "Internal authentication error",
|
|
82
|
+
logger.error("[AUTH-ADAPTER] RLS scoping failed for authenticated user", { error: error });
|
|
83
|
+
return c.json({ error: { message: "Internal authentication error",
|
|
84
|
+
code: "INTERNAL_ERROR" } }, 500);
|
|
81
85
|
}
|
|
82
86
|
} else {
|
|
83
87
|
// Not authenticated — scope as anon for RLS evaluation
|
|
84
88
|
try {
|
|
85
|
-
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
89
|
+
c.set("driver", await scopeDataDriver(driver, { uid: "anon",
|
|
90
|
+
roles: ["anon"] }));
|
|
86
91
|
} catch (error) {
|
|
87
|
-
|
|
88
|
-
return c.json({ error: { message: "Server configuration error",
|
|
92
|
+
logger.error("[AUTH-ADAPTER] Failed to create anon-scoped driver", { error: error });
|
|
93
|
+
return c.json({ error: { message: "Server configuration error",
|
|
94
|
+
code: "INTERNAL_ERROR" } }, 500);
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
// Enforce auth if required
|
|
93
99
|
if (enforceAuth && !c.get("user")) {
|
|
94
|
-
return c.json({ error: { message: "Unauthorized: Authentication required",
|
|
100
|
+
return c.json({ error: { message: "Unauthorized: Authentication required",
|
|
101
|
+
code: "UNAUTHORIZED" } }, 401);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
return next();
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin User Operations
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities and orchestration for admin-initiated user management
|
|
5
|
+
* (user creation via REST API, password reset via admin panel).
|
|
6
|
+
*
|
|
7
|
+
* Hook resolution order:
|
|
8
|
+
* 1. Collection-level hook (`auth.onCreateUser` on the collection) — closest to the data
|
|
9
|
+
* 2. Backend-level hook (`AuthHooks.onAdminCreateUser`) — global override
|
|
10
|
+
* 3. Built-in default — framework fallback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomBytes, createHash, randomInt } from "node:crypto";
|
|
14
|
+
import type { AuthRepository } from "./interfaces";
|
|
15
|
+
import type { EmailService, EmailConfig } from "../email";
|
|
16
|
+
import type { ResolvedAuthHooks } from "./auth-hooks";
|
|
17
|
+
import type { AuthCollectionConfig, AuthCollectionContext } from "@rebasepro/types";
|
|
18
|
+
import { getPasswordResetTemplate } from "../email/templates";
|
|
19
|
+
import { logger } from "../utils/logger";
|
|
20
|
+
|
|
21
|
+
// ─── Shared Crypto Utilities ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a cryptographically secure random password that meets strength requirements.
|
|
25
|
+
*
|
|
26
|
+
* 16 characters, guaranteed at least one uppercase, one lowercase, one digit.
|
|
27
|
+
* Ambiguous characters (0, O, 1, l, I) are excluded.
|
|
28
|
+
*/
|
|
29
|
+
export function generateSecurePassword(): string {
|
|
30
|
+
const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
31
|
+
const lower = "abcdefghjkmnpqrstuvwxyz";
|
|
32
|
+
const digits = "23456789";
|
|
33
|
+
const all = upper + lower + digits;
|
|
34
|
+
|
|
35
|
+
const pick = (chars: string) => chars[randomInt(chars.length)];
|
|
36
|
+
const parts = [pick(upper), pick(lower), pick(digits)];
|
|
37
|
+
|
|
38
|
+
for (let i = parts.length; i < 16; i++) {
|
|
39
|
+
parts.push(pick(all));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shuffle
|
|
43
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
44
|
+
const j = randomInt(i + 1);
|
|
45
|
+
[parts[i], parts[j]] = [parts[j], parts[i]];
|
|
46
|
+
}
|
|
47
|
+
return parts.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a cryptographically secure random token (80 hex characters).
|
|
52
|
+
*/
|
|
53
|
+
export function generateSecureToken(): string {
|
|
54
|
+
return randomBytes(40).toString("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hash a token for database storage using SHA-256.
|
|
59
|
+
*/
|
|
60
|
+
export function hashToken(token: string): string {
|
|
61
|
+
return createHash("sha256").update(token).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Admin User Creation ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Context needed by admin user creation / password reset operations.
|
|
68
|
+
*/
|
|
69
|
+
export interface AdminUserContext {
|
|
70
|
+
authRepo: AuthRepository;
|
|
71
|
+
emailService?: EmailService;
|
|
72
|
+
emailConfig?: EmailConfig;
|
|
73
|
+
resolvedHooks: ResolvedAuthHooks;
|
|
74
|
+
/** The parsed auth config from the collection (if `auth` is an object, not just `true`). */
|
|
75
|
+
collectionAuthConfig?: AuthCollectionConfig;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Result of preparing user values for admin-initiated creation.
|
|
80
|
+
*/
|
|
81
|
+
export interface AdminUserPrepareResult {
|
|
82
|
+
/** Values ready for `driver.saveEntity()`. */
|
|
83
|
+
values: Record<string, unknown>;
|
|
84
|
+
/** The cleartext password (for returning to admin or sending via email). */
|
|
85
|
+
clearPassword?: string;
|
|
86
|
+
/** Whether the hook already handled the invitation email. */
|
|
87
|
+
hookHandledEmail: boolean;
|
|
88
|
+
/** Whether an invitation was sent (only relevant when hookHandledEmail is true). */
|
|
89
|
+
invitationSent: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the `AuthCollectionContext` facade from server internals.
|
|
94
|
+
*
|
|
95
|
+
* This is the simplified context exposed to collection-level auth hooks,
|
|
96
|
+
* keeping them decoupled from internal interfaces like `AuthRepository`.
|
|
97
|
+
*/
|
|
98
|
+
function buildCollectionContext(ctx: AdminUserContext): AuthCollectionContext {
|
|
99
|
+
const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
hashPassword: (password: string) => ctx.resolvedHooks.hashPassword(password),
|
|
103
|
+
sendEmail: isEmailConfigured
|
|
104
|
+
? (options) => ctx.emailService!.send(options)
|
|
105
|
+
: undefined,
|
|
106
|
+
emailConfigured: isEmailConfigured,
|
|
107
|
+
appName: ctx.emailConfig?.appName || "Rebase",
|
|
108
|
+
resetPasswordUrl: ctx.emailConfig?.resetPasswordUrl || ""
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Prepare user values for an admin-initiated user creation.
|
|
114
|
+
*
|
|
115
|
+
* Resolution order:
|
|
116
|
+
* 1. Collection-level `auth.onCreateUser` — closest to the data
|
|
117
|
+
* 2. Backend-level `AuthHooks.onAdminCreateUser` — global override
|
|
118
|
+
* 3. Built-in default — generate password → hash → normalize email
|
|
119
|
+
*
|
|
120
|
+
* The caller is responsible for persisting (via `driver.saveEntity()`).
|
|
121
|
+
*/
|
|
122
|
+
export async function prepareAdminUserValues(
|
|
123
|
+
body: Record<string, unknown>,
|
|
124
|
+
ctx: AdminUserContext
|
|
125
|
+
): Promise<AdminUserPrepareResult> {
|
|
126
|
+
const { resolvedHooks, collectionAuthConfig } = ctx;
|
|
127
|
+
|
|
128
|
+
// 1. Collection-level hook (closest to the data)
|
|
129
|
+
if (collectionAuthConfig?.onCreateUser) {
|
|
130
|
+
const collectionCtx = buildCollectionContext(ctx);
|
|
131
|
+
const hookResult = await collectionAuthConfig.onCreateUser(body, collectionCtx);
|
|
132
|
+
return {
|
|
133
|
+
values: hookResult.values,
|
|
134
|
+
clearPassword: hookResult.temporaryPassword,
|
|
135
|
+
hookHandledEmail: true,
|
|
136
|
+
invitationSent: hookResult.invitationSent ?? false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2. Backend-level hook (global override)
|
|
141
|
+
if (resolvedHooks.onAdminCreateUser) {
|
|
142
|
+
const hookResult = await resolvedHooks.onAdminCreateUser(body, {
|
|
143
|
+
authRepo: ctx.authRepo,
|
|
144
|
+
emailService: ctx.emailService,
|
|
145
|
+
emailConfig: ctx.emailConfig,
|
|
146
|
+
hashPassword: (password: string) => resolvedHooks.hashPassword(password)
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
values: hookResult.values,
|
|
150
|
+
clearPassword: hookResult.temporaryPassword,
|
|
151
|
+
hookHandledEmail: true,
|
|
152
|
+
invitationSent: hookResult.invitationSent ?? false
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Built-in default
|
|
157
|
+
const password = body.password as string | undefined;
|
|
158
|
+
const clearPassword = password || generateSecurePassword();
|
|
159
|
+
const passwordHash = await resolvedHooks.hashPassword(clearPassword);
|
|
160
|
+
|
|
161
|
+
const values = { ...body };
|
|
162
|
+
values.passwordHash = passwordHash;
|
|
163
|
+
if (values.email) {
|
|
164
|
+
values.email = (values.email as string).toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
values.emailVerified = true;
|
|
167
|
+
delete values.password;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
values,
|
|
171
|
+
clearPassword: password ? undefined : clearPassword,
|
|
172
|
+
hookHandledEmail: false,
|
|
173
|
+
invitationSent: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle post-creation work for admin-created users.
|
|
179
|
+
*
|
|
180
|
+
* Sends an invitation email (password-reset link) if email is configured
|
|
181
|
+
* and no explicit password was provided. Falls back to returning the
|
|
182
|
+
* temporary password if email fails or is not configured.
|
|
183
|
+
*/
|
|
184
|
+
export async function finalizeAdminUserCreation(
|
|
185
|
+
entity: { id: string; values: Record<string, unknown> },
|
|
186
|
+
clearPassword: string | undefined,
|
|
187
|
+
ctx: AdminUserContext
|
|
188
|
+
): Promise<{
|
|
189
|
+
temporaryPassword?: string;
|
|
190
|
+
invitationSent: boolean;
|
|
191
|
+
}> {
|
|
192
|
+
// If an explicit password was provided (clearPassword is undefined), nothing to do
|
|
193
|
+
if (!clearPassword) {
|
|
194
|
+
return { invitationSent: false };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());
|
|
198
|
+
|
|
199
|
+
if (isEmailConfigured) {
|
|
200
|
+
try {
|
|
201
|
+
const token = generateSecureToken();
|
|
202
|
+
const tokenHash = hashToken(token);
|
|
203
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
|
|
204
|
+
|
|
205
|
+
await ctx.authRepo.createPasswordResetToken(entity.id, tokenHash, expiresAt);
|
|
206
|
+
|
|
207
|
+
const baseUrl = ctx.emailConfig?.resetPasswordUrl || "";
|
|
208
|
+
const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;
|
|
209
|
+
|
|
210
|
+
const appName = ctx.emailConfig?.appName || "Rebase";
|
|
211
|
+
const templateFn = ctx.emailConfig?.templates?.passwordReset;
|
|
212
|
+
const emailContent = templateFn
|
|
213
|
+
? templateFn(setPasswordUrl, { email: entity.values.email as string,
|
|
214
|
+
displayName: entity.values.displayName as string })
|
|
215
|
+
: getPasswordResetTemplate(setPasswordUrl, { email: entity.values.email as string,
|
|
216
|
+
displayName: entity.values.displayName as string }, appName);
|
|
217
|
+
|
|
218
|
+
await ctx.emailService!.send({
|
|
219
|
+
to: entity.values.email as string,
|
|
220
|
+
subject: emailContent.subject,
|
|
221
|
+
html: emailContent.html,
|
|
222
|
+
text: emailContent.text
|
|
223
|
+
});
|
|
224
|
+
return { invitationSent: true };
|
|
225
|
+
} catch (emailError: unknown) {
|
|
226
|
+
logger.error("Failed to send reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
227
|
+
// Fall back to returning the temporary password
|
|
228
|
+
return { temporaryPassword: clearPassword,
|
|
229
|
+
invitationSent: false };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// No email service — return the temporary password
|
|
234
|
+
return { temporaryPassword: clearPassword,
|
|
235
|
+
invitationSent: false };
|
|
236
|
+
}
|
|
@@ -20,6 +20,7 @@ import type { HonoEnv } from "../../api/types";
|
|
|
20
20
|
import type { ApiKeyStore } from "./api-key-store";
|
|
21
21
|
import type { ApiKeyMasked } from "./api-key-types";
|
|
22
22
|
import { scopeDataDriver } from "../rls-scope";
|
|
23
|
+
import { logger } from "../../utils/logger";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Check whether a token looks like a Rebase API key.
|
|
@@ -55,7 +56,7 @@ export interface ApiKeyAuthOptions {
|
|
|
55
56
|
export async function validateApiKey(
|
|
56
57
|
c: Context<HonoEnv>,
|
|
57
58
|
token: string,
|
|
58
|
-
options: ApiKeyAuthOptions
|
|
59
|
+
options: ApiKeyAuthOptions
|
|
59
60
|
): Promise<Response | true> {
|
|
60
61
|
const { store, driver } = options;
|
|
61
62
|
|
|
@@ -64,27 +65,31 @@ export async function validateApiKey(
|
|
|
64
65
|
|
|
65
66
|
if (!apiKey) {
|
|
66
67
|
return c.json({
|
|
67
|
-
error: { message: "Invalid API key",
|
|
68
|
+
error: { message: "Invalid API key",
|
|
69
|
+
code: "UNAUTHORIZED" }
|
|
68
70
|
}, 401);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// Check revocation
|
|
72
74
|
if (apiKey.revoked_at) {
|
|
73
75
|
return c.json({
|
|
74
|
-
error: { message: "API key has been revoked",
|
|
76
|
+
error: { message: "API key has been revoked",
|
|
77
|
+
code: "UNAUTHORIZED" }
|
|
75
78
|
}, 401);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
// Check expiration
|
|
79
82
|
if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
|
|
80
83
|
return c.json({
|
|
81
|
-
error: { message: "API key has expired",
|
|
84
|
+
error: { message: "API key has expired",
|
|
85
|
+
code: "UNAUTHORIZED" }
|
|
82
86
|
}, 401);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
// Set user identity — API keys represent service accounts
|
|
86
90
|
const userId = `api-key:${apiKey.id}`;
|
|
87
|
-
c.set("user", { userId,
|
|
91
|
+
c.set("user", { userId,
|
|
92
|
+
roles: [] });
|
|
88
93
|
|
|
89
94
|
// Expose masked key metadata for downstream permission checks
|
|
90
95
|
const masked: ApiKeyMasked = {
|
|
@@ -98,7 +103,7 @@ export async function validateApiKey(
|
|
|
98
103
|
updated_at: apiKey.updated_at,
|
|
99
104
|
last_used_at: apiKey.last_used_at,
|
|
100
105
|
expires_at: apiKey.expires_at,
|
|
101
|
-
revoked_at: apiKey.revoked_at
|
|
106
|
+
revoked_at: apiKey.revoked_at
|
|
102
107
|
};
|
|
103
108
|
// Store apiKey in the context for permission checking in api-generator
|
|
104
109
|
c.set("apiKey", masked);
|
|
@@ -107,13 +112,14 @@ export async function validateApiKey(
|
|
|
107
112
|
try {
|
|
108
113
|
const scopedDriver = await scopeDataDriver(driver, {
|
|
109
114
|
uid: userId,
|
|
110
|
-
roles: ["service"]
|
|
115
|
+
roles: ["service"]
|
|
111
116
|
});
|
|
112
117
|
c.set("driver", scopedDriver);
|
|
113
118
|
} catch (error) {
|
|
114
|
-
|
|
119
|
+
logger.error("[AUTH] RLS scoping failed for API key", { error: error });
|
|
115
120
|
return c.json({
|
|
116
|
-
error: { message: "Internal authentication error",
|
|
121
|
+
error: { message: "Internal authentication error",
|
|
122
|
+
code: "INTERNAL_ERROR" }
|
|
117
123
|
}, 500);
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -52,7 +52,7 @@ export function httpMethodToOperation(method: string): ApiKeyOperation {
|
|
|
52
52
|
export function isOperationAllowed(
|
|
53
53
|
permissions: ApiKeyPermission[],
|
|
54
54
|
collection: string,
|
|
55
|
-
operation: ApiKeyOperation
|
|
55
|
+
operation: ApiKeyOperation
|
|
56
56
|
): boolean {
|
|
57
57
|
for (const perm of permissions) {
|
|
58
58
|
const collectionMatch = perm.collection === "*" || perm.collection === collection;
|
|
@@ -68,7 +68,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
68
68
|
if (!validatePermissions(permissions)) {
|
|
69
69
|
throw ApiError.badRequest(
|
|
70
70
|
"Permissions must be an array of { collection: string, operations: ('read' | 'write' | 'delete')[] }",
|
|
71
|
-
"INVALID_INPUT"
|
|
71
|
+
"INVALID_INPUT"
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -97,7 +97,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
97
97
|
name: name.trim(),
|
|
98
98
|
permissions: permissions as ApiKeyPermission[],
|
|
99
99
|
rate_limit: (rate_limit as number | null) ?? null,
|
|
100
|
-
expires_at: (expires_at as string | null) ?? null
|
|
100
|
+
expires_at: (expires_at as string | null) ?? null
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const keyWithSecret = await store.createApiKey(request, createdBy);
|
|
@@ -134,7 +134,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
134
134
|
if (!validatePermissions(permissions)) {
|
|
135
135
|
throw ApiError.badRequest(
|
|
136
136
|
"Permissions must be an array of { collection: string, operations: ('read' | 'write' | 'delete')[] }",
|
|
137
|
-
"INVALID_INPUT"
|
|
137
|
+
"INVALID_INPUT"
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -15,9 +15,10 @@ import { logger } from "../../utils/logger";
|
|
|
15
15
|
import type {
|
|
16
16
|
ApiKey,
|
|
17
17
|
ApiKeyMasked,
|
|
18
|
+
ApiKeyPermission,
|
|
18
19
|
ApiKeyWithSecret,
|
|
19
20
|
CreateApiKeyRequest,
|
|
20
|
-
UpdateApiKeyRequest
|
|
21
|
+
UpdateApiKeyRequest
|
|
21
22
|
} from "./api-key-types";
|
|
22
23
|
|
|
23
24
|
const TABLE = "rebase.api_keys";
|
|
@@ -64,7 +65,7 @@ function toMasked(row: ApiKey): ApiKeyMasked {
|
|
|
64
65
|
updated_at: row.updated_at,
|
|
65
66
|
last_used_at: row.last_used_at,
|
|
66
67
|
expires_at: row.expires_at,
|
|
67
|
-
revoked_at: row.revoked_at
|
|
68
|
+
revoked_at: row.revoked_at
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -72,9 +73,14 @@ function toMasked(row: ApiKey): ApiKeyMasked {
|
|
|
72
73
|
* Parse a raw DB row into the typed `ApiKey` shape.
|
|
73
74
|
*/
|
|
74
75
|
function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let permissions = (row.permissions ?? []) as ApiKeyPermission[];
|
|
77
|
+
if (typeof row.permissions === "string") {
|
|
78
|
+
try {
|
|
79
|
+
permissions = JSON.parse(row.permissions) as ApiKeyPermission[];
|
|
80
|
+
} catch {
|
|
81
|
+
permissions = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
78
84
|
|
|
79
85
|
return {
|
|
80
86
|
id: row.id as string,
|
|
@@ -90,7 +96,7 @@ function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
|
|
90
96
|
updated_at: new Date(row.updated_at as string).toISOString(),
|
|
91
97
|
last_used_at: row.last_used_at ? new Date(row.last_used_at as string).toISOString() : null,
|
|
92
98
|
expires_at: row.expires_at ? new Date(row.expires_at as string).toISOString() : null,
|
|
93
|
-
revoked_at: row.revoked_at ? new Date(row.revoked_at as string).toISOString() : null
|
|
99
|
+
revoked_at: row.revoked_at ? new Date(row.revoked_at as string).toISOString() : null
|
|
94
100
|
};
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -212,7 +218,7 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
212
218
|
const apiKey = rowToApiKey(rows[0]);
|
|
213
219
|
return {
|
|
214
220
|
...toMasked(apiKey),
|
|
215
|
-
key: plaintext
|
|
221
|
+
key: plaintext
|
|
216
222
|
};
|
|
217
223
|
},
|
|
218
224
|
|
|
@@ -312,6 +318,6 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
312
318
|
// Non-blocking — don't fail requests because of a usage timestamp update
|
|
313
319
|
logger.error("[api-key-store] Failed to update last_used_at", { error: err });
|
|
314
320
|
}
|
|
315
|
-
}
|
|
321
|
+
}
|
|
316
322
|
};
|
|
317
323
|
}
|
|
@@ -14,7 +14,7 @@ export type {
|
|
|
14
14
|
ApiKeyPermission,
|
|
15
15
|
ApiKeyWithSecret,
|
|
16
16
|
CreateApiKeyRequest,
|
|
17
|
-
UpdateApiKeyRequest
|
|
17
|
+
UpdateApiKeyRequest
|
|
18
18
|
} from "./api-key-types";
|
|
19
19
|
|
|
20
20
|
// Store
|
|
@@ -28,7 +28,7 @@ export type { ApiKeyAuthOptions } from "./api-key-middleware";
|
|
|
28
28
|
// Permission guard
|
|
29
29
|
export {
|
|
30
30
|
httpMethodToOperation,
|
|
31
|
-
isOperationAllowed
|
|
31
|
+
isOperationAllowed
|
|
32
32
|
} from "./api-key-permission-guard";
|
|
33
33
|
export type { ApiKeyOperation } from "./api-key-permission-guard";
|
|
34
34
|
|
package/src/auth/apple-oauth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import jwt from "jsonwebtoken";
|
|
4
|
+
import { logger } from "../utils/logger";
|
|
4
5
|
/**
|
|
5
6
|
* Creates an Apple Sign In OAuth Provider integration.
|
|
6
7
|
*
|
|
@@ -75,7 +76,7 @@ export function createAppleProvider(config: {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
if (!tokenResponse.ok) {
|
|
78
|
-
|
|
79
|
+
logger.error("Failed to get Apple access token", { detail: await tokenResponse.text() });
|
|
79
80
|
return null;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -95,7 +96,7 @@ export function createAppleProvider(config: {
|
|
|
95
96
|
// the user object from the first auth for us to capture the name.
|
|
96
97
|
const email = decoded.email || payload.user?.email;
|
|
97
98
|
if (!email) {
|
|
98
|
-
|
|
99
|
+
logger.error("Apple user has no email");
|
|
99
100
|
return null;
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -112,7 +113,7 @@ export function createAppleProvider(config: {
|
|
|
112
113
|
photoUrl: null // Apple does not provide a profile photo
|
|
113
114
|
};
|
|
114
115
|
} catch (error) {
|
|
115
|
-
|
|
116
|
+
logger.error("Apple OAuth error", { error: error });
|
|
116
117
|
return null;
|
|
117
118
|
}
|
|
118
119
|
}
|
package/src/auth/auth-hooks.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "./password";
|
|
45
45
|
import type { PasswordValidationResult } from "./password";
|
|
46
46
|
import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
|
|
47
|
+
import type { EmailService, EmailConfig } from "../email";
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Authentication method identifier for lifecycle hooks.
|
|
@@ -205,28 +206,67 @@ export interface AuthHooks {
|
|
|
205
206
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
206
207
|
*/
|
|
207
208
|
afterUserDelete?(userId: string): Promise<void>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Optional hook to customize or override the default user creation flow via the admin panel/REST API.
|
|
212
|
+
* When provided, this replaces the built-in password generation, hashing, and invitation email logic.
|
|
213
|
+
*/
|
|
214
|
+
onAdminCreateUser?(
|
|
215
|
+
values: Record<string, unknown>,
|
|
216
|
+
ctx: {
|
|
217
|
+
authRepo: AuthRepository;
|
|
218
|
+
emailService?: EmailService;
|
|
219
|
+
emailConfig?: EmailConfig;
|
|
220
|
+
hashPassword: (password: string) => Promise<string>;
|
|
221
|
+
}
|
|
222
|
+
): Promise<{
|
|
223
|
+
values: Record<string, unknown>;
|
|
224
|
+
temporaryPassword?: string;
|
|
225
|
+
invitationSent?: boolean;
|
|
226
|
+
}>;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Optional hook to customize or override the default password reset flow via the admin panel.
|
|
230
|
+
* When provided, this replaces the built-in password reset token generation, hashing, and email logic.
|
|
231
|
+
*/
|
|
232
|
+
onAdminResetPassword?(
|
|
233
|
+
userId: string,
|
|
234
|
+
ctx: {
|
|
235
|
+
authRepo: AuthRepository;
|
|
236
|
+
emailService?: EmailService;
|
|
237
|
+
emailConfig?: EmailConfig;
|
|
238
|
+
}
|
|
239
|
+
): Promise<{
|
|
240
|
+
temporaryPassword?: string;
|
|
241
|
+
invitationSent?: boolean;
|
|
242
|
+
}>;
|
|
208
243
|
}
|
|
209
244
|
|
|
210
245
|
/**
|
|
211
|
-
* Resolved auth
|
|
246
|
+
* Resolved auth hooks — password operations are guaranteed to exist,
|
|
247
|
+
* all other hooks are passed through as-is (optional).
|
|
248
|
+
*
|
|
212
249
|
* Created by `resolveAuthHooks()` which merges user hooks
|
|
213
250
|
* with built-in defaults.
|
|
251
|
+
*
|
|
252
|
+
* Consumers should use the resolved object exclusively —
|
|
253
|
+
* never access the raw `AuthHooks` directly.
|
|
214
254
|
*/
|
|
215
|
-
export
|
|
216
|
-
hashPassword
|
|
217
|
-
|
|
218
|
-
validatePasswordStrength(password: string): PasswordValidationResult;
|
|
219
|
-
}
|
|
255
|
+
export type ResolvedAuthHooks =
|
|
256
|
+
Required<Pick<AuthHooks, "hashPassword" | "verifyPassword" | "validatePasswordStrength">>
|
|
257
|
+
& Omit<AuthHooks, "hashPassword" | "verifyPassword" | "validatePasswordStrength">;
|
|
220
258
|
|
|
221
259
|
/**
|
|
222
260
|
* Merge user-provided hooks with the built-in defaults to produce
|
|
223
|
-
* a complete set of resolved
|
|
261
|
+
* a complete set of resolved hooks.
|
|
224
262
|
*
|
|
225
263
|
* This is the single point where defaults are applied — all consumers
|
|
226
|
-
* call this once and use the resolved
|
|
264
|
+
* call this once and use the resolved hooks throughout.
|
|
227
265
|
*/
|
|
228
|
-
export function resolveAuthHooks(hooks?: AuthHooks):
|
|
266
|
+
export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthHooks {
|
|
229
267
|
return {
|
|
268
|
+
...hooks,
|
|
269
|
+
|
|
230
270
|
hashPassword: hooks?.hashPassword
|
|
231
271
|
?? defaultHashPassword,
|
|
232
272
|
|
|
@@ -234,6 +274,6 @@ export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthOperations {
|
|
|
234
274
|
?? defaultVerifyPassword,
|
|
235
275
|
|
|
236
276
|
validatePasswordStrength: hooks?.validatePasswordStrength
|
|
237
|
-
?? defaultValidatePasswordStrength
|
|
277
|
+
?? defaultValidatePasswordStrength
|
|
238
278
|
};
|
|
239
279
|
}
|
|
@@ -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 Bitbucket OAuth Provider integration (OAuth 2.0 consumer).
|
|
@@ -29,7 +30,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
|
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
if (!tokenResponse.ok) {
|
|
32
|
-
|
|
33
|
+
logger.error("Failed to get Bitbucket access token", { detail: await tokenResponse.text() });
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -41,7 +42,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
if (!profileResponse.ok) {
|
|
44
|
-
|
|
45
|
+
logger.error("Failed to get Bitbucket user info", { detail: await profileResponse.text() });
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -65,7 +66,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
|
|
|
65
66
|
email = primary?.email || null;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
if (!email) {
|
|
69
|
+
if (!email) { logger.error("Bitbucket user has no verified email"); return null; }
|
|
69
70
|
|
|
70
71
|
return {
|
|
71
72
|
providerId: p.uuid,
|
|
@@ -74,7 +75,7 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
|
|
|
74
75
|
photoUrl: p.links?.avatar?.href || null
|
|
75
76
|
};
|
|
76
77
|
} catch (error) {
|
|
77
|
-
|
|
78
|
+
logger.error("Bitbucket OAuth error", { error: error });
|
|
78
79
|
return null;
|
|
79
80
|
}
|
|
80
81
|
}
|