@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { ApiError } from "../api/errors";
|
|
5
|
+
import { HonoEnv } from "../api/types";
|
|
6
|
+
import { requireAuth } from "./middleware";
|
|
7
|
+
import { logger } from "../utils/logger";
|
|
8
|
+
import { strictAuthLimiter, defaultAuthLimiter } from "./rate-limiter";
|
|
9
|
+
import { hashRefreshToken } from "./jwt";
|
|
10
|
+
import type { AuthModuleConfig } from "./routes";
|
|
11
|
+
import { resolveAuthHooks } from "./auth-hooks";
|
|
12
|
+
import type { CreateUserData } from "./interfaces";
|
|
13
|
+
|
|
14
|
+
interface SessionRoutesConfig {
|
|
15
|
+
router: Hono<HonoEnv>;
|
|
16
|
+
config: AuthModuleConfig;
|
|
17
|
+
ops: ReturnType<typeof resolveAuthHooks>;
|
|
18
|
+
parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
|
|
19
|
+
buildAuthResponse: (
|
|
20
|
+
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
|
|
21
|
+
roleIds: string[],
|
|
22
|
+
accessToken: string,
|
|
23
|
+
refreshToken: string
|
|
24
|
+
) => unknown;
|
|
25
|
+
createSessionAndTokens: (userId: string, userAgent: string, ipAddress: string) => Promise<{
|
|
26
|
+
roleIds: string[];
|
|
27
|
+
accessToken: string;
|
|
28
|
+
refreshToken: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mountSessionRoutes(opts: SessionRoutesConfig): void {
|
|
33
|
+
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens } = opts;
|
|
34
|
+
const authRepo = config.authRepo;
|
|
35
|
+
|
|
36
|
+
const logoutSchema = z.object({
|
|
37
|
+
refreshToken: z.string().optional()
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const linkSchema = z.object({
|
|
41
|
+
email: z.string().email("Invalid email address").max(255),
|
|
42
|
+
password: z.string().min(1, "Password is required").max(128)
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const updateProfileSchema = z.object({
|
|
46
|
+
displayName: z.string().max(255).optional(),
|
|
47
|
+
photoURL: z.string().url().max(2048).optional()
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const isEmailConfigured = () => !!(config.emailService && config.emailService.isConfigured());
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* POST /auth/logout
|
|
54
|
+
*/
|
|
55
|
+
router.post("/logout", async (c) => {
|
|
56
|
+
const { refreshToken } = parseBody(logoutSchema, await c.req.json());
|
|
57
|
+
|
|
58
|
+
if (refreshToken) {
|
|
59
|
+
const tokenHash = hashRefreshToken(refreshToken);
|
|
60
|
+
await authRepo.deleteRefreshToken(tokenHash);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Call afterLogout hook (fire-and-forget)
|
|
64
|
+
// Extract userId from the access token if present
|
|
65
|
+
const authHeader = c.req.header("authorization");
|
|
66
|
+
if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
|
|
67
|
+
const { verifyAccessToken } = await import("./jwt");
|
|
68
|
+
const payload = verifyAccessToken(authHeader.substring(7));
|
|
69
|
+
if (payload) {
|
|
70
|
+
ops.afterLogout(payload.userId).catch((err) => {
|
|
71
|
+
logger.error("[AuthHooks] afterLogout error", {
|
|
72
|
+
error: err instanceof Error ? err.message : err
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return c.json({ success: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* GET /auth/sessions
|
|
83
|
+
* Get active refresh tokens (sessions) for the current user
|
|
84
|
+
*/
|
|
85
|
+
router.get("/sessions", requireAuth, async (c) => {
|
|
86
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
87
|
+
if (!userCtx) {
|
|
88
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const currentRefreshToken = c.req.header("x-refresh-token") as string;
|
|
92
|
+
const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
|
|
93
|
+
|
|
94
|
+
const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
|
|
95
|
+
|
|
96
|
+
const mappedSessions = sessions.map((s) => ({
|
|
97
|
+
id: s.id,
|
|
98
|
+
userAgent: s.userAgent,
|
|
99
|
+
ipAddress: s.ipAddress,
|
|
100
|
+
createdAt: s.createdAt,
|
|
101
|
+
isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
return c.json({ sessions: mappedSessions });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* DELETE /auth/sessions
|
|
109
|
+
* Delete all refresh tokens for the current user (remote logout every device)
|
|
110
|
+
*/
|
|
111
|
+
router.delete("/sessions", requireAuth, async (c) => {
|
|
112
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
113
|
+
if (!userCtx) {
|
|
114
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
|
|
118
|
+
return c.json({
|
|
119
|
+
success: true,
|
|
120
|
+
message: "All sessions revoked successfully"
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* DELETE /auth/sessions/:id
|
|
126
|
+
* Delete a specific refresh token (remote logout)
|
|
127
|
+
*/
|
|
128
|
+
router.delete("/sessions/:id", requireAuth, async (c) => {
|
|
129
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
130
|
+
if (!userCtx) {
|
|
131
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const id = c.req.param("id");
|
|
135
|
+
if (!id) {
|
|
136
|
+
throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await authRepo.deleteRefreshTokenById(id, userCtx.userId);
|
|
140
|
+
return c.json({
|
|
141
|
+
success: true,
|
|
142
|
+
message: "Session revoked successfully"
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* GET /auth/me
|
|
148
|
+
* Get current authenticated user
|
|
149
|
+
*/
|
|
150
|
+
router.get("/me", requireAuth, async (c) => {
|
|
151
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
152
|
+
if (!userCtx) {
|
|
153
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
157
|
+
if (!result) {
|
|
158
|
+
throw ApiError.notFound("User not found");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return c.json({
|
|
162
|
+
user: {
|
|
163
|
+
uid: result.user.id,
|
|
164
|
+
email: result.user.email,
|
|
165
|
+
displayName: result.user.displayName,
|
|
166
|
+
photoURL: result.user.photoUrl,
|
|
167
|
+
emailVerified: result.user.emailVerified,
|
|
168
|
+
roles: result.roles.map((r) => r.id),
|
|
169
|
+
metadata: result.user.metadata ?? {}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* PATCH /auth/me
|
|
176
|
+
* Update current authenticated user profile
|
|
177
|
+
*/
|
|
178
|
+
router.patch("/me", requireAuth, async (c) => {
|
|
179
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
180
|
+
if (!userCtx) {
|
|
181
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
|
|
185
|
+
|
|
186
|
+
const updatedUser = await authRepo.updateUser(userCtx.userId, {
|
|
187
|
+
displayName: displayName !== undefined ? displayName : undefined,
|
|
188
|
+
photoUrl: photoURL !== undefined ? photoURL : undefined
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!updatedUser) {
|
|
192
|
+
throw ApiError.notFound("User not found");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
196
|
+
if (!result) {
|
|
197
|
+
throw ApiError.notFound("User not found");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return c.json({
|
|
201
|
+
user: {
|
|
202
|
+
uid: result.user.id,
|
|
203
|
+
email: result.user.email,
|
|
204
|
+
displayName: result.user.displayName,
|
|
205
|
+
photoURL: result.user.photoUrl,
|
|
206
|
+
emailVerified: result.user.emailVerified,
|
|
207
|
+
roles: result.roles.map((r) => r.id),
|
|
208
|
+
metadata: result.user.metadata ?? {}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* GET /auth/config
|
|
215
|
+
* Get public auth configuration
|
|
216
|
+
*/
|
|
217
|
+
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
218
|
+
let needsSetup: boolean;
|
|
219
|
+
if (config.isBootstrapCompleted) {
|
|
220
|
+
needsSetup = !(await config.isBootstrapCompleted());
|
|
221
|
+
} else {
|
|
222
|
+
const allUsers = await authRepo.listUsers();
|
|
223
|
+
needsSetup = allUsers.length === 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const registrationAllowed = needsSetup || !!config.allowRegistration;
|
|
227
|
+
const enabledProviders = (config.oauthProviders || []).map((p) => p.id);
|
|
228
|
+
|
|
229
|
+
return c.json({
|
|
230
|
+
needsSetup,
|
|
231
|
+
registrationEnabled: registrationAllowed,
|
|
232
|
+
emailServiceEnabled: isEmailConfigured(),
|
|
233
|
+
enabledProviders
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* POST /auth/anonymous
|
|
239
|
+
* Create an anonymous user with temporary credentials
|
|
240
|
+
*/
|
|
241
|
+
router.post("/anonymous", strictAuthLimiter, async (c) => {
|
|
242
|
+
const anonId = randomBytes(16).toString("hex");
|
|
243
|
+
const anonEmail = `anon_${anonId.slice(0, 8)}@anonymous.local`;
|
|
244
|
+
|
|
245
|
+
let createData: CreateUserData = {
|
|
246
|
+
email: anonEmail,
|
|
247
|
+
emailVerified: false,
|
|
248
|
+
isAnonymous: true
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (ops.beforeUserCreate) {
|
|
252
|
+
createData = await ops.beforeUserCreate(createData);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const user = await authRepo.createUser(createData);
|
|
256
|
+
|
|
257
|
+
// Assign default role (follow register route pattern, but never auto-admin)
|
|
258
|
+
if (config.defaultRole) {
|
|
259
|
+
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
263
|
+
user.id,
|
|
264
|
+
c.req.header("user-agent") || "unknown",
|
|
265
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Fire afterUserCreate hook
|
|
269
|
+
if (ops.afterUserCreate) {
|
|
270
|
+
ops.afterUserCreate(user).catch((err) => {
|
|
271
|
+
logger.error("[AuthHooks] afterUserCreate error", {
|
|
272
|
+
error: err instanceof Error ? err.message : err
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Fire onAuthenticated hook
|
|
278
|
+
if (ops.onAuthenticated) {
|
|
279
|
+
ops.onAuthenticated(user, "anonymous").catch((err) => {
|
|
280
|
+
logger.error("[AuthHooks] onAuthenticated error", {
|
|
281
|
+
error: err instanceof Error ? err.message : err
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* POST /auth/anonymous/link
|
|
291
|
+
* Upgrade an anonymous user to a permanent account with email/password
|
|
292
|
+
*/
|
|
293
|
+
router.post("/anonymous/link", requireAuth, async (c) => {
|
|
294
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
295
|
+
if (!userCtx) {
|
|
296
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const user = await authRepo.getUserById(userCtx.userId);
|
|
300
|
+
if (!user?.isAnonymous) {
|
|
301
|
+
throw ApiError.badRequest("User is not anonymous", "NOT_ANONYMOUS");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { email, password } = parseBody(linkSchema, await c.req.json());
|
|
305
|
+
|
|
306
|
+
// Validate password strength
|
|
307
|
+
const passwordValidation = ops.validatePasswordStrength(password);
|
|
308
|
+
if (!passwordValidation.valid) {
|
|
309
|
+
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if email is already taken
|
|
313
|
+
const existingUser = await authRepo.getUserByEmail(email.toLowerCase());
|
|
314
|
+
if (existingUser) {
|
|
315
|
+
throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Hash password
|
|
319
|
+
const passwordHash = await ops.hashPassword(password);
|
|
320
|
+
|
|
321
|
+
// Update user: set email, password, remove anonymous flag
|
|
322
|
+
const updatedUser = await authRepo.updateUser(user.id, {
|
|
323
|
+
email: email.toLowerCase(),
|
|
324
|
+
passwordHash,
|
|
325
|
+
isAnonymous: false
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!updatedUser) {
|
|
329
|
+
throw ApiError.notFound("User not found");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Generate new tokens with updated identity
|
|
333
|
+
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
334
|
+
user.id,
|
|
335
|
+
c.req.header("user-agent") || "unknown",
|
|
336
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return c.json(buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken));
|
|
340
|
+
});
|
|
341
|
+
}
|
package/src/auth/slack-oauth.ts
CHANGED
|
@@ -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 Slack OAuth Provider integration (OAuth 2.0 / "Sign in with Slack").
|
|
@@ -27,13 +28,13 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
if (!tokenResponse.ok) {
|
|
30
|
-
|
|
31
|
+
logger.error("Failed to get Slack access token", { detail: await tokenResponse.text() });
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const tokenData = await tokenResponse.json() as { ok: boolean; access_token?: string; error?: string };
|
|
35
36
|
if (!tokenData.ok || !tokenData.access_token) {
|
|
36
|
-
|
|
37
|
+
logger.error("Slack token exchange failed", { detail: tokenData.error });
|
|
37
38
|
return null;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -42,7 +43,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
|
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
if (!profileResponse.ok) {
|
|
45
|
-
|
|
46
|
+
logger.error("Failed to get Slack user info", { detail: await profileResponse.text() });
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -52,7 +53,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
if (!p.ok || !p.email) {
|
|
55
|
-
|
|
56
|
+
logger.error("Slack user has no email");
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -63,7 +64,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
|
|
|
63
64
|
photoUrl: p.picture || null
|
|
64
65
|
};
|
|
65
66
|
} catch (error) {
|
|
66
|
-
|
|
67
|
+
logger.error("Slack OAuth error", { error: error });
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -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 Spotify OAuth Provider integration.
|
|
@@ -30,7 +31,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
|
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
if (!tokenResponse.ok) {
|
|
33
|
-
|
|
34
|
+
logger.error("Failed to get Spotify access token", { detail: await tokenResponse.text() });
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -41,7 +42,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
if (!profileResponse.ok) {
|
|
44
|
-
|
|
45
|
+
logger.error("Failed to get Spotify user info", { detail: await profileResponse.text() });
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -50,7 +51,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
|
|
|
50
51
|
email?: string; images?: Array<{ url: string }>;
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
if (!p.email) {
|
|
54
|
+
if (!p.email) { logger.error("Spotify user has no email"); return null; }
|
|
54
55
|
|
|
55
56
|
return {
|
|
56
57
|
providerId: p.id,
|
|
@@ -59,7 +60,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
|
|
|
59
60
|
photoUrl: p.images?.[0]?.url || null
|
|
60
61
|
};
|
|
61
62
|
} catch (error) {
|
|
62
|
-
|
|
63
|
+
logger.error("Spotify OAuth error", { error: error });
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -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 Twitter/X OAuth 2.0 Provider integration.
|
|
@@ -47,7 +48,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
if (!tokenResponse.ok) {
|
|
50
|
-
|
|
51
|
+
logger.error("Failed to get Twitter access token", { detail: await tokenResponse.text() });
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -63,7 +64,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
63
64
|
);
|
|
64
65
|
|
|
65
66
|
if (!profileResponse.ok) {
|
|
66
|
-
|
|
67
|
+
logger.error("Failed to get Twitter user info", { detail: await profileResponse.text() });
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -112,7 +113,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
112
113
|
photoUrl: profileData.profile_image_url?.replace("_normal", "_400x400") || null
|
|
113
114
|
};
|
|
114
115
|
} catch (error) {
|
|
115
|
-
|
|
116
|
+
logger.error("Twitter OAuth error", { error: error });
|
|
116
117
|
return null;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -2,6 +2,7 @@ import { EntityCollection } from "@rebasepro/types";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
|
+
import { logger } from "../utils/logger";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Asynchronously load collection files from a directory for backend initialization
|
|
@@ -10,7 +11,7 @@ export async function loadCollectionsFromDirectory(directory: string): Promise<E
|
|
|
10
11
|
const collections: EntityCollection[] = [];
|
|
11
12
|
try {
|
|
12
13
|
if (!fs.existsSync(directory)) {
|
|
13
|
-
|
|
14
|
+
logger.warn(`[loadCollectionsFromDirectory] Collections directory not found: ${directory}`);
|
|
14
15
|
return collections;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -34,16 +35,16 @@ export async function loadCollectionsFromDirectory(directory: string): Promise<E
|
|
|
34
35
|
if (module && module.default) {
|
|
35
36
|
collections.push(module.default);
|
|
36
37
|
} else {
|
|
37
|
-
|
|
38
|
+
logger.warn(`[loadCollectionsFromDirectory] File ${file} does not have a default export. Skipping.`);
|
|
38
39
|
}
|
|
39
40
|
} catch (err: unknown) {
|
|
40
41
|
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
-
|
|
42
|
+
logger.error(`[loadCollectionsFromDirectory] Failed to load collection from ${file}: ${message}`);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
} catch (err) {
|
|
46
|
-
|
|
47
|
+
logger.error(`[loadCollectionsFromDirectory] Error reading collections directory: ${err}`);
|
|
47
48
|
}
|
|
48
49
|
return collections;
|
|
49
50
|
}
|
|
@@ -44,7 +44,7 @@ describe("CronScheduler", () => {
|
|
|
44
44
|
describe("validateCronExpression", () => {
|
|
45
45
|
it.each([
|
|
46
46
|
"0 * * * *", "*/5 * * * *", "0 0 1 * *", "30 2 * * 1",
|
|
47
|
-
"0 0 * * 0", "0,15,30,45 * * * *", "0 0 1-15 * *"
|
|
47
|
+
"0 0 * * 0", "0,15,30,45 * * * *", "0 0 1-15 * *"
|
|
48
48
|
])("accepts valid expression: %s", (expr) => {
|
|
49
49
|
expect(validateCronExpression(expr)).toEqual({ valid: true });
|
|
50
50
|
});
|
|
@@ -106,7 +106,9 @@ describe("CronScheduler", () => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it("preserves definition metadata", () => {
|
|
109
|
-
scheduler.registerJobs([makeJob("meta", { name: "My Job",
|
|
109
|
+
scheduler.registerJobs([makeJob("meta", { name: "My Job",
|
|
110
|
+
description: "Desc",
|
|
111
|
+
schedule: "30 2 * * 1" })]);
|
|
110
112
|
const job = scheduler.getJob("meta")!;
|
|
111
113
|
expect(job.name).toBe("My Job");
|
|
112
114
|
expect(job.description).toBe("Desc");
|
|
@@ -262,7 +264,8 @@ describe("CronScheduler", () => {
|
|
|
262
264
|
// Try second trigger while first is running
|
|
263
265
|
const second = await scheduler.triggerJob("slow");
|
|
264
266
|
|
|
265
|
-
expect(second!.result).toEqual({ skipped: true,
|
|
267
|
+
expect(second!.result).toEqual({ skipped: true,
|
|
268
|
+
reason: "already_executing" });
|
|
266
269
|
expect(second!.logs).toContain("Skipped: job is already running");
|
|
267
270
|
|
|
268
271
|
// Let the first one finish
|
|
@@ -480,7 +483,7 @@ describe("CronScheduler", () => {
|
|
|
480
483
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
481
484
|
insertLog,
|
|
482
485
|
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
483
|
-
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
|
|
486
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
|
|
484
487
|
};
|
|
485
488
|
scheduler.setStore(mockStore);
|
|
486
489
|
scheduler.registerJobs([makeJob("persisted")]);
|
|
@@ -494,7 +497,7 @@ describe("CronScheduler", () => {
|
|
|
494
497
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
495
498
|
insertLog: jest.fn<() => Promise<void>>().mockRejectedValue(new Error("DB down")),
|
|
496
499
|
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
497
|
-
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
|
|
500
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
|
|
498
501
|
};
|
|
499
502
|
scheduler.setStore(mockStore);
|
|
500
503
|
scheduler.registerJobs([makeJob("resilient")]);
|
|
@@ -505,12 +508,14 @@ describe("CronScheduler", () => {
|
|
|
505
508
|
|
|
506
509
|
it("seeds counters from store on start", async () => {
|
|
507
510
|
const stats = new Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>();
|
|
508
|
-
stats.set("seeded", { totalRuns: 42,
|
|
511
|
+
stats.set("seeded", { totalRuns: 42,
|
|
512
|
+
totalFailures: 3,
|
|
513
|
+
lastRunAt: "2026-01-01T00:00:00Z" });
|
|
509
514
|
const mockStore = {
|
|
510
515
|
ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
511
516
|
insertLog: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
512
517
|
fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
|
|
513
|
-
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats)
|
|
518
|
+
fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats)
|
|
514
519
|
};
|
|
515
520
|
scheduler.setStore(mockStore);
|
|
516
521
|
scheduler.registerJobs([makeJob("seeded")]);
|
|
@@ -527,11 +532,18 @@ describe("CronScheduler", () => {
|
|
|
527
532
|
|
|
528
533
|
describe("status shape", () => {
|
|
529
534
|
it("returns all expected fields", () => {
|
|
530
|
-
scheduler.registerJobs([makeJob("shape", { name: "Shape Test",
|
|
535
|
+
scheduler.registerJobs([makeJob("shape", { name: "Shape Test",
|
|
536
|
+
description: "Desc",
|
|
537
|
+
schedule: "15 3 * * *" })]);
|
|
531
538
|
expect(scheduler.getJob("shape")).toMatchObject({
|
|
532
|
-
id: "shape",
|
|
533
|
-
|
|
534
|
-
|
|
539
|
+
id: "shape",
|
|
540
|
+
name: "Shape Test",
|
|
541
|
+
description: "Desc",
|
|
542
|
+
schedule: "15 3 * * *",
|
|
543
|
+
enabled: true,
|
|
544
|
+
state: "idle",
|
|
545
|
+
totalRuns: 0,
|
|
546
|
+
totalFailures: 0
|
|
535
547
|
});
|
|
536
548
|
});
|
|
537
549
|
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
CronJobRunState,
|
|
6
6
|
CronJobContext
|
|
7
7
|
} from "@rebasepro/types";
|
|
8
|
-
import type { RebaseClient } from "@rebasepro/
|
|
8
|
+
import type { RebaseClient } from "@rebasepro/types";
|
|
9
9
|
import type { LoadedCronJob } from "./cron-loader";
|
|
10
10
|
import type { CronStore } from "./cron-store";
|
|
11
11
|
import { logger } from "../utils/logger.js";
|
|
@@ -62,33 +62,38 @@ function expandCronField(field: string, min: number, max: number): number[] {
|
|
|
62
62
|
*/
|
|
63
63
|
export function validateCronExpression(schedule: string): { valid: true } | { valid: false; reason: string } {
|
|
64
64
|
if (!schedule || typeof schedule !== "string") {
|
|
65
|
-
return { valid: false,
|
|
65
|
+
return { valid: false,
|
|
66
|
+
reason: "Schedule must be a non-empty string" };
|
|
66
67
|
}
|
|
67
68
|
const parts = schedule.trim().split(/\s+/);
|
|
68
69
|
if (parts.length !== 5) {
|
|
69
|
-
return { valid: false,
|
|
70
|
+
return { valid: false,
|
|
71
|
+
reason: `Expected 5 fields, got ${parts.length}` };
|
|
70
72
|
}
|
|
71
73
|
const fieldRanges: [string, number, number][] = [
|
|
72
74
|
["minute", 0, 59],
|
|
73
75
|
["hour", 0, 23],
|
|
74
76
|
["day of month", 1, 31],
|
|
75
77
|
["month", 1, 12],
|
|
76
|
-
["day of week", 0, 6]
|
|
78
|
+
["day of week", 0, 6]
|
|
77
79
|
];
|
|
78
80
|
for (let i = 0; i < 5; i++) {
|
|
79
81
|
const [name, min, max] = fieldRanges[i];
|
|
80
82
|
try {
|
|
81
83
|
const values = expandCronField(parts[i], min, max);
|
|
82
84
|
if (values.length === 0) {
|
|
83
|
-
return { valid: false,
|
|
85
|
+
return { valid: false,
|
|
86
|
+
reason: `${name} field "${parts[i]}" produces no values` };
|
|
84
87
|
}
|
|
85
88
|
for (const v of values) {
|
|
86
89
|
if (v < min || v > max) {
|
|
87
|
-
return { valid: false,
|
|
90
|
+
return { valid: false,
|
|
91
|
+
reason: `${name} field value ${v} out of range [${min}–${max}]` };
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
} catch (err) {
|
|
91
|
-
return { valid: false,
|
|
95
|
+
return { valid: false,
|
|
96
|
+
reason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
return { valid: true };
|
|
@@ -362,7 +367,8 @@ export class CronScheduler {
|
|
|
362
367
|
finishedAt: new Date().toISOString(),
|
|
363
368
|
durationMs: 0,
|
|
364
369
|
success: true,
|
|
365
|
-
result: { skipped: true,
|
|
370
|
+
result: { skipped: true,
|
|
371
|
+
reason: "already_executing" },
|
|
366
372
|
logs: ["Skipped: job is already running"],
|
|
367
373
|
manual: true
|
|
368
374
|
};
|
package/src/cron/cron-store.ts
CHANGED
|
@@ -158,7 +158,7 @@ function rowToLogEntry(row: Record<string, unknown>): CronJobLogEntry {
|
|
|
158
158
|
success: row.success as boolean,
|
|
159
159
|
error: (row.error as string) ?? undefined,
|
|
160
160
|
result: row.result ?? undefined,
|
|
161
|
-
logs: Array.isArray(row.logs) ? row.logs : (row.logs ? JSON.parse(row.logs as string) : []),
|
|
161
|
+
logs: Array.isArray(row.logs) ? row.logs : (row.logs ? (() => { try { return JSON.parse(row.logs as string); } catch { return []; } })() : []),
|
|
162
162
|
manual: row.manual as boolean
|
|
163
163
|
};
|
|
164
164
|
}
|