@storyshelf/app 0.6.1 → 0.7.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 +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credentials.mjs +3 -2
- package/dist/credentials.mjs.map +1 -1
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +202 -124
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +109 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +2 -2
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -2
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -6
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
4
|
import { getStore } from "../store.mjs";
|
|
6
5
|
import { flash } from "./flash.mjs";
|
|
6
|
+
import { notificationChannelModel } from "./scoped-models.mjs";
|
|
7
7
|
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
|
+
import { createGitChannel } from "./settings-git-channel.mjs";
|
|
8
9
|
import { NotificationChannelModel, NotificationSubscriptionModel } from "@storyshelf/core/models";
|
|
9
10
|
//#region src/routers/settings-notifications.ts
|
|
10
|
-
/** Notifications settings (channels plus my preferences). */
|
|
11
|
+
/** Notifications settings (message and git channels plus my preferences). */
|
|
11
12
|
function registerNotificationSettings(app) {
|
|
12
13
|
app.get("/projects/:slug/settings/notifications", async (c) => c.html(await renderSettingsPage(c, "notifications")));
|
|
13
14
|
app.post("/projects/:slug/settings/notifications", handleCreateChannel);
|
|
@@ -28,6 +29,11 @@ function parseChecks(form, name) {
|
|
|
28
29
|
async function handleCreateChannel(c) {
|
|
29
30
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
30
31
|
const form = await c.req.formData();
|
|
32
|
+
const gitProvider = asString(form.get("gitProvider"));
|
|
33
|
+
return gitProvider ? await createGitChannel(c, project, gitProvider, form) : await createMessageChannel(c, project, form);
|
|
34
|
+
}
|
|
35
|
+
/** The message-channel form: a provider and a target (email address or webhook URL). */
|
|
36
|
+
async function createMessageChannel(c, project, form) {
|
|
31
37
|
const provider = asString(form.get("provider"));
|
|
32
38
|
const target = asString(form.get("target"));
|
|
33
39
|
if (provider && target) return await createChannelRecord(c, project, provider, target, form);
|
|
@@ -44,11 +50,10 @@ async function targetError(c, message) {
|
|
|
44
50
|
return c.html(await renderSettingsPage(c, "notifications", { errors: { target: message } }) ?? "", 400);
|
|
45
51
|
}
|
|
46
52
|
async function createChannelRecord(c, project, provider, target, form) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await persistChannel(project, provider, target, parsed.data, form);
|
|
53
|
+
const config = channelConfig(provider, target, form);
|
|
54
|
+
const checked = NotificationChannelModel.validateInput(getStore().notifiers, provider, config);
|
|
55
|
+
if (!checked.ok) return checked.field === "provider" ? await providerError(c, checked.message) : await targetError(c, checked.message);
|
|
56
|
+
await persistChannel(project, provider, target, config, form);
|
|
52
57
|
flash(c, "Notification channel added");
|
|
53
58
|
return c.html(await renderSettingsPage(c, "notifications") ?? "", 201);
|
|
54
59
|
}
|
|
@@ -63,7 +68,7 @@ function channelConfig(provider, target, form) {
|
|
|
63
68
|
}
|
|
64
69
|
/** Persist a validated channel (webhook URLs land in the secret column). */
|
|
65
70
|
async function persistChannel(project, provider, target, config, form) {
|
|
66
|
-
await
|
|
71
|
+
await notificationChannelModel().create({
|
|
67
72
|
projectId: project.id,
|
|
68
73
|
provider,
|
|
69
74
|
config,
|
|
@@ -73,11 +78,11 @@ async function persistChannel(project, provider, target, config, form) {
|
|
|
73
78
|
}
|
|
74
79
|
async function handleDeleteChannel(c) {
|
|
75
80
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
76
|
-
const model =
|
|
81
|
+
const model = notificationChannelModel();
|
|
77
82
|
const channel = await model.get(c.req.param("channelId") ?? "");
|
|
78
83
|
if (!channel || channel.projectId !== project.id) return c.html(await renderSettingsPage(c, "notifications", { globalError: "Channel not found" }) ?? "", 404);
|
|
79
84
|
await model.remove(channel.id);
|
|
80
|
-
flash(c, "
|
|
85
|
+
flash(c, "Channel removed");
|
|
81
86
|
return c.html(await renderSettingsPage(c, "notifications") ?? "");
|
|
82
87
|
}
|
|
83
88
|
async function handleSaveMine(c) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/routers/settings-notifications.ts"],"sourcesContent":["import { NotificationChannelModel, NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport {
|
|
1
|
+
{"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/routers/settings-notifications.ts"],"sourcesContent":["import { NotificationChannelModel, NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notificationChannelModel } from \"./scoped-models.ts\";\nimport { createGitChannel } from \"./settings-git-channel.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Notifications settings (message and git channels plus my preferences). */\nexport function registerNotificationSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/notifications\", async (c) =>\n c.html(await renderSettingsPage(c, \"notifications\")),\n );\n app.post(\"/projects/:slug/settings/notifications\", handleCreateChannel);\n app.post(\"/projects/:slug/settings/notifications/:channelId/delete\", handleDeleteChannel);\n app.post(\"/projects/:slug/settings/notifications/me\", handleSaveMine);\n}\n\n/** Parse the comma-separated events field. */\nfunction parseEvents(form: FormData): string[] | undefined {\n const raw = asString(form.get(\"events\"));\n if (!raw) {\n return undefined;\n }\n const events = raw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n return events.length > 0 ? events : undefined;\n}\n\n/** Parse repeated checkbox values (topics). */\nfunction parseChecks(form: FormData, name: string): string[] {\n return form\n .getAll(name)\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n}\n\nasync function handleCreateChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const gitProvider = asString(form.get(\"gitProvider\"));\n return gitProvider\n ? await createGitChannel(c, project, gitProvider, form)\n : await createMessageChannel(c, project, form);\n}\n\n/** The message-channel form: a provider and a target (email address or webhook URL). */\nasync function createMessageChannel(\n c: Context,\n project: Project,\n form: FormData,\n): Promise<Response> {\n const provider = asString(form.get(\"provider\"));\n const target = asString(form.get(\"target\"));\n if (provider && target) {\n return await createChannelRecord(c, project, provider, target, form);\n }\n const missing = provider ? \"target\" : \"provider\";\n const message = provider ? \"Enter a target\" : \"Choose a provider\";\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { [missing]: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a provider-field error. */\nasync function providerError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { provider: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a target-field error. */\nasync function targetError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { target: message } })) ?? \"\",\n 400,\n );\n}\n\nasync function createChannelRecord(\n c: Context,\n project: Project,\n provider: string,\n target: string,\n form: FormData,\n): Promise<Response> {\n const config = channelConfig(provider, target, form);\n const checked = NotificationChannelModel.validateInput(getStore().notifiers, provider, config);\n if (!checked.ok) {\n return checked.field === \"provider\"\n ? await providerError(c, checked.message)\n : await targetError(c, checked.message);\n }\n await persistChannel(project, provider, target, config, form);\n flash(c, \"Notification channel added\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\", 201);\n}\n\n/** Build the provider config from the form (address vs secret per kind). */\nfunction channelConfig(provider: string, target: string, form: FormData): Record<string, unknown> {\n const style = asString(form.get(\"style\")) ?? \"compact\";\n if (provider === \"email\") {\n return { to: target, style };\n }\n return { style };\n}\n\n/** Persist a validated channel (webhook URLs land in the secret column). */\nasync function persistChannel(\n project: Project,\n provider: string,\n target: string,\n config: unknown,\n form: FormData,\n): Promise<void> {\n await notificationChannelModel().create({\n projectId: project.id,\n provider,\n config: config as Record<string, unknown>,\n secret: provider === \"email\" ? undefined : target,\n events: parseEvents(form),\n });\n}\n\nasync function handleDeleteChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const model = notificationChannelModel();\n const channel = await model.get(c.req.param(\"channelId\") ?? \"\");\n if (!channel || channel.projectId !== project.id) {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Channel not found\",\n })) ?? \"\",\n 404,\n );\n }\n await model.remove(channel.id);\n flash(c, \"Channel removed\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n}\n\nasync function handleSaveMine(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const user = getStore().user;\n if (user) {\n await saveSubscription(project, user.id, await c.req.formData());\n flash(c, \"Notification preferences saved\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n }\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Sign in to save notification preferences\",\n })) ?? \"\",\n 401,\n );\n}\n\n/** Persist the viewer's own subscription (unchecked means opt out). */\nasync function saveSubscription(project: Project, userId: string, form: FormData): Promise<void> {\n const model = new NotificationSubscriptionModel(getStore().db);\n if (form.get(\"enabled\") === \"on\") {\n await model.upsert(project.id, userId, { events: parseChecks(form, \"events\"), via: [\"email\"] });\n return;\n }\n await model.remove(project.id, userId);\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,6 @@ import { hxRedirect } from "./htmx.mjs";
|
|
|
6
6
|
import { flash } from "./flash.mjs";
|
|
7
7
|
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
8
|
import { TokenModel } from "@storyshelf/core/models";
|
|
9
|
-
import { randomToken } from "@storyshelf/core/utils";
|
|
10
9
|
//#region src/routers/settings-tokens.ts
|
|
11
10
|
/** CI token settings (create with one-time display, delete). */
|
|
12
11
|
function registerTokenSettings(app) {
|
|
@@ -19,14 +18,12 @@ async function handleCreateToken(c) {
|
|
|
19
18
|
const form = await c.req.formData();
|
|
20
19
|
const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
|
|
21
20
|
if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
|
|
22
|
-
const
|
|
23
|
-
await new TokenModel(getStore().db).create(project.id, {
|
|
21
|
+
const { value } = await new TokenModel(getStore().db).issue(project.id, {
|
|
24
22
|
name: tokenName,
|
|
25
|
-
hash: token.hash,
|
|
26
23
|
userId: getStore().user?.id ?? null
|
|
27
24
|
});
|
|
28
25
|
flash(c, "Token created — copy it now, it is shown once");
|
|
29
|
-
return c.html(await renderSettingsPage(c, "tokens", { secret:
|
|
26
|
+
return c.html(await renderSettingsPage(c, "tokens", { secret: value }) ?? "");
|
|
30
27
|
}
|
|
31
28
|
async function handleDeleteToken(c) {
|
|
32
29
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/routers/settings-tokens.ts"],"sourcesContent":["import { TokenModel } from \"@storyshelf/core/models\";\nimport
|
|
1
|
+
{"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/routers/settings-tokens.ts"],"sourcesContent":["import { TokenModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** CI token settings (create with one-time display, delete). */\nexport function registerTokenSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/tokens\", async (c) =>\n c.html(await renderSettingsPage(c, \"tokens\")),\n );\n app.post(\"/projects/:slug/settings/tokens\", handleCreateToken);\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", handleDeleteToken);\n}\n\nasync function handleCreateToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html(\n (await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\",\n 400,\n );\n }\n const { value } = await new TokenModel(getStore().db).issue(project.id, {\n name: tokenName,\n userId: getStore().user?.id ?? null,\n });\n flash(c, \"Token created — copy it now, it is shown once\");\n return c.html((await renderSettingsPage(c, \"tokens\", { secret: value })) ?? \"\");\n}\n\nasync function handleDeleteToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\") ?? \"\");\n flash(c, \"Token deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,sBAAsB,KAAwB;CAC5D,IAAI,IAAI,mCAAmC,OAAO,MAChD,EAAE,KAAK,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAC9C;CACA,IAAI,KAAK,mCAAmC,iBAAiB;CAC7D,IAAI,KAAK,mDAAmD,iBAAiB;AAC/E;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC,KAAK,SAAS,KAAK,IAAI,MAAM,CAAC;CAC9E,IAAI,CAAC,WACH,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,mBAAmB,CAAC,KAAM,IAChF,GACF;CAEF,MAAM,EAAE,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,QAAQ,IAAI;EACtE,MAAM;EACN,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;CACjC,CAAC;CACD,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,UAAU,EAAE,QAAQ,MAAM,CAAC,KAAM,EAAE;AAChF;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,MAAM,SAAS,KAAK,EAAE;CACvE,MAAM,GAAG,eAAe;CACxB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import {
|
|
5
|
-
import { getStore } from "../store.mjs";
|
|
4
|
+
import { notFound } from "./helpers.mjs";
|
|
6
5
|
import { hxRedirect } from "./htmx.mjs";
|
|
7
6
|
import { flash } from "./flash.mjs";
|
|
8
|
-
import {
|
|
7
|
+
import { webhookModel } from "./scoped-models.mjs";
|
|
9
8
|
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
10
9
|
import { WebhookModel } from "@storyshelf/core/models";
|
|
11
|
-
import { randomToken } from "@storyshelf/core/utils";
|
|
12
10
|
//#region src/routers/settings-webhooks.ts
|
|
13
11
|
/** Webhook settings (create with one-time secret display, delete). */
|
|
14
12
|
function registerWebhookSettings(app) {
|
|
@@ -16,18 +14,6 @@ function registerWebhookSettings(app) {
|
|
|
16
14
|
app.post("/projects/:slug/settings/webhooks", handleCreateWebhook);
|
|
17
15
|
app.post("/projects/:slug/settings/webhooks/:webhookId/delete", handleDeleteWebhook);
|
|
18
16
|
}
|
|
19
|
-
/** Validate a webhook URL, returning the error message or null when valid. */
|
|
20
|
-
function webhookUrlError(url) {
|
|
21
|
-
if (!url) return "A valid URL is required";
|
|
22
|
-
let protocol;
|
|
23
|
-
try {
|
|
24
|
-
protocol = new URL(url).protocol;
|
|
25
|
-
} catch {
|
|
26
|
-
return "Enter a valid URL";
|
|
27
|
-
}
|
|
28
|
-
if (protocol !== "https:" && protocol !== "http:") return "URL must use http or https";
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
17
|
/** Parse the comma-separated events field. */
|
|
32
18
|
function parseWebhookEvents(form) {
|
|
33
19
|
const eventsRaw = asString(form.get("events"));
|
|
@@ -38,26 +24,23 @@ async function handleCreateWebhook(c) {
|
|
|
38
24
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
39
25
|
const form = await c.req.formData();
|
|
40
26
|
const url = asString(form.get("url"));
|
|
41
|
-
const urlError =
|
|
27
|
+
const urlError = WebhookModel.urlError(url);
|
|
42
28
|
if (urlError || !url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: urlError ?? "Invalid URL" } }) ?? "", 400);
|
|
43
29
|
return await createWebhookRecord(c, project, url, parseWebhookEvents(form));
|
|
44
30
|
}
|
|
45
31
|
async function createWebhookRecord(c, project, url, events) {
|
|
46
|
-
const
|
|
47
|
-
const secret = randomToken("whsec_").value;
|
|
48
|
-
await webhookModel.create(project.id, {
|
|
32
|
+
const { secret } = await webhookModel().issue(project.id, {
|
|
49
33
|
url,
|
|
50
|
-
events
|
|
51
|
-
secret
|
|
34
|
+
events
|
|
52
35
|
});
|
|
53
36
|
flash(c, "Webhook created — copy the signing secret now");
|
|
54
37
|
return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
|
|
55
38
|
}
|
|
56
39
|
async function handleDeleteWebhook(c) {
|
|
57
40
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
58
|
-
const webhook = await
|
|
41
|
+
const webhook = await webhookModel().get(project.id, c.req.param("webhookId") ?? "");
|
|
59
42
|
if (!webhook) notFound("Webhook not found");
|
|
60
|
-
await
|
|
43
|
+
await webhookModel().remove(project.id, webhook.id);
|
|
61
44
|
flash(c, "Webhook deleted");
|
|
62
45
|
return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
|
|
63
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/routers/settings-webhooks.ts"],"sourcesContent":["import { WebhookModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport
|
|
1
|
+
{"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/routers/settings-webhooks.ts"],"sourcesContent":["import { WebhookModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { webhookModel } from \"./scoped-models.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Webhook settings (create with one-time secret display, delete). */\nexport function registerWebhookSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/webhooks\", async (c) =>\n c.html(await renderSettingsPage(c, \"webhooks\")),\n );\n app.post(\"/projects/:slug/settings/webhooks\", handleCreateWebhook);\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", handleDeleteWebhook);\n}\n\n/** Parse the comma-separated events field. */\nfunction parseWebhookEvents(form: FormData): string[] | undefined {\n const eventsRaw = asString(form.get(\"events\"));\n if (!eventsRaw) {\n return undefined;\n }\n return eventsRaw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n}\n\nasync function handleCreateWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n const urlError = WebhookModel.urlError(url);\n if (urlError || !url) {\n return c.html(\n (await renderSettingsPage(c, \"webhooks\", { errors: { url: urlError ?? \"Invalid URL\" } })) ??\n \"\",\n 400,\n );\n }\n return await createWebhookRecord(c, project, url, parseWebhookEvents(form));\n}\n\nasync function createWebhookRecord(\n c: Context,\n project: Project,\n url: string,\n events: string[] | undefined,\n): Promise<Response> {\n const { secret } = await webhookModel().issue(project.id, { url, events });\n flash(c, \"Webhook created — copy the signing secret now\");\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n}\n\nasync function handleDeleteWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const webhook = await webhookModel().get(project.id, c.req.param(\"webhookId\") ?? \"\");\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await webhookModel().remove(project.id, webhook.id);\n flash(c, \"Webhook deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,qCAAqC,OAAO,MAClD,EAAE,KAAK,MAAM,mBAAmB,GAAG,UAAU,CAAC,CAChD;CACA,IAAI,KAAK,qCAAqC,mBAAmB;CACjE,IAAI,KAAK,uDAAuD,mBAAmB;AACrF;;AAGA,SAAS,mBAAmB,MAAsC;CAChE,MAAM,YAAY,SAAS,KAAK,IAAI,QAAQ,CAAC;CAC7C,IAAI,CAAC,WACH;CAEF,OAAO,UACJ,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;CACpC,MAAM,WAAW,aAAa,SAAS,GAAG;CAC1C,IAAI,YAAY,CAAC,KACf,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,YAAY,EAAE,QAAQ,EAAE,KAAK,YAAY,cAAc,EAAE,CAAC,KACrF,IACF,GACF;CAEF,OAAO,MAAM,oBAAoB,GAAG,SAAS,KAAK,mBAAmB,IAAI,CAAC;AAC5E;AAEA,eAAe,oBACb,GACA,SACA,KACA,QACmB;CACnB,MAAM,EAAE,WAAW,MAAM,aAAa,CAAC,CAAC,MAAM,QAAQ,IAAI;EAAE;EAAK;CAAO,CAAC;CACzE,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,YAAY,EAAE,OAAO,CAAC,KAAM,IAAI,GAAG;AAChF;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,UAAU,MAAM,aAAa,CAAC,CAAC,IAAI,QAAQ,IAAI,EAAE,IAAI,MAAM,WAAW,KAAK,EAAE;CACnF,IAAI,CAAC,SACH,SAAS,mBAAmB;CAE9B,MAAM,aAAa,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;CAClD,MAAM,GAAG,iBAAiB;CAC1B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,mBAAmB;AACpE"}
|
|
@@ -3,9 +3,12 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
5
|
import { getStore } from "../store.mjs";
|
|
6
|
+
import { notificationsPaused } from "../notifications-pause.mjs";
|
|
6
7
|
import { notFound } from "./helpers.mjs";
|
|
8
|
+
import { runnerTargets } from "../targets.mjs";
|
|
9
|
+
import { isSiteAdmin } from "./insights.handlers.mjs";
|
|
7
10
|
import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
|
|
8
|
-
import { LabelModel, MemberModel, NotificationChannelModel, NotificationSubscriptionModel, ProjectGroupMappingModel, ProjectModel,
|
|
11
|
+
import { DesignCredentialModel, DesignModel, LabelModel, MemberModel, NotificationChannelModel, NotificationSubscriptionModel, PackageUsageModel, ProjectGroupMappingModel, ProjectModel, TokenModel, WebhookModel, credentialDaysLeft } from "@storyshelf/core/models";
|
|
9
12
|
//#region src/routers/settings.handlers.ts
|
|
10
13
|
/** Tokens without hashes for the settings UI. */
|
|
11
14
|
async function loadTokenSummaries(db, projectId) {
|
|
@@ -19,17 +22,6 @@ async function loadWebhookSummaries(db, projectId) {
|
|
|
19
22
|
events: WebhookModel.eventsOf(webhook)
|
|
20
23
|
}));
|
|
21
24
|
}
|
|
22
|
-
/** Status-config summaries for the settings UI. */
|
|
23
|
-
async function loadStatusConfigSummaries(db, secret, projectId) {
|
|
24
|
-
return (await new StatusConfigModel(db, void 0, secret).list(projectId)).map((row) => ({
|
|
25
|
-
id: row.id,
|
|
26
|
-
provider: row.provider,
|
|
27
|
-
config: JSON.parse(row.config),
|
|
28
|
-
hasToken: row.tokenEncrypted.length > 0,
|
|
29
|
-
createdAt: row.createdAt,
|
|
30
|
-
updatedAt: row.updatedAt
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
25
|
/** True when the user may administer the project. */
|
|
34
26
|
function isProjectAdmin(authEnabled, user, members) {
|
|
35
27
|
return !authEnabled || user?.role === "admin" || members.some((member) => member.userId === user?.id && member.role === "admin");
|
|
@@ -54,15 +46,22 @@ async function loadMembersSection(db, projectId) {
|
|
|
54
46
|
async function loadGroupMappings(db, projectId) {
|
|
55
47
|
return await new ProjectGroupMappingModel(db).list(projectId);
|
|
56
48
|
}
|
|
49
|
+
/** Delivery kind of a stored channel's provider (unwired providers read as messages). */
|
|
50
|
+
function deliveryOf(notifiers, provider) {
|
|
51
|
+
return NotificationChannelModel.isStateKind(notifiers, provider) ? "state" : "message";
|
|
52
|
+
}
|
|
57
53
|
/** Channel summaries for the settings UI. */
|
|
58
|
-
async function loadNotificationChannels(db, secret, projectId) {
|
|
54
|
+
async function loadNotificationChannels(db, secret, projectId, notifiers) {
|
|
59
55
|
return (await new NotificationChannelModel(db, void 0, secret).list(projectId)).map((row) => ({
|
|
60
56
|
id: row.id,
|
|
61
57
|
provider: row.provider,
|
|
58
|
+
delivery: deliveryOf(notifiers, row.provider),
|
|
62
59
|
config: NotificationChannelModel.configOf(row),
|
|
63
60
|
hasSecret: row.secretEncrypted !== null,
|
|
64
61
|
events: NotificationChannelModel.eventsOf(row),
|
|
65
|
-
enabled: row.enabled
|
|
62
|
+
enabled: row.enabled,
|
|
63
|
+
lastError: row.lastError,
|
|
64
|
+
lastErrorAt: row.lastErrorAt
|
|
66
65
|
}));
|
|
67
66
|
}
|
|
68
67
|
/** The viewer's own subscription for the settings UI (null when opted out). */
|
|
@@ -78,38 +77,106 @@ async function loadMySubscription(db, projectId, userId) {
|
|
|
78
77
|
}
|
|
79
78
|
/** Notification channels, providers, and my subscription for the UI. */
|
|
80
79
|
async function loadNotificationSection(db, secret, projectId, userId, notifiers) {
|
|
81
|
-
const notificationChannels = await loadNotificationChannels(db, secret, projectId);
|
|
80
|
+
const notificationChannels = await loadNotificationChannels(db, secret, projectId, notifiers);
|
|
82
81
|
const mySubscription = await loadMySubscription(db, projectId, userId);
|
|
83
82
|
return {
|
|
84
83
|
notificationChannels,
|
|
85
|
-
notifyProviders: notifiers.map((candidate) =>
|
|
84
|
+
notifyProviders: notifiers.map((candidate) => ({
|
|
85
|
+
kind: candidate.metadata.kind,
|
|
86
|
+
name: candidate.metadata.name,
|
|
87
|
+
delivery: deliveryOf(notifiers, candidate.metadata.kind)
|
|
88
|
+
})),
|
|
86
89
|
mySubscription
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
|
-
/** Roster, tokens
|
|
90
|
-
async function loadInventorySection(db,
|
|
92
|
+
/** Roster, tokens and webhooks for the settings UI. */
|
|
93
|
+
async function loadInventorySection(db, projectId) {
|
|
91
94
|
const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);
|
|
92
95
|
return {
|
|
93
96
|
labelTypes,
|
|
94
97
|
members,
|
|
95
98
|
groupMappings,
|
|
96
99
|
tokens: await loadTokenSummaries(db, projectId),
|
|
97
|
-
webhooks: await loadWebhookSummaries(db, projectId)
|
|
98
|
-
statusConfigs: await loadStatusConfigSummaries(db, secret, projectId)
|
|
100
|
+
webhooks: await loadWebhookSummaries(db, projectId)
|
|
99
101
|
};
|
|
100
102
|
}
|
|
103
|
+
/** Package links plus the other projects that can be picked as upstream. */
|
|
104
|
+
async function loadLinksSection(db, project) {
|
|
105
|
+
const projects = await new ProjectModel(db).list();
|
|
106
|
+
const byId = new Map(projects.map((candidate) => [candidate.id, candidate]));
|
|
107
|
+
return {
|
|
108
|
+
links: (await new PackageUsageModel(db).listLinks(project.id)).flatMap((row) => {
|
|
109
|
+
const upstream = byId.get(row.upstreamId);
|
|
110
|
+
return upstream ? [{
|
|
111
|
+
packageName: row.packageName,
|
|
112
|
+
upstreamSlug: upstream.slug,
|
|
113
|
+
upstreamName: upstream.name
|
|
114
|
+
}] : [];
|
|
115
|
+
}),
|
|
116
|
+
linkCandidates: projects.filter((candidate) => candidate.id !== project.id).map((candidate) => ({
|
|
117
|
+
slug: candidate.slug,
|
|
118
|
+
name: candidate.name
|
|
119
|
+
}))
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Design files, component mappings and per-provider credential health for the Design tab (never a token). */
|
|
123
|
+
async function loadDesignSection(db, secret, providers, projectId) {
|
|
124
|
+
const model = new DesignModel(db, providers);
|
|
125
|
+
const files = (await model.projectLinks(projectId)).map((link) => ({
|
|
126
|
+
id: link.id,
|
|
127
|
+
label: link.label ?? "Design file",
|
|
128
|
+
url: link.url
|
|
129
|
+
}));
|
|
130
|
+
const components = (await model.componentLinks(projectId)).map((link) => ({
|
|
131
|
+
id: link.id,
|
|
132
|
+
importPath: link.importPath ?? "",
|
|
133
|
+
url: link.url
|
|
134
|
+
}));
|
|
135
|
+
const credentials = new DesignCredentialModel(db, secret);
|
|
136
|
+
return {
|
|
137
|
+
files,
|
|
138
|
+
components,
|
|
139
|
+
providers: await Promise.all(providers.map(async (provider) => await providerSettings(credentials, provider, projectId)))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function providerSettings(credentials, provider, projectId) {
|
|
143
|
+
const { metadata } = provider;
|
|
144
|
+
const maxAgeDays = metadata.credentialMaxAgeDays;
|
|
145
|
+
const row = await credentials.info(projectId, metadata.kind);
|
|
146
|
+
return {
|
|
147
|
+
kind: metadata.kind,
|
|
148
|
+
name: metadata.name,
|
|
149
|
+
credentialLabel: metadata.credentialLabel,
|
|
150
|
+
credentialHelp: metadata.credentialHelp,
|
|
151
|
+
...metadata.credentialPlaceholder ? { credentialPlaceholder: metadata.credentialPlaceholder } : {},
|
|
152
|
+
urlHint: metadata.urlHint,
|
|
153
|
+
...maxAgeDays === void 0 ? {} : { maxAgeDays },
|
|
154
|
+
credential: row ? {
|
|
155
|
+
status: credentialState(row.status),
|
|
156
|
+
daysLeft: maxAgeDays === void 0 ? null : credentialDaysLeft(row.createdAt, maxAgeDays),
|
|
157
|
+
checkedAt: row.checkedAt
|
|
158
|
+
} : null
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** The stored status column as a known state (anything unrecognized reads as unknown). */
|
|
162
|
+
function credentialState(status) {
|
|
163
|
+
return status === "ok" || status === "unauthorized" || status === "forbidden" ? status : "unknown";
|
|
164
|
+
}
|
|
101
165
|
async function loadSettingsData(slug) {
|
|
102
166
|
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
103
167
|
if (!project) return null;
|
|
104
|
-
const { db, config, user, authEnabled,
|
|
105
|
-
const inventory = await loadInventorySection(db,
|
|
168
|
+
const { db, config, user, authEnabled, design: designProviders, notifiers } = getStore();
|
|
169
|
+
const inventory = await loadInventorySection(db, project.id);
|
|
106
170
|
const notifications = await loadNotificationSection(db, credentialKeys(config), project.id, user?.id, notifiers);
|
|
107
171
|
const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);
|
|
172
|
+
const linksSection = await loadLinksSection(db, project);
|
|
173
|
+
const design = await loadDesignSection(db, credentialKeys(config), designProviders, project.id);
|
|
108
174
|
return {
|
|
109
175
|
project,
|
|
110
176
|
...inventory,
|
|
111
177
|
...notifications,
|
|
112
|
-
|
|
178
|
+
...linksSection,
|
|
179
|
+
design,
|
|
113
180
|
isAdmin
|
|
114
181
|
};
|
|
115
182
|
}
|
|
@@ -121,6 +188,9 @@ async function renderSettingsPage(c, tab, formState) {
|
|
|
121
188
|
project: data.project,
|
|
122
189
|
activeTab: tab,
|
|
123
190
|
labelTypes: data.labelTypes,
|
|
191
|
+
links: data.links,
|
|
192
|
+
linkCandidates: data.linkCandidates,
|
|
193
|
+
design: data.design,
|
|
124
194
|
tokens: data.tokens,
|
|
125
195
|
members: data.members,
|
|
126
196
|
groupMappings: data.groupMappings,
|
|
@@ -128,11 +198,22 @@ async function renderSettingsPage(c, tab, formState) {
|
|
|
128
198
|
notificationChannels: data.notificationChannels,
|
|
129
199
|
notifyProviders: data.notifyProviders,
|
|
130
200
|
mySubscription: data.mySubscription,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
201
|
+
isAdmin: data.isAdmin,
|
|
202
|
+
notificationsPaused: data.notifyProviders.length > 0 && await notificationsPaused(),
|
|
203
|
+
ai: aiSettingsData(c),
|
|
204
|
+
targets: await runnerTargets()
|
|
134
205
|
}, formState);
|
|
135
206
|
}
|
|
207
|
+
/** AI availability and profile choices for the settings AI tab. */
|
|
208
|
+
function aiSettingsData(c) {
|
|
209
|
+
const { ai } = getStore();
|
|
210
|
+
return {
|
|
211
|
+
available: ai !== void 0,
|
|
212
|
+
profiles: ai?.profileNames() ?? [],
|
|
213
|
+
defaultProfile: ai?.defaultProfile() ?? "",
|
|
214
|
+
isSiteAdmin: isSiteAdmin(c)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
136
217
|
/** Find a project by slug or throw 404. */
|
|
137
218
|
async function findProject(slug) {
|
|
138
219
|
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.handlers.mjs","names":[],"sources":["../../src/routers/settings.handlers.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { MemberModel } from \"@storyshelf/core/models\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport { ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { StatusConfigModel } from \"@storyshelf/core/models\";\nimport { TokenModel } from \"@storyshelf/core/models\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { SecretInput } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport {\n renderProjectSettingsPage,\n type SettingsFormState,\n type SettingsTab,\n} from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type {\n SettingsMySubscription,\n SettingsNotificationChannel,\n} from \"../pages/settings-notifications.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { notFound } from \"./helpers.ts\";\n/** Aggregated data for rendering a settings tab. */\nexport interface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Tokens without hashes for the settings UI. */\nasync function loadTokenSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<Omit<Token, \"hash\">[]> {\n const tokensDb = await new TokenModel(db).list(projectId);\n return tokensDb.map(({ hash: _hash, ...rest }) => rest);\n}\n\n/** Webhook summaries for the settings UI. */\nasync function loadWebhookSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<SettingsWebhook[]> {\n const webhooksDb = await new WebhookModel(db).list(projectId);\n return webhooksDb.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n }));\n}\n\n/** Status-config summaries for the settings UI. */\nasync function loadStatusConfigSummaries(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<SettingsStatusConfig[]> {\n const statusConfigsDb = await new StatusConfigModel(db, undefined, secret).list(projectId);\n return statusConfigsDb.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: row.tokenEncrypted.length > 0,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\n/** True when the user may administer the project. */\nfunction isProjectAdmin(\n authEnabled: boolean,\n user: AuthUser | null,\n members: SettingsMember[],\n): boolean {\n return (\n !authEnabled ||\n user?.role === \"admin\" ||\n members.some((member) => member.userId === user?.id && member.role === \"admin\")\n );\n}\n\n/** Label types + member roster for the settings UI. */\nasync function loadProjectRoster(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{ labelTypes: LabelType[]; members: SettingsMember[] }> {\n const labelTypes = await new LabelModel(db).listTypes(projectId);\n const members = await new MemberModel(db).list(projectId);\n return { labelTypes, members };\n}\n\n/** Label types + member roster + group mappings for the settings UI. */\nasync function loadMembersSection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n}> {\n const { labelTypes, members } = await loadProjectRoster(db, projectId);\n const groupMappings = await loadGroupMappings(db, projectId);\n return { labelTypes, members, groupMappings };\n}\n\n/** Group mappings for the members settings tab. */\nasync function loadGroupMappings(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<ProjectGroupMapping[]> {\n return await new ProjectGroupMappingModel(db).list(projectId);\n}\n\n/** Channel summaries for the settings UI. */\nasync function loadNotificationChannels(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<SettingsNotificationChannel[]> {\n const rows = await new NotificationChannelModel(db, undefined, secret).list(projectId);\n return rows.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n }));\n}\n\n/** The viewer's own subscription for the settings UI (null when opted out). */\nasync function loadMySubscription(\n db: DatabaseAdapter,\n projectId: string,\n userId: string | undefined,\n): Promise<SettingsMySubscription | null> {\n if (!userId) {\n return null;\n }\n const row = await new NotificationSubscriptionModel(db).getFor(projectId, userId);\n if (!row) {\n return null;\n }\n return {\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Notification channels, providers, and my subscription for the UI. */\nasync function loadNotificationSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n userId: string | undefined,\n notifiers: { metadata: { kind: string } }[],\n): Promise<{\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n}> {\n const notificationChannels = await loadNotificationChannels(db, secret, projectId);\n const mySubscription = await loadMySubscription(db, projectId, userId);\n return {\n notificationChannels,\n notifyProviders: notifiers.map((candidate) => candidate.metadata.kind),\n mySubscription,\n };\n}\n\n/** Roster, tokens, webhooks, and status configs for the settings UI. */\nasync function loadInventorySection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n tokens: Omit<Token, \"hash\">[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n}> {\n const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);\n const tokens = await loadTokenSummaries(db, projectId);\n const webhooks = await loadWebhookSummaries(db, projectId);\n const statusConfigs = await loadStatusConfigSummaries(db, secret, projectId);\n return { labelTypes, members, groupMappings, tokens, webhooks, statusConfigs };\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, gitHosts, notifiers } = getStore();\n const inventory = await loadInventorySection(db, credentialKeys(config), project.id);\n const notifications = await loadNotificationSection(\n db,\n credentialKeys(config),\n project.id,\n user?.id,\n notifiers,\n );\n const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);\n return { project, ...inventory, ...notifications, gitHosts, isAdmin };\n}\n\n/** Render the project settings page for the given tab, optionally with form state. */\nexport async function renderSettingsPage(\n c: Context,\n tab: SettingsTab,\n formState?: SettingsFormState,\n): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n tokens: data.tokens,\n members: data.members,\n groupMappings: data.groupMappings,\n webhooks: data.webhooks,\n notificationChannels: data.notificationChannels,\n notifyProviders: data.notifyProviders,\n mySubscription: data.mySubscription,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\n/** Find a project by slug or throw 404. */\nexport async function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\n/** Trim a form value to a string, dropping files and blanks. */\nexport function asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"settings.handlers.mjs","names":[],"sources":["../../src/routers/settings.handlers.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n credentialDaysLeft,\n DesignCredentialModel,\n DesignModel,\n LabelModel,\n} from \"@storyshelf/core/models\";\nimport { PackageUsageModel } from \"@storyshelf/core/models\";\nimport { MemberModel } from \"@storyshelf/core/models\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport { ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { TokenModel } from \"@storyshelf/core/models\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { SecretInput } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { notificationsPaused } from \"../notifications-pause.ts\";\nimport {\n renderProjectSettingsPage,\n type SettingsFormState,\n type SettingsTab,\n} from \"../pages/project-settings.tsx\";\nimport type { SettingsAiData } from \"../pages/settings-ai.tsx\";\nimport type { DesignCredentialState } from \"../pages/settings-design-credential.tsx\";\nimport type { SettingsDesignData, SettingsDesignProvider } from \"../pages/settings-design.tsx\";\nimport type { SettingsLink, SettingsLinkCandidate } from \"../pages/settings-links.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type {\n SettingsMySubscription,\n SettingsNotificationChannel,\n SettingsNotifyProvider,\n} from \"../pages/settings-notifications.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { runnerTargets } from \"../targets.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { isSiteAdmin } from \"./insights.handlers.ts\";\n/** Aggregated data for rendering a settings tab. */\nexport interface SettingsData {\n project: Project;\n links: SettingsLink[];\n linkCandidates: SettingsLinkCandidate[];\n design: SettingsDesignData;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n isAdmin: boolean;\n}\n\n/** Tokens without hashes for the settings UI. */\nasync function loadTokenSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<Omit<Token, \"hash\">[]> {\n const tokensDb = await new TokenModel(db).list(projectId);\n return tokensDb.map(({ hash: _hash, ...rest }) => rest);\n}\n\n/** Webhook summaries for the settings UI. */\nasync function loadWebhookSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<SettingsWebhook[]> {\n const webhooksDb = await new WebhookModel(db).list(projectId);\n return webhooksDb.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n }));\n}\n\n/** True when the user may administer the project. */\nfunction isProjectAdmin(\n authEnabled: boolean,\n user: AuthUser | null,\n members: SettingsMember[],\n): boolean {\n return (\n !authEnabled ||\n user?.role === \"admin\" ||\n members.some((member) => member.userId === user?.id && member.role === \"admin\")\n );\n}\n\n/** Label types + member roster for the settings UI. */\nasync function loadProjectRoster(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{ labelTypes: LabelType[]; members: SettingsMember[] }> {\n const labelTypes = await new LabelModel(db).listTypes(projectId);\n const members = await new MemberModel(db).list(projectId);\n return { labelTypes, members };\n}\n\n/** Label types + member roster + group mappings for the settings UI. */\nasync function loadMembersSection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n}> {\n const { labelTypes, members } = await loadProjectRoster(db, projectId);\n const groupMappings = await loadGroupMappings(db, projectId);\n return { labelTypes, members, groupMappings };\n}\n\n/** Group mappings for the members settings tab. */\nasync function loadGroupMappings(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<ProjectGroupMapping[]> {\n return await new ProjectGroupMappingModel(db).list(projectId);\n}\n\n/** Delivery kind of a stored channel's provider (unwired providers read as messages). */\nfunction deliveryOf(\n notifiers: readonly NotifierProvider[],\n provider: string,\n): SettingsNotificationChannel[\"delivery\"] {\n return NotificationChannelModel.isStateKind(notifiers, provider) ? \"state\" : \"message\";\n}\n\n/** Channel summaries for the settings UI. */\nasync function loadNotificationChannels(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n notifiers: readonly NotifierProvider[],\n): Promise<SettingsNotificationChannel[]> {\n const rows = await new NotificationChannelModel(db, undefined, secret).list(projectId);\n return rows.map((row) => ({\n id: row.id,\n provider: row.provider,\n delivery: deliveryOf(notifiers, row.provider),\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n lastError: row.lastError,\n lastErrorAt: row.lastErrorAt,\n }));\n}\n\n/** The viewer's own subscription for the settings UI (null when opted out). */\nasync function loadMySubscription(\n db: DatabaseAdapter,\n projectId: string,\n userId: string | undefined,\n): Promise<SettingsMySubscription | null> {\n if (!userId) {\n return null;\n }\n const row = await new NotificationSubscriptionModel(db).getFor(projectId, userId);\n if (!row) {\n return null;\n }\n return {\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Notification channels, providers, and my subscription for the UI. */\nasync function loadNotificationSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n userId: string | undefined,\n notifiers: readonly NotifierProvider[],\n): Promise<{\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n}> {\n const notificationChannels = await loadNotificationChannels(db, secret, projectId, notifiers);\n const mySubscription = await loadMySubscription(db, projectId, userId);\n const notifyProviders = notifiers.map((candidate) => ({\n kind: candidate.metadata.kind,\n name: candidate.metadata.name,\n delivery: deliveryOf(notifiers, candidate.metadata.kind),\n }));\n return { notificationChannels, notifyProviders, mySubscription };\n}\n\n/** Roster, tokens and webhooks for the settings UI. */\nasync function loadInventorySection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n tokens: Omit<Token, \"hash\">[];\n webhooks: SettingsWebhook[];\n}> {\n const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);\n const tokens = await loadTokenSummaries(db, projectId);\n const webhooks = await loadWebhookSummaries(db, projectId);\n return { labelTypes, members, groupMappings, tokens, webhooks };\n}\n\n/** Package links plus the other projects that can be picked as upstream. */\nasync function loadLinksSection(\n db: DatabaseAdapter,\n project: Project,\n): Promise<{ links: SettingsLink[]; linkCandidates: SettingsLinkCandidate[] }> {\n const projects = await new ProjectModel(db).list();\n const byId = new Map(projects.map((candidate) => [candidate.id, candidate]));\n const rows = await new PackageUsageModel(db).listLinks(project.id);\n const links = rows.flatMap((row) => {\n const upstream = byId.get(row.upstreamId);\n return upstream\n ? [{ packageName: row.packageName, upstreamSlug: upstream.slug, upstreamName: upstream.name }]\n : [];\n });\n const linkCandidates = projects\n .filter((candidate) => candidate.id !== project.id)\n .map((candidate) => ({ slug: candidate.slug, name: candidate.name }));\n return { links, linkCandidates };\n}\n\n/** Design files, component mappings and per-provider credential health for the Design tab (never a token). */\nasync function loadDesignSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n providers: DesignProvider[],\n projectId: string,\n): Promise<SettingsDesignData> {\n const model = new DesignModel(db, providers);\n const files = (await model.projectLinks(projectId)).map((link) => ({\n id: link.id,\n label: link.label ?? \"Design file\",\n url: link.url,\n }));\n const components = (await model.componentLinks(projectId)).map((link) => ({\n id: link.id,\n importPath: link.importPath ?? \"\",\n url: link.url,\n }));\n const credentials = new DesignCredentialModel(db, secret);\n const rows = await Promise.all(\n providers.map(async (provider) => await providerSettings(credentials, provider, projectId)),\n );\n return { files, components, providers: rows };\n}\n\nasync function providerSettings(\n credentials: DesignCredentialModel,\n provider: DesignProvider,\n projectId: string,\n): Promise<SettingsDesignProvider> {\n const { metadata } = provider;\n const maxAgeDays = metadata.credentialMaxAgeDays;\n const row = await credentials.info(projectId, metadata.kind);\n return {\n kind: metadata.kind,\n name: metadata.name,\n credentialLabel: metadata.credentialLabel,\n credentialHelp: metadata.credentialHelp,\n ...(metadata.credentialPlaceholder\n ? { credentialPlaceholder: metadata.credentialPlaceholder }\n : {}),\n urlHint: metadata.urlHint,\n ...(maxAgeDays === undefined ? {} : { maxAgeDays }),\n credential: row\n ? {\n status: credentialState(row.status),\n daysLeft: maxAgeDays === undefined ? null : credentialDaysLeft(row.createdAt, maxAgeDays),\n checkedAt: row.checkedAt,\n }\n : null,\n };\n}\n\n/** The stored status column as a known state (anything unrecognized reads as unknown). */\nfunction credentialState(status: string): DesignCredentialState {\n return status === \"ok\" || status === \"unauthorized\" || status === \"forbidden\"\n ? status\n : \"unknown\";\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, design: designProviders, notifiers } = getStore();\n const inventory = await loadInventorySection(db, project.id);\n const notifications = await loadNotificationSection(\n db,\n credentialKeys(config),\n project.id,\n user?.id,\n notifiers,\n );\n const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);\n const linksSection = await loadLinksSection(db, project);\n const design = await loadDesignSection(db, credentialKeys(config), designProviders, project.id);\n return { project, ...inventory, ...notifications, ...linksSection, design, isAdmin };\n}\n\n/** Render the project settings page for the given tab, optionally with form state. */\nexport async function renderSettingsPage(\n c: Context,\n tab: SettingsTab,\n formState?: SettingsFormState,\n): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n links: data.links,\n linkCandidates: data.linkCandidates,\n design: data.design,\n tokens: data.tokens,\n members: data.members,\n groupMappings: data.groupMappings,\n webhooks: data.webhooks,\n notificationChannels: data.notificationChannels,\n notifyProviders: data.notifyProviders,\n mySubscription: data.mySubscription,\n isAdmin: data.isAdmin,\n notificationsPaused: data.notifyProviders.length > 0 && (await notificationsPaused()),\n ai: aiSettingsData(c),\n targets: await runnerTargets(),\n },\n formState,\n );\n}\n\n/** AI availability and profile choices for the settings AI tab. */\nfunction aiSettingsData(c: Context): SettingsAiData {\n const { ai } = getStore();\n return {\n available: ai !== undefined,\n profiles: ai?.profileNames() ?? [],\n defaultProfile: ai?.defaultProfile() ?? \"\",\n isSiteAdmin: isSiteAdmin(c),\n };\n}\n\n/** Find a project by slug or throw 404. */\nexport async function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\n/** Trim a form value to a string, dropping files and blanks. */\nexport function asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { registerAiSettings } from "./settings-ai.mjs";
|
|
5
|
+
import { registerCaptureSettings } from "./settings-capture.mjs";
|
|
6
|
+
import { registerDesignSettings } from "./settings-design.mjs";
|
|
4
7
|
import { registerGeneralSettings } from "./settings-general.mjs";
|
|
5
8
|
import { registerLabelSettings } from "./settings-labels.mjs";
|
|
9
|
+
import { registerLinkSettings } from "./settings-links.mjs";
|
|
6
10
|
import { registerMemberSettings } from "./settings-members.mjs";
|
|
7
11
|
import { registerNotificationSettings } from "./settings-notifications.mjs";
|
|
8
|
-
import { registerStatusSettings } from "./settings-status.mjs";
|
|
9
12
|
import { registerTokenSettings } from "./settings-tokens.mjs";
|
|
10
13
|
import { registerWebhookSettings } from "./settings-webhooks.mjs";
|
|
11
14
|
//#region src/routers/settings.ts
|
|
12
15
|
/** Register the server-rendered project settings pages and their form handlers. */
|
|
13
16
|
function registerSettingsPages(app) {
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
registerProjectSettings(app);
|
|
18
|
+
registerCatalogSettings(app);
|
|
16
19
|
registerTokenSettings(app);
|
|
17
20
|
registerWebhookSettings(app);
|
|
18
21
|
registerNotificationSettings(app);
|
|
19
22
|
registerMemberSettings(app);
|
|
20
|
-
|
|
23
|
+
registerAiSettings(app);
|
|
24
|
+
}
|
|
25
|
+
function registerProjectSettings(app) {
|
|
26
|
+
registerGeneralSettings(app);
|
|
27
|
+
registerCaptureSettings(app);
|
|
28
|
+
}
|
|
29
|
+
function registerCatalogSettings(app) {
|
|
30
|
+
registerLabelSettings(app);
|
|
31
|
+
registerLinkSettings(app);
|
|
32
|
+
registerDesignSettings(app);
|
|
21
33
|
}
|
|
22
34
|
//#endregion
|
|
23
35
|
export { registerSettingsPages };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfRouter } from \"../app-types.ts\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport {
|
|
1
|
+
{"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfRouter } from \"../app-types.ts\";\nimport { registerAiSettings } from \"./settings-ai.ts\";\nimport { registerCaptureSettings } from \"./settings-capture.ts\";\nimport { registerDesignSettings } from \"./settings-design.ts\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport { registerLinkSettings } from \"./settings-links.ts\";\nimport { registerMemberSettings } from \"./settings-members.ts\";\nimport { registerNotificationSettings } from \"./settings-notifications.ts\";\nimport { registerTokenSettings } from \"./settings-tokens.ts\";\nimport { registerWebhookSettings } from \"./settings-webhooks.ts\";\n\n/** Register the server-rendered project settings pages and their form handlers. */\nexport function registerSettingsPages(app: ShelfRouter): void {\n registerProjectSettings(app);\n registerCatalogSettings(app);\n registerTokenSettings(app);\n registerWebhookSettings(app);\n registerNotificationSettings(app);\n registerMemberSettings(app);\n registerAiSettings(app);\n}\n\nfunction registerProjectSettings(app: ShelfRouter): void {\n registerGeneralSettings(app);\n registerCaptureSettings(app);\n}\n\nfunction registerCatalogSettings(app: ShelfRouter): void {\n registerLabelSettings(app);\n registerLinkSettings(app);\n registerDesignSettings(app);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAaA,SAAgB,sBAAsB,KAAwB;CAC5D,wBAAwB,GAAG;CAC3B,wBAAwB,GAAG;CAC3B,sBAAsB,GAAG;CACzB,wBAAwB,GAAG;CAC3B,6BAA6B,GAAG;CAChC,uBAAuB,GAAG;CAC1B,mBAAmB,GAAG;AACxB;AAEA,SAAS,wBAAwB,KAAwB;CACvD,wBAAwB,GAAG;CAC3B,wBAAwB,GAAG;AAC7B;AAEA,SAAS,wBAAwB,KAAwB;CACvD,sBAAsB,GAAG;CACzB,qBAAqB,GAAG;CACxB,uBAAuB,GAAG;AAC5B"}
|