@storyshelf/core 0.1.3 → 0.3.2

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.
Files changed (239) hide show
  1. package/README.md +50 -46
  2. package/dist/adapter/auth.d.mts +4 -4
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/{adapters → adapter}/capture-queue.d.mts +4 -4
  5. package/dist/adapter/capture-queue.mjs +4 -0
  6. package/dist/adapter/capture-runner.d.mts +7 -7
  7. package/dist/adapter/database.d.mts +20 -9
  8. package/dist/adapter/database.mjs +60 -1
  9. package/dist/adapter/database.mjs.map +1 -0
  10. package/dist/adapter/git-host/comments.d.mts +41 -0
  11. package/dist/adapter/git-host/comments.mjs +54 -0
  12. package/dist/adapter/git-host/comments.mjs.map +1 -0
  13. package/dist/adapter/git-host.d.mts +3 -7
  14. package/dist/adapter/init.d.mts +58 -0
  15. package/dist/adapter/init.mjs +93 -0
  16. package/dist/adapter/init.mjs.map +1 -0
  17. package/dist/adapter/metadata.d.mts +83 -0
  18. package/dist/adapter/metadata.mjs +4 -0
  19. package/dist/adapter/storage.d.mts +15 -4
  20. package/dist/adapters/webhook-events.mjs +12 -4
  21. package/dist/adapters/webhook-events.mjs.map +1 -1
  22. package/dist/capture/adapter.d.mts +5 -1
  23. package/dist/capture/dispatch.mjs +150 -0
  24. package/dist/capture/dispatch.mjs.map +1 -0
  25. package/dist/capture/orchestrator.d.mts +2 -0
  26. package/dist/capture/orchestrator.mjs +5 -44
  27. package/dist/capture/orchestrator.mjs.map +1 -1
  28. package/dist/capture/pipeline.d.mts +4 -1
  29. package/dist/capture/pipeline.mjs +6 -6
  30. package/dist/capture/pipeline.mjs.map +1 -1
  31. package/dist/capture/queue.d.mts +2 -1
  32. package/dist/capture/queue.mjs +2 -1
  33. package/dist/capture/queue.mjs.map +1 -1
  34. package/dist/capture/skip-checks.mjs +76 -0
  35. package/dist/capture/skip-checks.mjs.map +1 -0
  36. package/dist/capture/statics.mjs +124 -0
  37. package/dist/capture/statics.mjs.map +1 -0
  38. package/dist/capture/status-fanout.mjs.map +1 -1
  39. package/dist/capture/storybook.mjs.map +1 -1
  40. package/dist/capture.d.mts +8 -0
  41. package/dist/capture.mjs +9 -0
  42. package/dist/config.d.mts +10 -2
  43. package/dist/config.mjs +14 -2
  44. package/dist/config.mjs.map +1 -1
  45. package/dist/ddl.d.mts +1 -1
  46. package/dist/ddl.mjs +1 -1
  47. package/dist/ddl.mjs.map +1 -1
  48. package/dist/diff/engine.mjs.map +1 -1
  49. package/dist/diff/options.d.mts +3 -1
  50. package/dist/diff.d.mts +5 -0
  51. package/dist/diff.mjs +6 -0
  52. package/dist/index.d.mts +32 -25
  53. package/dist/index.mjs +207 -253
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/logger.d.mts +3 -3
  56. package/dist/middleware/auth-gate.mjs +36 -0
  57. package/dist/middleware/auth-gate.mjs.map +1 -0
  58. package/dist/middleware/csrf.mjs.map +1 -1
  59. package/dist/middleware/request-log.mjs +28 -0
  60. package/dist/middleware/request-log.mjs.map +1 -0
  61. package/dist/middleware/store-scope.mjs +29 -0
  62. package/dist/middleware/store-scope.mjs.map +1 -0
  63. package/dist/models/baseline.mjs +7 -5
  64. package/dist/models/baseline.mjs.map +1 -1
  65. package/dist/models/build.mjs +3 -1
  66. package/dist/models/build.mjs.map +1 -1
  67. package/dist/models/comment.mjs +2 -1
  68. package/dist/models/comment.mjs.map +1 -1
  69. package/dist/models/label.mjs +2 -1
  70. package/dist/models/label.mjs.map +1 -1
  71. package/dist/models/member.mjs +1 -1
  72. package/dist/models/member.mjs.map +1 -1
  73. package/dist/models/project.mjs +1 -1
  74. package/dist/models/project.mjs.map +1 -1
  75. package/dist/models/snapshot.mjs +1 -1
  76. package/dist/models/snapshot.mjs.map +1 -1
  77. package/dist/models/status-config.mjs +1 -1
  78. package/dist/models/status-config.mjs.map +1 -1
  79. package/dist/models/token.mjs +1 -1
  80. package/dist/models/token.mjs.map +1 -1
  81. package/dist/models/webhook.mjs +11 -3
  82. package/dist/models/webhook.mjs.map +1 -1
  83. package/dist/pages/build-detail.mjs +9 -4
  84. package/dist/pages/build-detail.mjs.map +1 -1
  85. package/dist/pages/build-diff-comments.mjs +113 -0
  86. package/dist/pages/build-diff-comments.mjs.map +1 -0
  87. package/dist/pages/build-diff-header.mjs +163 -0
  88. package/dist/pages/build-diff-header.mjs.map +1 -0
  89. package/dist/pages/build-diff-nav.mjs +66 -0
  90. package/dist/pages/build-diff-nav.mjs.map +1 -0
  91. package/dist/pages/build-diff-viewer.mjs +197 -0
  92. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  93. package/dist/pages/build-diff.mjs +65 -413
  94. package/dist/pages/build-diff.mjs.map +1 -1
  95. package/dist/pages/compute-jobs.mjs +1 -1
  96. package/dist/pages/compute-jobs.mjs.map +1 -1
  97. package/dist/pages/label-detail.mjs +4 -3
  98. package/dist/pages/label-detail.mjs.map +1 -1
  99. package/dist/pages/login.mjs.map +1 -1
  100. package/dist/pages/project-builds.mjs +5 -3
  101. package/dist/pages/project-builds.mjs.map +1 -1
  102. package/dist/pages/project-create.mjs.map +1 -1
  103. package/dist/pages/project-settings.mjs.map +1 -1
  104. package/dist/pages/projects.mjs +11 -6
  105. package/dist/pages/projects.mjs.map +1 -1
  106. package/dist/pages/root.mjs +6 -3
  107. package/dist/pages/root.mjs.map +1 -1
  108. package/dist/pages/settings-general.mjs.map +1 -1
  109. package/dist/pages/settings-labels.mjs +1 -1
  110. package/dist/pages/settings-labels.mjs.map +1 -1
  111. package/dist/pages/settings-members.mjs +1 -1
  112. package/dist/pages/settings-members.mjs.map +1 -1
  113. package/dist/pages/settings-status.mjs +2 -1
  114. package/dist/pages/settings-status.mjs.map +1 -1
  115. package/dist/pages/settings-tests.mjs +3 -3
  116. package/dist/pages/settings-tests.mjs.map +1 -1
  117. package/dist/pages/settings-tokens.mjs.map +1 -1
  118. package/dist/pages/settings-webhooks.mjs.map +1 -1
  119. package/dist/pages/storybook.mjs +38 -1
  120. package/dist/pages/storybook.mjs.map +1 -1
  121. package/dist/paths.mjs.map +1 -0
  122. package/dist/retention/purge.mjs +6 -3
  123. package/dist/retention/purge.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +1 -1
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs.map +1 -1
  127. package/dist/routers/auth.mjs.map +1 -1
  128. package/dist/routers/builds.handlers.mjs +106 -21
  129. package/dist/routers/builds.handlers.mjs.map +1 -1
  130. package/dist/routers/builds.mjs +54 -34
  131. package/dist/routers/builds.mjs.map +1 -1
  132. package/dist/routers/comments.mjs.map +1 -1
  133. package/dist/routers/health.mjs +137 -0
  134. package/dist/routers/health.mjs.map +1 -0
  135. package/dist/routers/helpers.mjs +2 -2
  136. package/dist/routers/helpers.mjs.map +1 -1
  137. package/dist/routers/htmx.mjs.map +1 -1
  138. package/dist/routers/labels.mjs +1 -1
  139. package/dist/routers/labels.mjs.map +1 -1
  140. package/dist/routers/media.mjs +2 -2
  141. package/dist/routers/media.mjs.map +1 -1
  142. package/dist/routers/members.mjs.map +1 -1
  143. package/dist/routers/projects.mjs +1 -1
  144. package/dist/routers/projects.mjs.map +1 -1
  145. package/dist/routers/schemas.mjs.map +1 -1
  146. package/dist/routers/settings-general.mjs +95 -0
  147. package/dist/routers/settings-general.mjs.map +1 -0
  148. package/dist/routers/settings-labels.mjs +49 -0
  149. package/dist/routers/settings-labels.mjs.map +1 -0
  150. package/dist/routers/settings-members.mjs +30 -0
  151. package/dist/routers/settings-members.mjs.map +1 -0
  152. package/dist/routers/settings-status.mjs +73 -0
  153. package/dist/routers/settings-status.mjs.map +1 -0
  154. package/dist/routers/settings-tokens.mjs +40 -0
  155. package/dist/routers/settings-tokens.mjs.map +1 -0
  156. package/dist/routers/settings-webhooks.mjs +63 -0
  157. package/dist/routers/settings-webhooks.mjs.map +1 -0
  158. package/dist/routers/settings.handlers.mjs +93 -0
  159. package/dist/routers/settings.handlers.mjs.map +1 -0
  160. package/dist/routers/settings.mjs +13 -247
  161. package/dist/routers/settings.mjs.map +1 -1
  162. package/dist/routers/snapshots.mjs +1 -1
  163. package/dist/routers/snapshots.mjs.map +1 -1
  164. package/dist/routers/status-configs.mjs +1 -1
  165. package/dist/routers/status-configs.mjs.map +1 -1
  166. package/dist/routers/storybook.mjs +11 -4
  167. package/dist/routers/storybook.mjs.map +1 -1
  168. package/dist/routers/tokens.mjs +1 -1
  169. package/dist/routers/tokens.mjs.map +1 -1
  170. package/dist/routers/ui.mjs +4 -4
  171. package/dist/routers/ui.mjs.map +1 -1
  172. package/dist/routers/webhooks.mjs +6 -6
  173. package/dist/routers/webhooks.mjs.map +1 -1
  174. package/dist/schema/baseline.d.mts +18 -0
  175. package/dist/schema/baseline.mjs +22 -0
  176. package/dist/schema/baseline.mjs.map +1 -0
  177. package/dist/schema/build.d.mts +26 -0
  178. package/dist/schema/build.mjs +29 -0
  179. package/dist/schema/build.mjs.map +1 -0
  180. package/dist/schema/comment.d.mts +19 -0
  181. package/dist/schema/comment.mjs +26 -0
  182. package/dist/schema/comment.mjs.map +1 -0
  183. package/dist/schema/label.d.mts +25 -0
  184. package/dist/schema/label.mjs +30 -0
  185. package/dist/schema/label.mjs.map +1 -0
  186. package/dist/schema/member.d.mts +15 -0
  187. package/dist/schema/member.mjs +19 -0
  188. package/dist/schema/member.mjs.map +1 -0
  189. package/dist/schema/project.d.mts +34 -0
  190. package/dist/schema/project.mjs +25 -0
  191. package/dist/schema/project.mjs.map +1 -0
  192. package/dist/schema/snapshot.d.mts +30 -0
  193. package/dist/schema/snapshot.mjs +34 -0
  194. package/dist/schema/snapshot.mjs.map +1 -0
  195. package/dist/schema/status-config.d.mts +16 -0
  196. package/dist/schema/status-config.mjs +20 -0
  197. package/dist/schema/status-config.mjs.map +1 -0
  198. package/dist/schema/token.d.mts +15 -0
  199. package/dist/schema/token.mjs +19 -0
  200. package/dist/schema/token.mjs.map +1 -0
  201. package/dist/schema/user.d.mts +17 -0
  202. package/dist/schema/user.mjs +19 -0
  203. package/dist/schema/user.mjs.map +1 -0
  204. package/dist/schema/webhook.d.mts +16 -0
  205. package/dist/schema/webhook.mjs +20 -0
  206. package/dist/schema/webhook.mjs.map +1 -0
  207. package/dist/schema.d.mts +13 -168
  208. package/dist/schema.mjs +12 -2
  209. package/dist/schema.mjs.map +1 -1
  210. package/dist/store.mjs.map +1 -1
  211. package/dist/types.d.mts +1 -2
  212. package/dist/types.mjs.map +1 -1
  213. package/dist/ui/document.mjs.map +1 -1
  214. package/dist/ui/feedback.mjs +24 -0
  215. package/dist/ui/feedback.mjs.map +1 -0
  216. package/dist/ui/{components.mjs → forms.mjs} +3 -18
  217. package/dist/ui/forms.mjs.map +1 -0
  218. package/dist/ui/styles.mjs.map +1 -1
  219. package/dist/urls.mjs.map +1 -1
  220. package/package.json +47 -45
  221. package/dist/adapters/metadata.d.mts +0 -24
  222. package/dist/models/baseline.d.mts +0 -21
  223. package/dist/models/build.d.mts +0 -53
  224. package/dist/models/comment.d.mts +0 -22
  225. package/dist/models/label.d.mts +0 -49
  226. package/dist/models/member.d.mts +0 -27
  227. package/dist/models/project.d.mts +0 -43
  228. package/dist/models/snapshot.d.mts +0 -47
  229. package/dist/models/token.d.mts +0 -33
  230. package/dist/models/webhook.d.mts +0 -38
  231. package/dist/retention/purge.d.mts +0 -31
  232. package/dist/schema-tables.mjs +0 -166
  233. package/dist/schema-tables.mjs.map +0 -1
  234. package/dist/ui/components.mjs.map +0 -1
  235. package/dist/ui/document.d.mts +0 -9
  236. package/dist/utils/paths.mjs.map +0 -1
  237. package/dist/utils/ulid.d.mts +0 -10
  238. /package/dist/{utils/paths.d.mts → paths.d.mts} +0 -0
  239. /package/dist/{utils/paths.mjs → paths.mjs} +0 -0
@@ -0,0 +1,40 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { randomToken } from "../utils/hash.mjs";
5
+ import { getStore } from "../store.mjs";
6
+ import { TokenModel } from "../models/token.mjs";
7
+ import { hxRedirect } from "./htmx.mjs";
8
+ import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
9
+ //#region src/routers/settings-tokens.ts
10
+ /** CI token settings (create with one-time display, delete). */
11
+ function registerTokenSettings(app) {
12
+ app.get("/projects/:slug/settings/tokens", async (c) => c.html(await renderSettingsPage(c, "tokens")));
13
+ app.post("/projects/:slug/settings/tokens", handleCreateToken);
14
+ app.post("/projects/:slug/settings/tokens/:tokenId/delete", handleDeleteToken);
15
+ }
16
+ async function handleCreateToken(c) {
17
+ const project = await findProject(c.req.param("slug") ?? "");
18
+ const form = await c.req.formData();
19
+ const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
20
+ if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
21
+ const token = randomToken("shelf_");
22
+ await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);
23
+ return await renderTokenCreated(c, token.value);
24
+ }
25
+ /** Re-render the tokens tab with the one-time secret banner injected. */
26
+ async function renderTokenCreated(c, tokenValue) {
27
+ const html = await renderSettingsPage(c, "tokens");
28
+ const inject = `<div class="alert alert--success" role="alert"><strong class="alert__title">Token created</strong><div class="alert__body">Copy now — shown once: <code>${tokenValue}</code></div></div>`;
29
+ const withToken = html.replace("<nav class=\"tabs\"", `${inject}<nav class="tabs"`);
30
+ return c.html(withToken);
31
+ }
32
+ async function handleDeleteToken(c) {
33
+ const project = await findProject(c.req.param("slug") ?? "");
34
+ await new TokenModel(getStore().db).remove(c.req.param("tokenId") ?? "");
35
+ return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
36
+ }
37
+ //#endregion
38
+ export { registerTokenSettings };
39
+
40
+ //# sourceMappingURL=settings-tokens.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/routers/settings-tokens.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** CI token settings (create with one-time display, delete). */\nexport function registerTokenSettings(app: ShelfApp): 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 token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);\n return await renderTokenCreated(c, token.value);\n}\n\n/** Re-render the tokens tab with the one-time secret banner injected. */\nasync function renderTokenCreated(c: Context, tokenValue: string): Promise<Response> {\n const html = await renderSettingsPage(c, \"tokens\");\n const inject = `<div class=\"alert alert--success\" role=\"alert\"><strong class=\"alert__title\">Token created</strong><div class=\"alert__body\">Copy now — shown once: <code>${tokenValue}</code></div></div>`;\n const withToken = html.replace('<nav class=\"tabs\"', `${inject}<nav class=\"tabs\"`);\n return c.html(withToken);\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 return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,sBAAsB,KAAqB;CACzD,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,QAAQ,YAAY,QAAQ;CAClC,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,WAAW,MAAM,IAAI;CAC5E,OAAO,MAAM,mBAAmB,GAAG,MAAM,KAAK;AAChD;;AAGA,eAAe,mBAAmB,GAAY,YAAuC;CACnF,MAAM,OAAO,MAAM,mBAAmB,GAAG,QAAQ;CACjD,MAAM,SAAS,2JAA2J,WAAW;CACrL,MAAM,YAAY,KAAK,QAAQ,uBAAqB,GAAG,OAAO,kBAAkB;CAChF,OAAO,EAAE,KAAK,SAAS;AACzB;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,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
@@ -0,0 +1,63 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { WebhookModel } from "../models/webhook.mjs";
5
+ import { randomToken } from "../utils/hash.mjs";
6
+ import { getStore } from "../store.mjs";
7
+ import { notFound } from "./helpers.mjs";
8
+ import { hxRedirect } from "./htmx.mjs";
9
+ import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
10
+ //#region src/routers/settings-webhooks.ts
11
+ /** Webhook settings (create with one-time secret display, delete). */
12
+ function registerWebhookSettings(app) {
13
+ app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await renderSettingsPage(c, "webhooks")));
14
+ app.post("/projects/:slug/settings/webhooks", handleCreateWebhook);
15
+ app.post("/projects/:slug/settings/webhooks/:webhookId/delete", handleDeleteWebhook);
16
+ }
17
+ /** Validate a webhook URL, returning the error message or null when valid. */
18
+ function webhookUrlError(url) {
19
+ if (!url) return "A valid URL is required";
20
+ let protocol;
21
+ try {
22
+ protocol = new URL(url).protocol;
23
+ } catch {
24
+ return "Enter a valid URL";
25
+ }
26
+ if (protocol !== "https:" && protocol !== "http:") return "URL must use http or https";
27
+ return null;
28
+ }
29
+ /** Parse the comma-separated events field. */
30
+ function parseWebhookEvents(form) {
31
+ const eventsRaw = asString(form.get("events"));
32
+ if (!eventsRaw) return;
33
+ return eventsRaw.split(",").map((event) => event.trim()).filter((event) => event.length > 0);
34
+ }
35
+ async function handleCreateWebhook(c) {
36
+ const project = await findProject(c.req.param("slug") ?? "");
37
+ const form = await c.req.formData();
38
+ const url = asString(form.get("url"));
39
+ const urlError = webhookUrlError(url);
40
+ if (urlError || !url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: urlError ?? "Invalid URL" } }) ?? "", 400);
41
+ return await createWebhookRecord(c, project, url, parseWebhookEvents(form));
42
+ }
43
+ async function createWebhookRecord(c, project, url, events) {
44
+ const webhookModel = new WebhookModel(getStore().db, getStore().config.secret);
45
+ const secret = randomToken("whsec_").value;
46
+ await webhookModel.create(project.id, {
47
+ url,
48
+ events,
49
+ secret
50
+ });
51
+ return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
52
+ }
53
+ async function handleDeleteWebhook(c) {
54
+ const project = await findProject(c.req.param("slug") ?? "");
55
+ const webhook = await new WebhookModel(getStore().db, getStore().config.secret).get(project.id, c.req.param("webhookId") ?? "");
56
+ if (!webhook) notFound("Webhook not found");
57
+ await new WebhookModel(getStore().db, getStore().config.secret).remove(project.id, webhook.id);
58
+ return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
59
+ }
60
+ //#endregion
61
+ export { registerWebhookSettings };
62
+
63
+ //# sourceMappingURL=settings-webhooks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/routers/settings-webhooks.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Webhook settings (create with one-time secret display, delete). */\nexport function registerWebhookSettings(app: ShelfApp): 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/** Validate a webhook URL, returning the error message or null when valid. */\nfunction webhookUrlError(url: string | undefined): string | null {\n if (!url) {\n return \"A valid URL is required\";\n }\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n return \"Enter a valid URL\";\n }\n if (protocol !== \"https:\" && protocol !== \"http:\") {\n return \"URL must use http or https\";\n }\n return null;\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 = webhookUrlError(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 webhookModel = new WebhookModel(getStore().db, getStore().config.secret);\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\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 new WebhookModel(getStore().db, getStore().config.secret).get(project.id, c.req.param(\"webhookId\") ?? \"\");\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db, getStore().config.secret).remove(project.id, webhook.id);\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,wBAAwB,KAAqB;CAC3D,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,gBAAgB,KAAwC;CAC/D,IAAI,CAAC,KACH,OAAO;CAET,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,GAAG,CAAC,CAAC;CAC1B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,aAAa,YAAY,aAAa,SACxC,OAAO;CAET,OAAO;AACT;;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,gBAAgB,GAAG;CACpC,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,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM;CAC7E,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;CACrC,MAAM,aAAa,OAAO,QAAQ,IAAI;EAAE;EAAK;EAAQ;CAAO,CAAC;CAC7D,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,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,IAAI,QAAQ,IAAI,EAAE,IAAI,MAAM,WAAW,KAAK,EAAE;CAC9H,IAAI,CAAC,SACH,SAAS,mBAAmB;CAE9B,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;CAC7F,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,mBAAmB;AACpE"}
@@ -0,0 +1,93 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { ProjectModel } from "../models/project.mjs";
5
+ import { WebhookModel } from "../models/webhook.mjs";
6
+ import { StatusConfigModel } from "../models/status-config.mjs";
7
+ import { getStore } from "../store.mjs";
8
+ import { LabelModel } from "../models/label.mjs";
9
+ import { MemberModel } from "../models/member.mjs";
10
+ import { TokenModel } from "../models/token.mjs";
11
+ import { notFound } from "./helpers.mjs";
12
+ import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
13
+ //#region src/routers/settings.handlers.ts
14
+ /** Tokens without hashes for the settings UI. */
15
+ async function loadTokenSummaries(db, projectId) {
16
+ return (await new TokenModel(db).list(projectId)).map(({ hash: _hash, ...rest }) => rest);
17
+ }
18
+ /** Webhook summaries for the settings UI. */
19
+ async function loadWebhookSummaries(db, projectId) {
20
+ return (await new WebhookModel(db).list(projectId)).map((webhook) => ({
21
+ id: webhook.id,
22
+ url: webhook.url,
23
+ events: WebhookModel.eventsOf(webhook)
24
+ }));
25
+ }
26
+ /** Status-config summaries for the settings UI. */
27
+ async function loadStatusConfigSummaries(db, secret, projectId) {
28
+ return (await new StatusConfigModel(db, secret).list(projectId)).map((row) => ({
29
+ id: row.id,
30
+ provider: row.provider,
31
+ config: JSON.parse(row.config),
32
+ hasToken: row.tokenEncrypted.length > 0,
33
+ createdAt: row.createdAt,
34
+ updatedAt: row.updatedAt
35
+ }));
36
+ }
37
+ /** True when the user may administer the project. */
38
+ function isProjectAdmin(authEnabled, user, members) {
39
+ return !authEnabled || user?.role === "admin" || members.some((member) => member.userId === user?.id && member.role === "admin");
40
+ }
41
+ /** Label types + member roster for the settings UI. */
42
+ async function loadProjectRoster(db, projectId) {
43
+ return {
44
+ labelTypes: await new LabelModel(db).listTypes(projectId),
45
+ members: await new MemberModel(db).list(projectId)
46
+ };
47
+ }
48
+ async function loadSettingsData(slug) {
49
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
50
+ if (!project) return null;
51
+ const { db, config, user, authEnabled, gitHosts } = getStore();
52
+ const { labelTypes, members } = await loadProjectRoster(db, project.id);
53
+ return {
54
+ project,
55
+ labelTypes,
56
+ tokens: await loadTokenSummaries(db, project.id),
57
+ members,
58
+ webhooks: await loadWebhookSummaries(db, project.id),
59
+ statusConfigs: await loadStatusConfigSummaries(db, config.secret, project.id),
60
+ gitHosts,
61
+ isAdmin: isProjectAdmin(authEnabled, user, members)
62
+ };
63
+ }
64
+ /** Render the project settings page for the given tab, optionally with form state. */
65
+ async function renderSettingsPage(c, tab, formState) {
66
+ const data = await loadSettingsData(c.req.param("slug") ?? "");
67
+ if (!data) notFound("Project not found");
68
+ return await renderProjectSettingsPage({
69
+ project: data.project,
70
+ activeTab: tab,
71
+ labelTypes: data.labelTypes,
72
+ tokens: data.tokens,
73
+ members: data.members,
74
+ webhooks: data.webhooks,
75
+ statusConfigs: data.statusConfigs,
76
+ gitHosts: data.gitHosts,
77
+ isAdmin: data.isAdmin
78
+ }, formState);
79
+ }
80
+ /** Find a project by slug or throw 404. */
81
+ async function findProject(slug) {
82
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
83
+ if (!project) notFound("Project not found");
84
+ return project;
85
+ }
86
+ /** Trim a form value to a string, dropping files and blanks. */
87
+ function asString(value) {
88
+ return typeof value === "string" ? value.trim() : void 0;
89
+ }
90
+ //#endregion
91
+ export { asString, findProject, renderSettingsPage };
92
+
93
+ //# sourceMappingURL=settings.handlers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.handlers.mjs","names":[],"sources":["../../src/routers/settings.handlers.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { AuthUser } from \"../adapters/auth.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { WebhookModel } from \"../models/webhook.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 { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport type { LabelType } from \"../schema/label.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Token } from \"../schema/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound } from \"./helpers.ts\";\n\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 webhooks: SettingsWebhook[];\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: string | undefined,\n projectId: string,\n): Promise<SettingsStatusConfig[]> {\n const statusConfigsDb = await new StatusConfigModel(db, 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\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 } = getStore();\n const { labelTypes, members } = await loadProjectRoster(db, project.id);\n const tokens = await loadTokenSummaries(db, project.id);\n const webhooks = await loadWebhookSummaries(db, project.id);\n const statusConfigs = await loadStatusConfigSummaries(db, config.secret, project.id);\n const isAdmin = isProjectAdmin(authEnabled, user, members);\n return { project, labelTypes, tokens, members, webhooks, statusConfigs, 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 webhooks: data.webhooks,\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":";;;;;;;;;;;;;;AAqCA,eAAe,mBACb,IACA,WACgC;CAEhC,QAAO,MADgB,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,SAAS,EAAA,CACxC,KAAK,EAAE,MAAM,OAAO,GAAG,WAAW,IAAI;AACxD;;AAGA,eAAe,qBACb,IACA,WAC4B;CAE5B,QAAO,MADkB,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK,SAAS,EAAA,CAC1C,KAAK,aAAa;EAClC,IAAI,QAAQ;EACZ,KAAK,QAAQ;EACb,QAAQ,aAAa,SAAS,OAAO;CACvC,EAAE;AACJ;;AAGA,eAAe,0BACb,IACA,QACA,WACiC;CAEjC,QAAO,MADuB,IAAI,kBAAkB,IAAI,MAAM,CAAC,CAAC,KAAK,SAAS,EAAA,CACvD,KAAK,SAAS;EACnC,IAAI,IAAI;EACR,UAAU,IAAI;EACd,QAAQ,KAAK,MAAM,IAAI,MAAM;EAC7B,UAAU,IAAI,eAAe,SAAS;EACtC,WAAW,IAAI;EACf,WAAW,IAAI;CACjB,EAAE;AACJ;;AAGA,SAAS,eACP,aACA,MACA,SACS;CACT,OACE,CAAC,eACD,MAAM,SAAS,WACf,QAAQ,MAAM,WAAW,OAAO,WAAW,MAAM,MAAM,OAAO,SAAS,OAAO;AAElF;;AAGA,eAAe,kBACb,IACA,WACiE;CAGjE,OAAO;EAAE,YAAA,MAFgB,IAAI,WAAW,EAAE,CAAC,CAAC,UAAU,SAAS;EAE1C,SAAA,MADC,IAAI,YAAY,EAAE,CAAC,CAAC,KAAK,SAAS;CAC3B;AAC/B;AAEA,eAAe,iBAAiB,MAA4C;CAC1E,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,IAAI;CACpE,IAAI,CAAC,SACH,OAAO;CAET,MAAM,EAAE,IAAI,QAAQ,MAAM,aAAa,aAAa,SAAS;CAC7D,MAAM,EAAE,YAAY,YAAY,MAAM,kBAAkB,IAAI,QAAQ,EAAE;CAKtE,OAAO;EAAE;EAAS;EAAY,QAAA,MAJT,mBAAmB,IAAI,QAAQ,EAAE;EAIhB;EAAS,UAAA,MAHxB,qBAAqB,IAAI,QAAQ,EAAE;EAGD,eAAA,MAF7B,0BAA0B,IAAI,OAAO,QAAQ,QAAQ,EAAE;EAEX;EAAU,SADlE,eAAe,aAAa,MAAM,OACsC;CAAE;AAC5F;;AAGA,eAAsB,mBACpB,GACA,KACA,WACiB;CAEjB,MAAM,OAAO,MAAM,iBADN,EAAE,IAAI,MAAM,MAAM,KAAK,EACI;CACxC,IAAI,CAAC,MACH,SAAS,mBAAmB;CAE9B,OAAO,MAAM,0BACX;EACE,SAAS,KAAK;EACd,WAAW;EACX,YAAY,KAAK;EACjB,QAAQ,KAAK;EACb,SAAS,KAAK;EACd,UAAU,KAAK;EACf,eAAe,KAAK;EACpB,UAAU,KAAK;EACf,SAAS,KAAK;CAChB,GACA,SACF;AACF;;AAGA,eAAsB,YAAY,MAAgC;CAChE,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,IAAI;CACpE,IAAI,CAAC,SACH,SAAS,mBAAmB;CAE9B,OAAO;AACT;;AAGA,SAAgB,SAAS,OAAsD;CAC7E,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,KAAA;AACpD"}
@@ -1,257 +1,23 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { ProjectModel } from "../models/project.mjs";
5
- import { LabelModel } from "../models/label.mjs";
6
- import { getStore } from "../store.mjs";
7
- import { MemberModel } from "../models/member.mjs";
8
- import { TokenModel } from "../models/token.mjs";
9
- import { randomToken } from "../utils/hash.mjs";
10
- import { notFound } from "./helpers.mjs";
11
- import { hxRedirect } from "./htmx.mjs";
12
- import { WebhookModel } from "../models/webhook.mjs";
13
- import { StatusConfigModel } from "../models/status-config.mjs";
14
- import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
4
+ import { registerGeneralSettings } from "./settings-general.mjs";
5
+ import { registerLabelSettings } from "./settings-labels.mjs";
6
+ import { registerMemberSettings } from "./settings-members.mjs";
7
+ import { registerStatusSettings } from "./settings-status.mjs";
8
+ import { registerTokenSettings } from "./settings-tokens.mjs";
9
+ import { registerWebhookSettings } from "./settings-webhooks.mjs";
15
10
  //#region src/routers/settings.ts
16
- async function loadSettingsData(slug) {
17
- const project = await new ProjectModel(getStore().db).getBySlug(slug);
18
- if (!project) return null;
19
- const { db, config, user, authEnabled, gitHosts } = getStore();
20
- const labelTypes = await new LabelModel(db).listTypes(project.id);
21
- const tokens = (await new TokenModel(db).list(project.id)).map(({ hash: _hash, ...rest }) => rest);
22
- const members = await new MemberModel(db).list(project.id);
23
- return {
24
- project,
25
- labelTypes,
26
- tokens,
27
- members,
28
- webhooks: (await new WebhookModel(db).list(project.id)).map((webhook) => ({
29
- id: webhook.id,
30
- url: webhook.url,
31
- events: WebhookModel.eventsOf(webhook)
32
- })),
33
- statusConfigs: (await new StatusConfigModel(db, config.secret).list(project.id)).map((row) => ({
34
- id: row.id,
35
- provider: row.provider,
36
- config: JSON.parse(row.config),
37
- hasToken: row.tokenEncrypted.length > 0,
38
- createdAt: row.createdAt,
39
- updatedAt: row.updatedAt
40
- })),
41
- gitHosts,
42
- isAdmin: !authEnabled || user?.role === "admin" || members.some((member) => member.userId === user?.id && member.role === "admin")
43
- };
44
- }
45
- /** Render the project settings page for the given tab, optionally with form state. */
46
- async function renderSettingsPage(c, tab, formState) {
47
- const data = await loadSettingsData(c.req.param("slug") ?? "");
48
- if (!data) notFound("Project not found");
49
- return await renderProjectSettingsPage({
50
- project: data.project,
51
- activeTab: tab,
52
- labelTypes: data.labelTypes,
53
- tokens: data.tokens,
54
- members: data.members,
55
- webhooks: data.webhooks,
56
- statusConfigs: data.statusConfigs,
57
- gitHosts: data.gitHosts,
58
- isAdmin: data.isAdmin
59
- }, formState);
60
- }
61
- async function settingsPage(c, tab) {
62
- const html = await renderSettingsPage(c, tab);
63
- if (!html) notFound("Project not found");
64
- return html;
65
- }
66
- async function findProject(slug) {
67
- const project = await new ProjectModel(getStore().db).getBySlug(slug);
68
- if (!project) notFound("Project not found");
69
- return project;
70
- }
71
11
  /** Register the server-rendered project settings pages and their form handlers. */
72
12
  function registerSettingsPages(app) {
73
- app.get("/projects/:slug/settings", async (c) => c.html(await settingsPage(c, "general")));
74
- app.get("/projects/:slug/settings/tests", async (c) => c.html(await settingsPage(c, "tests")));
75
- app.get("/projects/:slug/settings/labels", async (c) => c.html(await settingsPage(c, "labels")));
76
- app.get("/projects/:slug/settings/tokens", async (c) => c.html(await settingsPage(c, "tokens")));
77
- app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await settingsPage(c, "webhooks")));
78
- app.get("/projects/:slug/settings/members", async (c) => c.html(await settingsPage(c, "members")));
79
- app.get("/projects/:slug/settings/status", async (c) => c.html(await settingsPage(c, "status")));
80
- app.post("/projects/:slug/settings", async (c) => {
81
- const slug = c.req.param("slug");
82
- const project = await findProject(slug);
83
- const form = await c.req.formData();
84
- const name = form.get("name");
85
- if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
86
- const gitRepository = asString(form.get("gitRepository"));
87
- const gitDefaultBranch = asString(form.get("gitDefaultBranch"));
88
- const pixelThreshold = asString(form.get("pixelThreshold"));
89
- const maxDiffRatio = asString(form.get("maxDiffRatio"));
90
- const publicBranchRegex = asString(form.get("publicBranchRegex"));
91
- try {
92
- await new ProjectModel(getStore().db).update(project.id, {
93
- name: name.trim(),
94
- gitRepository: gitRepository ?? void 0,
95
- gitDefaultBranch: gitDefaultBranch ?? void 0,
96
- pixelThreshold: pixelThreshold ? Number(pixelThreshold) : void 0,
97
- maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : void 0,
98
- publicBranchRegex: publicBranchRegex === "" ? null : publicBranchRegex ?? void 0
99
- });
100
- return hxRedirect(c, `/projects/${slug}/settings`);
101
- } catch (error) {
102
- const message = error instanceof Error ? error.message : "Failed to update";
103
- return c.html(await renderSettingsPage(c, "general", { globalError: message }) ?? "", 400);
104
- }
105
- });
106
- app.post("/projects/:slug/settings/tests", async (c) => {
107
- const slug = c.req.param("slug");
108
- const project = await findProject(slug);
109
- const form = await c.req.formData();
110
- const executePlay = form.get("executePlay") === "true";
111
- const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
112
- const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0;
113
- if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return c.html(await renderSettingsPage(c, "tests", { globalError: "Play timeout must be between 1000 and 30000" }) ?? "", 400);
114
- try {
115
- await new ProjectModel(getStore().db).update(project.id, {
116
- executePlay,
117
- playTimeoutMs: playTimeoutMs ?? void 0
118
- });
119
- return hxRedirect(c, `/projects/${slug}/settings/tests`);
120
- } catch (error) {
121
- const message = error instanceof Error ? error.message : "Failed to update";
122
- return c.html(await renderSettingsPage(c, "tests", { globalError: message }) ?? "", 400);
123
- }
124
- });
125
- app.post("/projects/:slug/delete", async (c) => {
126
- const project = await findProject(c.req.param("slug"));
127
- await new ProjectModel(getStore().db).remove(project.id);
128
- return hxRedirect(c, "/projects");
129
- });
130
- app.post("/projects/:slug/settings/labels", async (c) => {
131
- const project = await findProject(c.req.param("slug"));
132
- const form = await c.req.formData();
133
- const key = asString(form.get("key"));
134
- const labelName = asString(form.get("labelName")) ?? asString(form.get("name"));
135
- const linkTemplate = asString(form.get("linkTemplate"));
136
- if (!key || !labelName) return c.html(await renderSettingsPage(c, "labels", { globalError: "Key and name are required" }) ?? "", 400);
137
- try {
138
- await new LabelModel(getStore().db).createType(project.id, {
139
- key,
140
- name: labelName,
141
- linkTemplate
142
- });
143
- return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
144
- } catch (error) {
145
- const message = error instanceof Error ? error.message : "Failed to create label";
146
- return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
147
- }
148
- });
149
- app.post("/projects/:slug/settings/labels/:key/delete", async (c) => {
150
- const project = await findProject(c.req.param("slug"));
151
- try {
152
- await new LabelModel(getStore().db).removeType(project.id, c.req.param("key"));
153
- } catch {
154
- return c.html(await renderSettingsPage(c, "labels", { globalError: "Cannot delete built-in label" }) ?? "", 400);
155
- }
156
- return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
157
- });
158
- app.post("/projects/:slug/settings/tokens", async (c) => {
159
- const project = await findProject(c.req.param("slug"));
160
- const form = await c.req.formData();
161
- const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
162
- if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
163
- const token = randomToken("shelf_");
164
- await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);
165
- const html = await renderSettingsPage(c, "tokens");
166
- const inject = `<div class="alert alert--success" role="alert"><strong class="alert__title">Token created</strong><div class="alert__body">Copy now — shown once: <code>${token.value}</code></div></div>`;
167
- const withToken = html.replace("<nav class=\"tabs\"", `${inject}<nav class="tabs"`);
168
- return c.html(withToken);
169
- });
170
- app.post("/projects/:slug/settings/tokens/:tokenId/delete", async (c) => {
171
- const project = await findProject(c.req.param("slug"));
172
- await new TokenModel(getStore().db).remove(c.req.param("tokenId"));
173
- return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
174
- });
175
- app.post("/projects/:slug/settings/webhooks", async (c) => {
176
- const project = await findProject(c.req.param("slug"));
177
- const form = await c.req.formData();
178
- const url = asString(form.get("url"));
179
- if (!url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "A valid URL is required" } }) ?? "", 400);
180
- let parsedUrl;
181
- try {
182
- parsedUrl = new URL(url);
183
- } catch {
184
- return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "Enter a valid URL" } }) ?? "", 400);
185
- }
186
- if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "URL must use http or https" } }) ?? "", 400);
187
- const eventsRaw = asString(form.get("events"));
188
- const events = eventsRaw ? eventsRaw.split(",").map((event) => event.trim()).filter((event) => event.length > 0) : void 0;
189
- const webhookModel = new WebhookModel(getStore().db);
190
- const secret = randomToken("whsec_").value;
191
- await webhookModel.create(project.id, {
192
- url,
193
- events,
194
- secret
195
- });
196
- return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
197
- });
198
- app.post("/projects/:slug/settings/webhooks/:webhookId/delete", async (c) => {
199
- const project = await findProject(c.req.param("slug"));
200
- const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param("webhookId"));
201
- if (!webhook) notFound("Webhook not found");
202
- await new WebhookModel(getStore().db).remove(project.id, webhook.id);
203
- return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
204
- });
205
- app.post("/projects/:slug/settings/members", async (c) => {
206
- const project = await findProject(c.req.param("slug"));
207
- const form = await c.req.formData();
208
- const userId = asString(form.get("userId"));
209
- const role = asString(form.get("role"));
210
- if (!userId || !role) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
211
- await new MemberModel(getStore().db).set(project.id, userId, role);
212
- return hxRedirect(c, `/projects/${project.slug}/settings/members`);
213
- });
214
- app.post("/projects/:slug/settings/members/:userId/remove", async (c) => {
215
- const project = await findProject(c.req.param("slug"));
216
- await new MemberModel(getStore().db).remove(project.id, c.req.param("userId"));
217
- return hxRedirect(c, `/projects/${project.slug}/settings/members`);
218
- });
219
- app.post("/projects/:slug/settings/status", async (c) => {
220
- const project = await findProject(c.req.param("slug"));
221
- const form = await c.req.formData();
222
- const providerKey = asString(form.get("provider"));
223
- const token = asString(form.get("token"));
224
- const configRaw = asString(form.get("config")) ?? "{}";
225
- const provider = getStore().gitHosts.find((p) => p.metadata.kind === providerKey);
226
- if (!provider) return c.html(await renderSettingsPage(c, "status", { globalError: "Unknown git provider" }) ?? "", 400);
227
- if (!token) return c.html(await renderSettingsPage(c, "status", { errors: { token: "Token is required" } }) ?? "", 400);
228
- let config;
229
- try {
230
- config = JSON.parse(configRaw);
231
- } catch {
232
- return c.html(await renderSettingsPage(c, "status", { errors: { config: "Config must be valid JSON" } }) ?? "", 400);
233
- }
234
- const parsed = provider.metadata.schema.safeParse(config);
235
- if (!parsed.success) return c.html(await renderSettingsPage(c, "status", { errors: { config: parsed.error.message } }) ?? "", 400);
236
- const { db, config: shelfConfig } = getStore();
237
- await new StatusConfigModel(db, shelfConfig.secret).create(project.id, {
238
- provider: provider.metadata.kind,
239
- config: parsed.data,
240
- token
241
- });
242
- return hxRedirect(c, `/projects/${project.slug}/settings/status`);
243
- });
244
- app.post("/projects/:slug/settings/status/:id/delete", async (c) => {
245
- const project = await findProject(c.req.param("slug"));
246
- const { db, config: shelfConfig } = getStore();
247
- await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param("id"));
248
- return hxRedirect(c, `/projects/${project.slug}/settings/status`);
249
- });
250
- }
251
- function asString(value) {
252
- return typeof value === "string" ? value.trim() : void 0;
13
+ registerGeneralSettings(app);
14
+ registerLabelSettings(app);
15
+ registerTokenSettings(app);
16
+ registerWebhookSettings(app);
17
+ registerMemberSettings(app);
18
+ registerStatusSettings(app);
253
19
  }
254
20
  //#endregion
255
- export { registerSettingsPages, renderSettingsPage };
21
+ export { registerSettingsPages };
256
22
 
257
23
  //# sourceMappingURL=settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { renderProjectSettingsPage, type SettingsFormState, type SettingsTab } from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\ninterface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\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 } = getStore();\n const labelTypes = await new LabelModel(db).listTypes(project.id);\n const tokensDb = await new TokenModel(db).list(project.id);\n const tokens = tokensDb.map(({ hash: _hash, ...rest }) => rest);\n const members = await new MemberModel(db).list(project.id);\n const webhooksDb = await new WebhookModel(db).list(project.id);\n const webhooks = webhooksDb.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) }));\n const statusConfigsDb = await new StatusConfigModel(db, config.secret).list(project.id);\n const statusConfigs = 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 const isAdmin = !authEnabled || user?.role === \"admin\" || members.some((member) => member.userId === user?.id && member.role === \"admin\");\n return { project, labelTypes, tokens, members, webhooks, statusConfigs, gitHosts, isAdmin };\n}\n\n/** Render the project settings page for the given tab, optionally with form state. */\nexport async function renderSettingsPage(c: Context, tab: SettingsTab, formState?: SettingsFormState): 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 webhooks: data.webhooks,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\nasync function settingsPage(c: Context, tab: SettingsTab): Promise<string> {\n const html = await renderSettingsPage(c, tab);\n if (!html) {\n notFound(\"Project not found\");\n }\n return html;\n}\n\nasync 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/** Register the server-rendered project settings pages and their form handlers. */\nexport function registerSettingsPages(app: ShelfApp): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await settingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) => c.html(await settingsPage(c, \"tests\")));\n app.get(\"/projects/:slug/settings/labels\", async (c) => c.html(await settingsPage(c, \"labels\")));\n app.get(\"/projects/:slug/settings/tokens\", async (c) => c.html(await settingsPage(c, \"tokens\")));\n app.get(\"/projects/:slug/settings/webhooks\", async (c) => c.html(await settingsPage(c, \"webhooks\")));\n app.get(\"/projects/:slug/settings/members\", async (c) => c.html(await settingsPage(c, \"members\")));\n app.get(\"/projects/:slug/settings/status\", async (c) => c.html(await settingsPage(c, \"status\")));\n\n app.post(\"/projects/:slug/settings\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html((await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\", 400);\n }\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n const pixelThreshold = asString(form.get(\"pixelThreshold\"));\n const maxDiffRatio = asString(form.get(\"maxDiffRatio\"));\n const publicBranchRegex = asString(form.get(\"publicBranchRegex\"));\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: name.trim(),\n gitRepository: gitRepository ?? undefined,\n gitDefaultBranch: gitDefaultBranch ?? undefined,\n pixelThreshold: pixelThreshold ? Number(pixelThreshold) : undefined,\n maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : undefined,\n publicBranchRegex: publicBranchRegex === \"\" ? null : (publicBranchRegex ?? undefined),\n });\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/tests\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const executePlay = form.get(\"executePlay\") === \"true\";\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined;\n if (playTimeoutMs !== undefined && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: \"Play timeout must be between 1000 and 30000\" })) ?? \"\", 400);\n }\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n executePlay,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new ProjectModel(getStore().db).remove(project.id);\n return hxRedirect(c, \"/projects\");\n });\n\n app.post(\"/projects/:slug/settings/labels\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\", 400);\n }\n try {\n await new LabelModel(getStore().db).createType(project.id, { key, name: labelName, linkTemplate });\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/labels/:key/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\"));\n } catch {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ?? \"\", 400);\n }\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n });\n\n app.post(\"/projects/:slug/settings/tokens\", async (c) => {\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((await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\", 400);\n }\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);\n const html = await renderSettingsPage(c, \"tokens\");\n const inject = `<div class=\"alert alert--success\" role=\"alert\"><strong class=\"alert__title\">Token created</strong><div class=\"alert__body\">Copy now — shown once: <code>${token.value}</code></div></div>`;\n const withToken = html.replace('<nav class=\"tabs\"', `${inject}<nav class=\"tabs\"`);\n return c.html(withToken);\n });\n\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n });\n\n app.post(\"/projects/:slug/settings/webhooks\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n if (!url) {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"A valid URL is required\" } })) ?? \"\", 400);\n }\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(url);\n } catch {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"Enter a valid URL\" } })) ?? \"\", 400);\n }\n if (parsedUrl.protocol !== \"https:\" && parsedUrl.protocol !== \"http:\") {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"URL must use http or https\" } })) ?? \"\", 400);\n }\n const eventsRaw = asString(form.get(\"events\"));\n const events = eventsRaw ? eventsRaw.split(\",\").map((event) => event.trim()).filter((event) => event.length > 0) : undefined;\n const webhookModel = new WebhookModel(getStore().db);\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n });\n\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param(\"webhookId\"));\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n });\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html((await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ?? \"\", 400);\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as never);\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/status\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const providerKey = asString(form.get(\"provider\"));\n const token = asString(form.get(\"token\"));\n const configRaw = asString(form.get(\"config\")) ?? \"{}\";\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === providerKey);\n if (!provider) {\n return c.html((await renderSettingsPage(c, \"status\", { globalError: \"Unknown git provider\" })) ?? \"\", 400);\n }\n if (!token) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { token: \"Token is required\" } })) ?? \"\", 400);\n }\n let config: unknown;\n try {\n config = JSON.parse(configRaw);\n } catch {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: \"Config must be valid JSON\" } })) ?? \"\", 400);\n }\n const parsed = provider.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: parsed.error.message } })) ?? \"\", 400);\n }\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).create(project.id, { provider: provider.metadata.kind, config: parsed.data, token });\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n\n app.post(\"/projects/:slug/settings/status/:id/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param(\"id\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport { registerMemberSettings } from \"./settings-members.ts\";\nimport { registerStatusSettings } from \"./settings-status.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: ShelfApp): void {\n registerGeneralSettings(app);\n registerLabelSettings(app);\n registerTokenSettings(app);\n registerWebhookSettings(app);\n registerMemberSettings(app);\n registerStatusSettings(app);\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,sBAAsB,KAAqB;CACzD,wBAAwB,GAAG;CAC3B,sBAAsB,GAAG;CACzB,sBAAsB,GAAG;CACzB,wBAAwB,GAAG;CAC3B,uBAAuB,GAAG;CAC1B,uBAAuB,GAAG;AAC5B"}
@@ -1,10 +1,10 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { SnapshotModel } from "../models/snapshot.mjs";
4
5
  import { getStore } from "../store.mjs";
5
6
  import { resolveAuthorizedProject } from "./helpers.mjs";
6
7
  import { notFound, okSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
7
- import { SnapshotModel } from "../models/snapshot.mjs";
8
8
  import { APPROVER_ROLES, VIEW_ROLES, approveSnapshot, buildForProject, refreshBuild, snapshotForBuild } from "./builds.handlers.mjs";
9
9
  import { createRoute, z } from "@hono/zod-openapi";
10
10
  //#region src/routers/snapshots.ts