@storyshelf/app 0.5.3
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
|
+
import { groupMappingCreateSchema, groupMappingSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, unauthorized } from "./schemas.mjs";
|
|
7
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
|
+
import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
|
|
9
|
+
import { HTTPException } from "hono/http-exception";
|
|
10
|
+
//#region src/routers/members.ts
|
|
11
|
+
const VIEW_ROLES = [
|
|
12
|
+
"viewer",
|
|
13
|
+
"developer",
|
|
14
|
+
"approver",
|
|
15
|
+
"admin"
|
|
16
|
+
];
|
|
17
|
+
const ADMIN_ROLES = ["admin"];
|
|
18
|
+
const listMembersRoute = createRoute({
|
|
19
|
+
method: "get",
|
|
20
|
+
tags: ["Members"],
|
|
21
|
+
summary: "List members",
|
|
22
|
+
path: "/api/v1/projects/{slug}/members",
|
|
23
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
24
|
+
responses: {
|
|
25
|
+
200: {
|
|
26
|
+
content: { "application/json": { schema: memberSchema.array() } },
|
|
27
|
+
description: "List project members"
|
|
28
|
+
},
|
|
29
|
+
...notFound,
|
|
30
|
+
...unauthorized
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const setMemberRoute = createRoute({
|
|
34
|
+
method: "post",
|
|
35
|
+
tags: ["Members"],
|
|
36
|
+
summary: "Add or update a member",
|
|
37
|
+
path: "/api/v1/projects/{slug}/members",
|
|
38
|
+
request: {
|
|
39
|
+
params: z.object({ slug: z.string() }),
|
|
40
|
+
body: { content: { "application/json": { schema: memberSetSchema } } }
|
|
41
|
+
},
|
|
42
|
+
responses: {
|
|
43
|
+
201: {
|
|
44
|
+
content: { "application/json": { schema: memberSchema } },
|
|
45
|
+
description: "Added or updated member"
|
|
46
|
+
},
|
|
47
|
+
...notFound
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const updateMemberRoute = createRoute({
|
|
51
|
+
method: "patch",
|
|
52
|
+
tags: ["Members"],
|
|
53
|
+
summary: "Change a member's role",
|
|
54
|
+
path: "/api/v1/projects/{slug}/members/{userId}",
|
|
55
|
+
request: {
|
|
56
|
+
params: z.object({
|
|
57
|
+
slug: z.string(),
|
|
58
|
+
userId: z.string()
|
|
59
|
+
}),
|
|
60
|
+
body: { content: { "application/json": { schema: memberRoleSchema } } }
|
|
61
|
+
},
|
|
62
|
+
responses: {
|
|
63
|
+
200: {
|
|
64
|
+
content: { "application/json": { schema: memberSchema } },
|
|
65
|
+
description: "Updated member role"
|
|
66
|
+
},
|
|
67
|
+
...notFound
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
const deleteMemberRoute = createRoute({
|
|
71
|
+
method: "delete",
|
|
72
|
+
tags: ["Members"],
|
|
73
|
+
summary: "Remove a member",
|
|
74
|
+
path: "/api/v1/projects/{slug}/members/{userId}",
|
|
75
|
+
request: { params: z.object({
|
|
76
|
+
slug: z.string(),
|
|
77
|
+
userId: z.string()
|
|
78
|
+
}) },
|
|
79
|
+
responses: {
|
|
80
|
+
204: { description: "Removed member" },
|
|
81
|
+
...notFound
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const listGroupMappingsRoute = createRoute({
|
|
85
|
+
method: "get",
|
|
86
|
+
tags: ["Members"],
|
|
87
|
+
summary: "List group mappings",
|
|
88
|
+
path: "/api/v1/projects/{slug}/group-mappings",
|
|
89
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
90
|
+
responses: {
|
|
91
|
+
200: {
|
|
92
|
+
content: { "application/json": { schema: groupMappingSchema.array() } },
|
|
93
|
+
description: "List identity-provider group mappings"
|
|
94
|
+
},
|
|
95
|
+
...notFound,
|
|
96
|
+
...unauthorized
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const createGroupMappingRoute = createRoute({
|
|
100
|
+
method: "post",
|
|
101
|
+
tags: ["Members"],
|
|
102
|
+
summary: "Create a group mapping",
|
|
103
|
+
path: "/api/v1/projects/{slug}/group-mappings",
|
|
104
|
+
request: {
|
|
105
|
+
params: z.object({ slug: z.string() }),
|
|
106
|
+
body: { content: { "application/json": { schema: groupMappingCreateSchema } } }
|
|
107
|
+
},
|
|
108
|
+
responses: {
|
|
109
|
+
201: {
|
|
110
|
+
content: { "application/json": { schema: groupMappingSchema } },
|
|
111
|
+
description: "Created group mapping (exact group-name match)"
|
|
112
|
+
},
|
|
113
|
+
...notFound
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const deleteGroupMappingRoute = createRoute({
|
|
117
|
+
method: "delete",
|
|
118
|
+
tags: ["Members"],
|
|
119
|
+
summary: "Remove a group mapping",
|
|
120
|
+
path: "/api/v1/projects/{slug}/group-mappings/{mappingId}",
|
|
121
|
+
request: { params: z.object({
|
|
122
|
+
slug: z.string(),
|
|
123
|
+
mappingId: z.string()
|
|
124
|
+
}) },
|
|
125
|
+
responses: {
|
|
126
|
+
204: { description: "Removed group mapping" },
|
|
127
|
+
...notFound
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
/** Register the project member list, upsert, and removal endpoints. */
|
|
131
|
+
function registerMembers(app) {
|
|
132
|
+
app.openapi(listMembersRoute, async (c) => {
|
|
133
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
134
|
+
return c.json(await new MemberModel(getStore().db).list(project.id));
|
|
135
|
+
});
|
|
136
|
+
app.openapi(setMemberRoute, async (c) => {
|
|
137
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
138
|
+
const body = c.req.valid("json");
|
|
139
|
+
return c.json(await new MemberModel(getStore().db).set(project.id, body.userId, body.role), 201);
|
|
140
|
+
});
|
|
141
|
+
app.openapi(updateMemberRoute, async (c) => {
|
|
142
|
+
const { slug, userId } = c.req.valid("param");
|
|
143
|
+
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
144
|
+
const body = c.req.valid("json");
|
|
145
|
+
return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));
|
|
146
|
+
});
|
|
147
|
+
app.openapi(deleteMemberRoute, async (c) => {
|
|
148
|
+
const { slug, userId } = c.req.valid("param");
|
|
149
|
+
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
150
|
+
await new MemberModel(getStore().db).remove(project.id, userId);
|
|
151
|
+
return c.body(null, 204);
|
|
152
|
+
});
|
|
153
|
+
registerGroupMappingRoutes(app);
|
|
154
|
+
}
|
|
155
|
+
/** Register identity-provider group mapping endpoints. */
|
|
156
|
+
function registerGroupMappingRoutes(app) {
|
|
157
|
+
app.openapi(listGroupMappingsRoute, async (c) => {
|
|
158
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
159
|
+
return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));
|
|
160
|
+
});
|
|
161
|
+
app.openapi(createGroupMappingRoute, async (c) => {
|
|
162
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
163
|
+
const body = c.req.valid("json");
|
|
164
|
+
if (body.groupName.includes("*")) throw new HTTPException(400, { message: "Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access." });
|
|
165
|
+
return c.json(await new ProjectGroupMappingModel(getStore().db).create(project.id, body.groupName, body.role), 201);
|
|
166
|
+
});
|
|
167
|
+
app.openapi(deleteGroupMappingRoute, async (c) => {
|
|
168
|
+
const { slug, mappingId } = c.req.valid("param");
|
|
169
|
+
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
170
|
+
await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);
|
|
171
|
+
return c.body(null, 204);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { registerMembers };
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=members.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n if (body.groupName.includes(\"*\")) {\n throw new HTTPException(400, {\n message:\n \"Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access.\",\n });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { notFound, requireSiteAdmin, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
+
import { notFound as notFound$1, notificationChannelCreateSchema, notificationChannelSchema, unauthorized } from "./schemas.mjs";
|
|
8
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
|
+
import { NotificationChannelModel } from "@storyshelf/core/models";
|
|
10
|
+
//#region src/routers/notification-channels.ts
|
|
11
|
+
const ADMIN_ROLES = ["admin"];
|
|
12
|
+
/** Public channel view (secret redacted, config/events decoded). */
|
|
13
|
+
function toPublic(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
projectId: row.projectId,
|
|
17
|
+
provider: row.provider,
|
|
18
|
+
config: NotificationChannelModel.configOf(row),
|
|
19
|
+
hasSecret: row.secretEncrypted !== null,
|
|
20
|
+
events: NotificationChannelModel.eventsOf(row),
|
|
21
|
+
enabled: row.enabled,
|
|
22
|
+
createdAt: row.createdAt,
|
|
23
|
+
updatedAt: row.updatedAt
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Validate channel input against the provider descriptor. */
|
|
27
|
+
function validateChannelInput(provider, config) {
|
|
28
|
+
const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);
|
|
29
|
+
if (!descriptor) return {
|
|
30
|
+
ok: false,
|
|
31
|
+
message: `Unknown provider: ${provider}`
|
|
32
|
+
};
|
|
33
|
+
const parsed = descriptor.metadata.schema.safeParse(config);
|
|
34
|
+
if (!parsed.success) return {
|
|
35
|
+
ok: false,
|
|
36
|
+
message: parsed.error.message
|
|
37
|
+
};
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}
|
|
40
|
+
const listChannelsRoute = createRoute({
|
|
41
|
+
method: "get",
|
|
42
|
+
tags: ["Notifications"],
|
|
43
|
+
summary: "List notification channels",
|
|
44
|
+
path: "/api/v1/projects/{slug}/notification-channels",
|
|
45
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
46
|
+
responses: {
|
|
47
|
+
200: {
|
|
48
|
+
content: { "application/json": { schema: notificationChannelSchema.array() } },
|
|
49
|
+
description: "List notification channels"
|
|
50
|
+
},
|
|
51
|
+
...notFound$1,
|
|
52
|
+
...unauthorized
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const createChannelRoute = createRoute({
|
|
56
|
+
method: "post",
|
|
57
|
+
tags: ["Notifications"],
|
|
58
|
+
summary: "Create a notification channel",
|
|
59
|
+
path: "/api/v1/projects/{slug}/notification-channels",
|
|
60
|
+
request: {
|
|
61
|
+
params: z.object({ slug: z.string() }),
|
|
62
|
+
body: { content: { "application/json": { schema: notificationChannelCreateSchema } } }
|
|
63
|
+
},
|
|
64
|
+
responses: {
|
|
65
|
+
201: {
|
|
66
|
+
content: { "application/json": { schema: notificationChannelSchema } },
|
|
67
|
+
description: "Created notification channel"
|
|
68
|
+
},
|
|
69
|
+
400: {
|
|
70
|
+
content: { "application/json": { schema: z.object({ message: z.string() }) } },
|
|
71
|
+
description: "Bad request"
|
|
72
|
+
},
|
|
73
|
+
...notFound$1
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const deleteChannelRoute = createRoute({
|
|
77
|
+
method: "delete",
|
|
78
|
+
tags: ["Notifications"],
|
|
79
|
+
summary: "Delete a notification channel",
|
|
80
|
+
path: "/api/v1/projects/{slug}/notification-channels/{channelId}",
|
|
81
|
+
request: { params: z.object({
|
|
82
|
+
slug: z.string(),
|
|
83
|
+
channelId: z.string()
|
|
84
|
+
}) },
|
|
85
|
+
responses: {
|
|
86
|
+
204: { description: "Deleted notification channel" },
|
|
87
|
+
...notFound$1
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
/** Register the project notification-channel list, create, and delete endpoints. */
|
|
91
|
+
function registerNotificationChannels(app) {
|
|
92
|
+
app.openapi(listChannelsRoute, async (c) => {
|
|
93
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
94
|
+
const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).list(project.id);
|
|
95
|
+
return c.json(rows.map((row) => toPublic(row)));
|
|
96
|
+
});
|
|
97
|
+
app.openapi(createChannelRoute, async (c) => {
|
|
98
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
99
|
+
const body = c.req.valid("json");
|
|
100
|
+
const valid = validateChannelInput(body.provider, body.config);
|
|
101
|
+
if (!valid.ok) return c.json({ message: valid.message }, 400);
|
|
102
|
+
const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
|
|
103
|
+
projectId: project.id,
|
|
104
|
+
...body
|
|
105
|
+
});
|
|
106
|
+
return c.json(toPublic(row), 201);
|
|
107
|
+
});
|
|
108
|
+
app.openapi(deleteChannelRoute, async (c) => {
|
|
109
|
+
const { slug, channelId } = c.req.valid("param");
|
|
110
|
+
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
111
|
+
const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
|
|
112
|
+
if ((await model.get(channelId))?.projectId !== project.id) notFound("Notification channel not found");
|
|
113
|
+
await model.remove(channelId);
|
|
114
|
+
return c.body(null, 204);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const listSystemRoute = createRoute({
|
|
118
|
+
method: "get",
|
|
119
|
+
tags: ["Notifications"],
|
|
120
|
+
summary: "List site-wide channels",
|
|
121
|
+
path: "/api/v1/admin/notification-channels",
|
|
122
|
+
request: {},
|
|
123
|
+
responses: {
|
|
124
|
+
200: {
|
|
125
|
+
content: { "application/json": { schema: notificationChannelSchema.array() } },
|
|
126
|
+
description: "List site-wide notification channels"
|
|
127
|
+
},
|
|
128
|
+
...unauthorized
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const createSystemRoute = createRoute({
|
|
132
|
+
method: "post",
|
|
133
|
+
tags: ["Notifications"],
|
|
134
|
+
summary: "Create a site-wide channel",
|
|
135
|
+
path: "/api/v1/admin/notification-channels",
|
|
136
|
+
request: { body: { content: { "application/json": { schema: notificationChannelCreateSchema } } } },
|
|
137
|
+
responses: {
|
|
138
|
+
201: {
|
|
139
|
+
content: { "application/json": { schema: notificationChannelSchema } },
|
|
140
|
+
description: "Created site-wide notification channel"
|
|
141
|
+
},
|
|
142
|
+
400: {
|
|
143
|
+
content: { "application/json": { schema: z.object({ message: z.string() }) } },
|
|
144
|
+
description: "Bad request"
|
|
145
|
+
},
|
|
146
|
+
...unauthorized
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
const deleteSystemRoute = createRoute({
|
|
150
|
+
method: "delete",
|
|
151
|
+
tags: ["Notifications"],
|
|
152
|
+
summary: "Delete a site-wide channel",
|
|
153
|
+
path: "/api/v1/admin/notification-channels/{channelId}",
|
|
154
|
+
request: { params: z.object({ channelId: z.string() }) },
|
|
155
|
+
responses: {
|
|
156
|
+
204: { description: "Deleted site-wide notification channel" },
|
|
157
|
+
...notFound$1
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
/** Register the site-admin (project-less, `sys:*`) channel endpoints. */
|
|
161
|
+
function registerSystemChannels(app) {
|
|
162
|
+
app.openapi(listSystemRoute, async (c) => {
|
|
163
|
+
requireSiteAdmin(c);
|
|
164
|
+
const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).listSystem();
|
|
165
|
+
return c.json(rows.map((row) => toPublic(row)));
|
|
166
|
+
});
|
|
167
|
+
app.openapi(createSystemRoute, async (c) => {
|
|
168
|
+
requireSiteAdmin(c);
|
|
169
|
+
const body = c.req.valid("json");
|
|
170
|
+
const valid = validateChannelInput(body.provider, body.config);
|
|
171
|
+
if (!valid.ok) return c.json({ message: valid.message }, 400);
|
|
172
|
+
const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
|
|
173
|
+
projectId: null,
|
|
174
|
+
...body
|
|
175
|
+
});
|
|
176
|
+
return c.json(toPublic(row), 201);
|
|
177
|
+
});
|
|
178
|
+
app.openapi(deleteSystemRoute, async (c) => {
|
|
179
|
+
requireSiteAdmin(c);
|
|
180
|
+
const { channelId } = c.req.valid("param");
|
|
181
|
+
const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
|
|
182
|
+
if ((await model.get(channelId))?.projectId !== null) notFound("Notification channel not found");
|
|
183
|
+
await model.remove(channelId);
|
|
184
|
+
return c.body(null, 204);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { registerNotificationChannels, registerSystemChannels };
|
|
189
|
+
|
|
190
|
+
//# sourceMappingURL=notification-channels.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Validate channel input against the provider descriptor. */\nfunction validateChannelInput(\n provider: string,\n config: unknown,\n): { ok: true } | { ok: false; message: string } {\n const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);\n if (!descriptor) {\n return { ok: false, message: `Unknown provider: ${provider}` };\n }\n const parsed = descriptor.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.message };\n }\n return { ok: true };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { resolveAuthorizedProject, unauthorized } from "./helpers.mjs";
|
|
6
|
+
import { notFound, notificationSubscriptionPutSchema, notificationSubscriptionSchema, unauthorized as unauthorized$1 } from "./schemas.mjs";
|
|
7
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
|
+
import { NotificationSubscriptionModel } from "@storyshelf/core/models";
|
|
9
|
+
//#region src/routers/notification-subscriptions.ts
|
|
10
|
+
const VIEW_ROLES = [
|
|
11
|
+
"viewer",
|
|
12
|
+
"developer",
|
|
13
|
+
"approver",
|
|
14
|
+
"admin"
|
|
15
|
+
];
|
|
16
|
+
const ADMIN_ROLES = ["admin"];
|
|
17
|
+
/** Public subscription view (event/via lists decoded). */
|
|
18
|
+
function toPublic(row) {
|
|
19
|
+
return {
|
|
20
|
+
projectId: row.projectId,
|
|
21
|
+
userId: row.userId,
|
|
22
|
+
events: NotificationSubscriptionModel.eventsOf(row),
|
|
23
|
+
via: NotificationSubscriptionModel.viaOf(row),
|
|
24
|
+
enabled: row.enabled
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Require a session user (subscriptions are meaningless without identity). */
|
|
28
|
+
function currentUserId() {
|
|
29
|
+
const user = getStore().user;
|
|
30
|
+
if (!user) unauthorized();
|
|
31
|
+
return user.id;
|
|
32
|
+
}
|
|
33
|
+
const getSubscriptionRoute = createRoute({
|
|
34
|
+
method: "get",
|
|
35
|
+
tags: ["Notifications"],
|
|
36
|
+
summary: "Get my notification subscription",
|
|
37
|
+
path: "/api/v1/projects/{slug}/notifications/me",
|
|
38
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
39
|
+
responses: {
|
|
40
|
+
200: {
|
|
41
|
+
content: { "application/json": { schema: notificationSubscriptionSchema.nullable() } },
|
|
42
|
+
description: "Your notification subscription (null when never opted in)"
|
|
43
|
+
},
|
|
44
|
+
...notFound,
|
|
45
|
+
...unauthorized$1
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const putSubscriptionRoute = createRoute({
|
|
49
|
+
method: "put",
|
|
50
|
+
tags: ["Notifications"],
|
|
51
|
+
summary: "Save my notification subscription",
|
|
52
|
+
path: "/api/v1/projects/{slug}/notifications/me",
|
|
53
|
+
request: {
|
|
54
|
+
params: z.object({ slug: z.string() }),
|
|
55
|
+
body: { content: { "application/json": { schema: notificationSubscriptionPutSchema } } }
|
|
56
|
+
},
|
|
57
|
+
responses: {
|
|
58
|
+
200: {
|
|
59
|
+
content: { "application/json": { schema: notificationSubscriptionSchema } },
|
|
60
|
+
description: "Saved notification subscription"
|
|
61
|
+
},
|
|
62
|
+
...notFound,
|
|
63
|
+
...unauthorized$1
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const deleteSubscriptionRoute = createRoute({
|
|
67
|
+
method: "delete",
|
|
68
|
+
tags: ["Notifications"],
|
|
69
|
+
summary: "Remove my notification subscription",
|
|
70
|
+
path: "/api/v1/projects/{slug}/notifications/me",
|
|
71
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
72
|
+
responses: {
|
|
73
|
+
204: { description: "Removed notification subscription (opted out)" },
|
|
74
|
+
...notFound,
|
|
75
|
+
...unauthorized$1
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const listSubscriptionsRoute = createRoute({
|
|
79
|
+
method: "get",
|
|
80
|
+
tags: ["Notifications"],
|
|
81
|
+
summary: "List notification subscriptions",
|
|
82
|
+
path: "/api/v1/projects/{slug}/notifications",
|
|
83
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
84
|
+
responses: {
|
|
85
|
+
200: {
|
|
86
|
+
content: { "application/json": { schema: notificationSubscriptionSchema.array() } },
|
|
87
|
+
description: "List notification subscriptions (admin audit)"
|
|
88
|
+
},
|
|
89
|
+
...notFound,
|
|
90
|
+
...unauthorized$1
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/** Register the self-service subscription endpoints plus the admin audit list. */
|
|
94
|
+
function registerNotificationSubscriptions(app) {
|
|
95
|
+
app.openapi(getSubscriptionRoute, async (c) => {
|
|
96
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
97
|
+
const row = await new NotificationSubscriptionModel(getStore().db).getFor(project.id, currentUserId());
|
|
98
|
+
return c.json(row ? toPublic(row) : null);
|
|
99
|
+
});
|
|
100
|
+
app.openapi(putSubscriptionRoute, async (c) => {
|
|
101
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
102
|
+
const body = c.req.valid("json");
|
|
103
|
+
const row = await new NotificationSubscriptionModel(getStore().db).upsert(project.id, currentUserId(), body);
|
|
104
|
+
return c.json(toPublic(row));
|
|
105
|
+
});
|
|
106
|
+
app.openapi(deleteSubscriptionRoute, async (c) => {
|
|
107
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
108
|
+
await new NotificationSubscriptionModel(getStore().db).remove(project.id, currentUserId());
|
|
109
|
+
return c.body(null, 204);
|
|
110
|
+
});
|
|
111
|
+
app.openapi(listSubscriptionsRoute, async (c) => {
|
|
112
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
113
|
+
const rows = await new NotificationSubscriptionModel(getStore().db).list(project.id);
|
|
114
|
+
return c.json(rows.map((row) => toPublic(row)));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { registerNotificationSubscriptions };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=notification-subscriptions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-subscriptions.mjs","names":["notFoundResponse","unauthorizedResponse"],"sources":["../../src/routers/notification-subscriptions.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { NotificationSubscriptionRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, unauthorized } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationSubscriptionPutSchema,\n notificationSubscriptionSchema,\n unauthorized as unauthorizedResponse,\n} from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public subscription view (event/via lists decoded). */\nfunction toPublic(row: NotificationSubscriptionRow): {\n projectId: string;\n userId: string;\n events: string[];\n via: string[];\n enabled: boolean;\n} {\n return {\n projectId: row.projectId,\n userId: row.userId,\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Require a session user (subscriptions are meaningless without identity). */\nfunction currentUserId(): string {\n const user = getStore().user;\n if (!user) {\n unauthorized();\n }\n return user.id;\n}\n\nconst getSubscriptionRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"Get my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema.nullable() } },\n description: \"Your notification subscription (null when never opted in)\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst putSubscriptionRoute = createRoute({\n method: \"put\",\n tags: [\"Notifications\"],\n summary: \"Save my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationSubscriptionPutSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema } },\n description: \"Saved notification subscription\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst deleteSubscriptionRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Remove my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 204: { description: \"Removed notification subscription (opted out)\" },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst listSubscriptionsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification subscriptions\",\n path: \"/api/v1/projects/{slug}/notifications\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema.array() } },\n description: \"List notification subscriptions (admin audit)\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\n/** Register the self-service subscription endpoints plus the admin audit list. */\nexport function registerNotificationSubscriptions(app: ShelfRouter): void {\n app.openapi(getSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const row = await new NotificationSubscriptionModel(getStore().db).getFor(\n project.id,\n currentUserId(),\n );\n return c.json(row ? toPublic(row) : null);\n });\n\n app.openapi(putSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const body = c.req.valid(\"json\");\n const row = await new NotificationSubscriptionModel(getStore().db).upsert(\n project.id,\n currentUserId(),\n body,\n );\n return c.json(toPublic(row));\n });\n\n app.openapi(deleteSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n await new NotificationSubscriptionModel(getStore().db).remove(project.id, currentUserId());\n return c.body(null, 204);\n });\n\n app.openapi(listSubscriptionsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationSubscriptionModel(getStore().db).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n}\n"],"mappings":"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"}
|