@headless-lms/server 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/app/community-mail.d.ts +10 -10
- package/dist/app/community-mail.js +72 -28
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +48 -10
- package/dist/app/container.js +268 -48
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.d.ts +47 -0
- package/dist/app/notification-channels.js +175 -0
- package/dist/app/notification-channels.js.map +1 -0
- package/dist/app/notification-reads.d.ts +23 -0
- package/dist/app/notification-reads.js +74 -0
- package/dist/app/notification-reads.js.map +1 -0
- package/dist/app/platform.d.ts +14 -0
- package/dist/app/platform.js +14 -0
- package/dist/app/platform.js.map +1 -0
- package/dist/http/config.d.ts +4 -0
- package/dist/http/errors.d.ts +1 -0
- package/dist/http/errors.js +24 -4
- package/dist/http/errors.js.map +1 -1
- package/dist/http/fastify.d.ts +8 -0
- package/dist/http/mcp/catalogue.d.ts +11 -9
- package/dist/http/mcp/catalogue.js +15 -21
- package/dist/http/mcp/catalogue.js.map +1 -1
- package/dist/http/mcp/challenge.js +3 -3
- package/dist/http/mcp/challenge.js.map +1 -1
- package/dist/http/mcp/route.js +7 -7
- package/dist/http/mcp/route.js.map +1 -1
- package/dist/http/mcp/test-support.d.ts +6 -5
- package/dist/http/mcp/test-support.js +32 -11
- package/dist/http/mcp/test-support.js.map +1 -1
- package/dist/http/mcp/tool.d.ts +3 -9
- package/dist/http/mcp/tool.js +14 -6
- package/dist/http/mcp/tool.js.map +1 -1
- package/dist/http/mcp/tools/automations.js +10 -20
- package/dist/http/mcp/tools/automations.js.map +1 -1
- package/dist/http/mcp/tools/catalog.js +25 -42
- package/dist/http/mcp/tools/catalog.js.map +1 -1
- package/dist/http/mcp/tools/comments.js +9 -19
- package/dist/http/mcp/tools/comments.js.map +1 -1
- package/dist/http/mcp/tools/communities.js +16 -31
- package/dist/http/mcp/tools/communities.js.map +1 -1
- package/dist/http/mcp/tools/community-members.js +9 -16
- package/dist/http/mcp/tools/community-members.js.map +1 -1
- package/dist/http/mcp/tools/community-moderation.js +13 -25
- package/dist/http/mcp/tools/community-moderation.js.map +1 -1
- package/dist/http/mcp/tools/courses.js +11 -21
- package/dist/http/mcp/tools/courses.js.map +1 -1
- package/dist/http/mcp/tools/events.d.ts +6 -0
- package/dist/http/mcp/tools/events.js +170 -0
- package/dist/http/mcp/tools/events.js.map +1 -0
- package/dist/http/mcp/tools/index.d.ts +5 -1
- package/dist/http/mcp/tools/index.js +45 -10
- package/dist/http/mcp/tools/index.js.map +1 -1
- package/dist/http/mcp/tools/organization.d.ts +6 -0
- package/dist/http/mcp/tools/organization.js +214 -0
- package/dist/http/mcp/tools/organization.js.map +1 -0
- package/dist/http/mcp/tools/people.js +30 -49
- package/dist/http/mcp/tools/people.js.map +1 -1
- package/dist/http/plugins/auth.js +37 -8
- package/dist/http/plugins/auth.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +5 -1
- package/dist/http/plugins/error-handler.js +13 -3
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +18 -2
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/plugins/platform.d.ts +7 -0
- package/dist/http/plugins/platform.js +13 -0
- package/dist/http/plugins/platform.js.map +1 -0
- package/dist/http/routes/access.js +54 -36
- package/dist/http/routes/access.js.map +1 -1
- package/dist/http/routes/activities.js +45 -45
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +30 -65
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +37 -42
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.js +33 -36
- package/dist/http/routes/bundles.js.map +1 -1
- package/dist/http/routes/communities.js +144 -136
- package/dist/http/routes/communities.js.map +1 -1
- package/dist/http/routes/community-moderation.js +83 -137
- package/dist/http/routes/community-moderation.js.map +1 -1
- package/dist/http/routes/course-analytics.js +32 -15
- package/dist/http/routes/course-analytics.js.map +1 -1
- package/dist/http/routes/courses.js +25 -32
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +9 -9
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +21 -39
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +41 -47
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/entitlements.js +19 -23
- package/dist/http/routes/entitlements.js.map +1 -1
- package/dist/http/routes/events.d.ts +7 -0
- package/dist/http/routes/events.js +333 -0
- package/dist/http/routes/events.js.map +1 -0
- package/dist/http/routes/integrations.js +40 -73
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/member/assets.js +10 -13
- package/dist/http/routes/member/assets.js.map +1 -1
- package/dist/http/routes/member/chats.js +177 -75
- package/dist/http/routes/member/chats.js.map +1 -1
- package/dist/http/routes/member/comments.js +65 -26
- package/dist/http/routes/member/comments.js.map +1 -1
- package/dist/http/routes/member/community.js +68 -51
- package/dist/http/routes/member/community.js.map +1 -1
- package/dist/http/routes/member/courses.js +12 -19
- package/dist/http/routes/member/courses.js.map +1 -1
- package/dist/http/routes/member/devices.d.ts +7 -0
- package/dist/http/routes/member/devices.js +97 -0
- package/dist/http/routes/member/devices.js.map +1 -0
- package/dist/http/routes/member/downloads.js +6 -9
- package/dist/http/routes/member/downloads.js.map +1 -1
- package/dist/http/routes/member/events.d.ts +7 -0
- package/dist/http/routes/member/events.js +386 -0
- package/dist/http/routes/member/events.js.map +1 -0
- package/dist/http/routes/member/files.js +71 -46
- package/dist/http/routes/member/files.js.map +1 -1
- package/dist/http/routes/member/index.js +6 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.d.ts +7 -0
- package/dist/http/routes/member/notification-settings.js +100 -0
- package/dist/http/routes/member/notification-settings.js.map +1 -0
- package/dist/http/routes/member/notifications.js +69 -45
- package/dist/http/routes/member/notifications.js.map +1 -1
- package/dist/http/routes/member/org.js +5 -15
- package/dist/http/routes/member/org.js.map +1 -1
- package/dist/http/routes/member/posts.js +94 -95
- package/dist/http/routes/member/posts.js.map +1 -1
- package/dist/http/routes/member/profile.js +15 -6
- package/dist/http/routes/member/profile.js.map +1 -1
- package/dist/http/routes/member/progress.js +7 -11
- package/dist/http/routes/member/progress.js.map +1 -1
- package/dist/http/routes/organizations.js +415 -110
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/platform.d.ts +7 -0
- package/dist/http/routes/platform.js +61 -0
- package/dist/http/routes/platform.js.map +1 -0
- package/dist/http/routes/students.js +12 -75
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.js +30 -39
- package/dist/http/routes/webhooks.js.map +1 -1
- package/dist/http/routes.js +9 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/access.d.ts +346 -64
- package/dist/http/schemas/access.js +39 -12
- package/dist/http/schemas/access.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +13 -8
- package/dist/http/schemas/assets.js +6 -5
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +4 -8
- package/dist/http/schemas/chats.d.ts +238 -36
- package/dist/http/schemas/chats.js +14 -7
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +140 -33
- package/dist/http/schemas/community.js +25 -9
- package/dist/http/schemas/community.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +51 -1
- package/dist/http/schemas/course-analytics.js +19 -1
- package/dist/http/schemas/course-analytics.js.map +1 -1
- package/dist/http/schemas/dashboard.d.ts +1 -0
- package/dist/http/schemas/dashboard.js +2 -1
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +45 -73
- package/dist/http/schemas/discussion.js +16 -4
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +9 -12
- package/dist/http/schemas/entitlements.js +11 -5
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/events.d.ts +527 -0
- package/dist/http/schemas/events.js +64 -0
- package/dist/http/schemas/events.js.map +1 -0
- package/dist/http/schemas/files.d.ts +15 -5
- package/dist/http/schemas/files.js +10 -2
- package/dist/http/schemas/files.js.map +1 -1
- package/dist/http/schemas/index.d.ts +14 -12
- package/dist/http/schemas/index.js +14 -12
- package/dist/http/schemas/invites.d.ts +3 -47
- package/dist/http/schemas/invites.js +4 -25
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/member.d.ts +94 -7
- package/dist/http/schemas/member.js +7 -3
- package/dist/http/schemas/member.js.map +1 -1
- package/dist/http/schemas/members.d.ts +329 -65
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +502 -175
- package/dist/http/schemas/notifications.js +148 -28
- package/dist/http/schemas/notifications.js.map +1 -1
- package/dist/http/schemas/platform.d.ts +37 -0
- package/dist/http/schemas/platform.js +19 -0
- package/dist/http/schemas/platform.js.map +1 -0
- package/dist/http/schemas/posts.d.ts +92 -38
- package/dist/http/schemas/posts.js +4 -4
- package/dist/http/schemas/posts.js.map +1 -1
- package/dist/http/schemas/profile.d.ts +1 -1
- package/dist/http/schemas/shared.d.ts +10 -13
- package/dist/http/schemas/shared.js +3 -11
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +22 -0
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -12
- package/dist/http/scope.js +30 -35
- package/dist/http/scope.js.map +1 -1
- package/dist/http/server.js +9 -0
- package/dist/http/server.js.map +1 -1
- package/dist/http/shutdown.d.ts +11 -0
- package/dist/http/shutdown.js +40 -0
- package/dist/http/shutdown.js.map +1 -0
- package/dist/http/sse.d.ts +19 -0
- package/dist/http/sse.js +104 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/http/test-caller.d.ts +13 -0
- package/dist/http/test-caller.js +14 -0
- package/dist/http/test-caller.js.map +1 -0
- package/dist/http/well-known.js +2 -2
- package/dist/http/well-known.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packages/server/package.js +1 -1
- package/package.json +7 -6
- package/dist/http/routes/member/actor.d.ts +0 -14
- package/dist/http/routes/member/actor.js +0 -11
- package/dist/http/routes/member/actor.js.map +0 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { PageQuery, refusals } from "../schemas/shared.js";
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
|
+
import { AddEventAsset, AddEventAttendee, CreateEvent, Event, EventAnalytics, EventAssetParams, EventAttendeeParams, EventAttendeesPage, EventFile, EventIdParam, EventsPage, EventsQuery, RenameEventAsset, ReorderEventAssets, UpdateEvent } from "../schemas/events.js";
|
|
4
|
+
import "../schemas/index.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/http/routes/events.ts
|
|
7
|
+
async function eventsRoutes(app, container) {
|
|
8
|
+
const r = app.withTypeProvider();
|
|
9
|
+
const content = container.content;
|
|
10
|
+
r.route({
|
|
11
|
+
method: "GET",
|
|
12
|
+
url: "/api/events",
|
|
13
|
+
preHandler: app.requireOrgSession,
|
|
14
|
+
schema: {
|
|
15
|
+
operationId: "listEvents",
|
|
16
|
+
tags: ["Events"],
|
|
17
|
+
summary: "List events",
|
|
18
|
+
description: `Requires \`events:read\`. Every event the caller holds \`events:read\` on, drafts and canceled included. \`from\` and \`to\` keep the events that overlap the range.`,
|
|
19
|
+
querystring: EventsQuery,
|
|
20
|
+
response: {
|
|
21
|
+
200: EventsPage,
|
|
22
|
+
...refusals(400, 403)
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handler: async (req) => {
|
|
26
|
+
const actor = await resolveActor(container, req);
|
|
27
|
+
return content.listEvents(req.query, actor);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
r.route({
|
|
31
|
+
method: "GET",
|
|
32
|
+
url: "/api/events/:eventId",
|
|
33
|
+
preHandler: app.requireOrgSession,
|
|
34
|
+
schema: {
|
|
35
|
+
operationId: "getEvent",
|
|
36
|
+
tags: ["Events"],
|
|
37
|
+
summary: "Get an event",
|
|
38
|
+
description: "Requires `events:read`.",
|
|
39
|
+
params: EventIdParam,
|
|
40
|
+
response: {
|
|
41
|
+
200: Event,
|
|
42
|
+
...refusals(403, 404)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
handler: async (req) => {
|
|
46
|
+
const actor = await resolveActor(container, req);
|
|
47
|
+
return content.getEvent(req.params.eventId, actor);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
r.route({
|
|
51
|
+
method: "POST",
|
|
52
|
+
url: "/api/events",
|
|
53
|
+
preHandler: app.requireOrgSession,
|
|
54
|
+
schema: {
|
|
55
|
+
operationId: "createEvent",
|
|
56
|
+
tags: ["Events"],
|
|
57
|
+
summary: "Create an event",
|
|
58
|
+
description: `Requires \`events:write\`. The event starts as a draft. 400 when it ends before it starts.`,
|
|
59
|
+
body: CreateEvent,
|
|
60
|
+
response: {
|
|
61
|
+
201: Event,
|
|
62
|
+
...refusals(400, 403)
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
handler: async (req, reply) => {
|
|
66
|
+
const actor = await resolveActor(container, req);
|
|
67
|
+
const event = await content.createEvent(req.body, actor);
|
|
68
|
+
return reply.code(201).send(event);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
r.route({
|
|
72
|
+
method: "PATCH",
|
|
73
|
+
url: "/api/events/:eventId",
|
|
74
|
+
preHandler: app.requireOrgSession,
|
|
75
|
+
schema: {
|
|
76
|
+
operationId: "updateEvent",
|
|
77
|
+
tags: ["Events"],
|
|
78
|
+
summary: "Update an event",
|
|
79
|
+
description: `Requires \`events:write\`. Changes only the fields sent; \`status\` publishes or unpublishes it. \`thumbnailAssetId\` must be a ready library image (409 \`conflict\` otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \`media:read\` (403 otherwise). 409 \`canceled\` when the event is canceled.`,
|
|
80
|
+
params: EventIdParam,
|
|
81
|
+
body: UpdateEvent,
|
|
82
|
+
response: {
|
|
83
|
+
200: Event,
|
|
84
|
+
...refusals(400, 403, 404, 409)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
handler: async (req) => {
|
|
88
|
+
const actor = await resolveActor(container, req);
|
|
89
|
+
return content.updateEvent(req.params.eventId, req.body, actor);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
r.route({
|
|
93
|
+
method: "POST",
|
|
94
|
+
url: "/api/events/:eventId/cancel",
|
|
95
|
+
preHandler: app.requireOrgSession,
|
|
96
|
+
schema: {
|
|
97
|
+
operationId: "cancelEvent",
|
|
98
|
+
tags: ["Events"],
|
|
99
|
+
summary: "Cancel an event",
|
|
100
|
+
description: `Requires \`events:write\`. Canceling is final. 409 \`canceled\` when it is already canceled.`,
|
|
101
|
+
params: EventIdParam,
|
|
102
|
+
response: {
|
|
103
|
+
200: Event,
|
|
104
|
+
...refusals(403, 404, 409)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
handler: async (req) => {
|
|
108
|
+
const actor = await resolveActor(container, req);
|
|
109
|
+
return content.cancelEvent(req.params.eventId, actor);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
r.route({
|
|
113
|
+
method: "DELETE",
|
|
114
|
+
url: "/api/events/:eventId",
|
|
115
|
+
preHandler: app.requireOrgSession,
|
|
116
|
+
schema: {
|
|
117
|
+
operationId: "deleteEvent",
|
|
118
|
+
tags: ["Events"],
|
|
119
|
+
summary: "Delete an event",
|
|
120
|
+
description: `Requires \`events:delete\`. Its attendees lose it; its library files survive.`,
|
|
121
|
+
params: EventIdParam,
|
|
122
|
+
response: {
|
|
123
|
+
204: z.void(),
|
|
124
|
+
...refusals(403, 404)
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
handler: async (req, reply) => {
|
|
128
|
+
const actor = await resolveActor(container, req);
|
|
129
|
+
await content.deleteEvent(req.params.eventId, actor);
|
|
130
|
+
return reply.code(204).send();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
r.route({
|
|
134
|
+
method: "GET",
|
|
135
|
+
url: "/api/events/:eventId/assets",
|
|
136
|
+
preHandler: app.requireOrgSession,
|
|
137
|
+
schema: {
|
|
138
|
+
operationId: "listEventAssets",
|
|
139
|
+
tags: ["Events"],
|
|
140
|
+
summary: "List an event's files in order",
|
|
141
|
+
description: "Requires `events:read`.",
|
|
142
|
+
params: EventIdParam,
|
|
143
|
+
response: {
|
|
144
|
+
200: z.array(EventFile),
|
|
145
|
+
...refusals(403, 404)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
handler: async (req) => {
|
|
149
|
+
const actor = await resolveActor(container, req);
|
|
150
|
+
return content.listEventAssets(req.params.eventId, actor);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
r.route({
|
|
154
|
+
method: "POST",
|
|
155
|
+
url: "/api/events/:eventId/assets",
|
|
156
|
+
preHandler: app.requireOrgSession,
|
|
157
|
+
schema: {
|
|
158
|
+
operationId: "addEventAsset",
|
|
159
|
+
tags: ["Events"],
|
|
160
|
+
summary: "Attach a file to an event",
|
|
161
|
+
description: `Requires \`events:write\`. Added last. 409 when the file is not a ready library file or is already attached. A file not uploaded for the event needs \`media:read\` (403 otherwise). Returns the event's files.`,
|
|
162
|
+
params: EventIdParam,
|
|
163
|
+
body: AddEventAsset,
|
|
164
|
+
response: {
|
|
165
|
+
200: z.array(EventFile),
|
|
166
|
+
...refusals(403, 404, 409)
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
handler: async (req) => {
|
|
170
|
+
const actor = await resolveActor(container, req);
|
|
171
|
+
return content.addEventAsset(req.params.eventId, req.body, actor);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
r.route({
|
|
175
|
+
method: "PUT",
|
|
176
|
+
url: "/api/events/:eventId/assets/order",
|
|
177
|
+
preHandler: app.requireOrgSession,
|
|
178
|
+
schema: {
|
|
179
|
+
operationId: "reorderEventAssets",
|
|
180
|
+
tags: ["Events"],
|
|
181
|
+
summary: "Reorder an event's files",
|
|
182
|
+
description: `Requires \`events:write\`. Send every attached file id exactly once (409 otherwise). Returns the event's files.`,
|
|
183
|
+
params: EventIdParam,
|
|
184
|
+
body: ReorderEventAssets,
|
|
185
|
+
response: {
|
|
186
|
+
200: z.array(EventFile),
|
|
187
|
+
...refusals(403, 404, 409)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
handler: async (req) => {
|
|
191
|
+
const actor = await resolveActor(container, req);
|
|
192
|
+
return content.reorderEventAssets(req.params.eventId, req.body.assetIds, actor);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
r.route({
|
|
196
|
+
method: "PATCH",
|
|
197
|
+
url: "/api/events/:eventId/assets/:assetId",
|
|
198
|
+
preHandler: app.requireOrgSession,
|
|
199
|
+
schema: {
|
|
200
|
+
operationId: "renameEventAsset",
|
|
201
|
+
tags: ["Events"],
|
|
202
|
+
summary: "Set a file's display name within an event",
|
|
203
|
+
description: `Requires \`events:write\`. Null falls back to the filename. Returns the event's files.`,
|
|
204
|
+
params: EventAssetParams,
|
|
205
|
+
body: RenameEventAsset,
|
|
206
|
+
response: {
|
|
207
|
+
200: z.array(EventFile),
|
|
208
|
+
...refusals(403, 404)
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
handler: async (req) => {
|
|
212
|
+
const actor = await resolveActor(container, req);
|
|
213
|
+
return content.renameEventAsset(req.params.eventId, req.params.assetId, req.body.displayName, actor);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
r.route({
|
|
217
|
+
method: "DELETE",
|
|
218
|
+
url: "/api/events/:eventId/assets/:assetId",
|
|
219
|
+
preHandler: app.requireOrgSession,
|
|
220
|
+
schema: {
|
|
221
|
+
operationId: "removeEventAsset",
|
|
222
|
+
tags: ["Events"],
|
|
223
|
+
summary: "Remove a file from an event",
|
|
224
|
+
description: `Requires \`events:write\`. The library file itself survives. Returns the event's files.`,
|
|
225
|
+
params: EventAssetParams,
|
|
226
|
+
response: {
|
|
227
|
+
200: z.array(EventFile),
|
|
228
|
+
...refusals(403, 404)
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
handler: async (req) => {
|
|
232
|
+
const actor = await resolveActor(container, req);
|
|
233
|
+
return content.removeEventAsset(req.params.eventId, req.params.assetId, actor);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
r.route({
|
|
237
|
+
method: "POST",
|
|
238
|
+
url: "/api/events/:eventId/attendees",
|
|
239
|
+
preHandler: app.requireOrgSession,
|
|
240
|
+
schema: {
|
|
241
|
+
operationId: "addEventAttendee",
|
|
242
|
+
tags: ["Events"],
|
|
243
|
+
summary: "Add an attendee to an event",
|
|
244
|
+
description: "Requires `access:write`, or `events:write` on the event or the org. Grants the event to the person, with source `manual`. Adding someone who already attends changes nothing. 404 when the event or the person does not exist; 409 `not_published`, `ended` or `full`.",
|
|
245
|
+
params: EventIdParam,
|
|
246
|
+
body: AddEventAttendee,
|
|
247
|
+
response: {
|
|
248
|
+
204: z.void(),
|
|
249
|
+
...refusals(400, 403, 404, 409)
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
handler: async (req, reply) => {
|
|
253
|
+
const actor = await resolveActor(container, req);
|
|
254
|
+
await container.entitlements.grant({
|
|
255
|
+
orgUserId: req.body.orgUserId,
|
|
256
|
+
contentId: req.params.eventId,
|
|
257
|
+
contentType: "event",
|
|
258
|
+
expiresAt: null
|
|
259
|
+
}, actor);
|
|
260
|
+
return reply.code(204).send();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
r.route({
|
|
264
|
+
method: "DELETE",
|
|
265
|
+
url: "/api/events/:eventId/attendees/:orgUserId",
|
|
266
|
+
preHandler: app.requireOrgSession,
|
|
267
|
+
schema: {
|
|
268
|
+
operationId: "removeEventAttendee",
|
|
269
|
+
tags: ["Events"],
|
|
270
|
+
summary: "Remove an attendee from an event",
|
|
271
|
+
description: "Requires `access:write`, or `events:write` on the event or the org. Revokes the person's direct grant to the event. 404 when they have no direct active grant; someone who attends only through a bundle has none.",
|
|
272
|
+
params: EventAttendeeParams,
|
|
273
|
+
response: {
|
|
274
|
+
204: z.void(),
|
|
275
|
+
...refusals(403, 404)
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
handler: async (req, reply) => {
|
|
279
|
+
const actor = await resolveActor(container, req);
|
|
280
|
+
await container.entitlements.revoke({
|
|
281
|
+
orgUserId: req.params.orgUserId,
|
|
282
|
+
contentId: req.params.eventId,
|
|
283
|
+
contentType: "event"
|
|
284
|
+
}, actor);
|
|
285
|
+
return reply.code(204).send();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
r.route({
|
|
289
|
+
method: "GET",
|
|
290
|
+
url: "/api/events/:eventId/attendees",
|
|
291
|
+
preHandler: app.requireOrgSession,
|
|
292
|
+
schema: {
|
|
293
|
+
operationId: "listEventAttendees",
|
|
294
|
+
tags: ["Events"],
|
|
295
|
+
summary: "List an event's attendees",
|
|
296
|
+
description: `Requires \`events:read\`. On the event or the org. The people attending, directly or through a bundle, by grant date. \`source\` is \`bundle\` for someone who attends only through a bundle.`,
|
|
297
|
+
params: EventIdParam,
|
|
298
|
+
querystring: PageQuery,
|
|
299
|
+
response: {
|
|
300
|
+
200: EventAttendeesPage,
|
|
301
|
+
...refusals(400, 403, 404)
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
handler: async (req) => {
|
|
305
|
+
const actor = await resolveActor(container, req);
|
|
306
|
+
return container.reporting.admin.eventAttendees(req.params.eventId, req.query, actor);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
r.route({
|
|
310
|
+
method: "GET",
|
|
311
|
+
url: "/api/events/:eventId/analytics",
|
|
312
|
+
preHandler: app.requireOrgSession,
|
|
313
|
+
schema: {
|
|
314
|
+
operationId: "getEventAnalytics",
|
|
315
|
+
tags: ["Events"],
|
|
316
|
+
summary: "An event's attendance figures",
|
|
317
|
+
description: `Requires \`events:read\`. On the event or the org. The attendees now and by source, the capacity, the revoked direct grants, and the current direct grants by grant date before the start.`,
|
|
318
|
+
params: EventIdParam,
|
|
319
|
+
response: {
|
|
320
|
+
200: EventAnalytics,
|
|
321
|
+
...refusals(403, 404)
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
handler: async (req) => {
|
|
325
|
+
const actor = await resolveActor(container, req);
|
|
326
|
+
return container.reporting.admin.eventAnalytics(req.params.eventId, actor);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
export { eventsRoutes };
|
|
332
|
+
|
|
333
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../../src/http/routes/events.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AddEventAsset,\n AddEventAttendee,\n CreateEvent,\n Event,\n EventAnalytics,\n EventAttendeesPage,\n EventAssetParams,\n EventAttendeeParams,\n EventFile,\n EventIdParam,\n EventsPage,\n EventsQuery,\n PageQuery,\n RenameEventAsset,\n ReorderEventAssets,\n UpdateEvent,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function eventsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEvents',\n tags: ['Events'],\n summary: 'List events',\n description: `Requires \\`events:read\\`. Every event the caller holds \\`events:read\\` on, drafts and canceled included. \\`from\\` and \\`to\\` keep the events that overlap the range.`,\n querystring: EventsQuery,\n response: { 200: EventsPage, ...refusals(400, 403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listEvents(req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEvent',\n tags: ['Events'],\n summary: 'Get an event',\n description: 'Requires `events:read`.',\n params: EventIdParam,\n response: { 200: Event, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.getEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createEvent',\n tags: ['Events'],\n summary: 'Create an event',\n description: `Requires \\`events:write\\`. The event starts as a draft. 400 when it ends before it starts.`,\n body: CreateEvent,\n response: { 201: Event, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const event = await content.createEvent(req.body, actor);\n return reply.code(201).send(event);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateEvent',\n tags: ['Events'],\n summary: 'Update an event',\n description: `Requires \\`events:write\\`. Changes only the fields sent; \\`status\\` publishes or unpublishes it. \\`thumbnailAssetId\\` must be a ready library image (409 \\`conflict\\` otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise). 409 \\`canceled\\` when the event is canceled.`,\n params: EventIdParam,\n body: UpdateEvent,\n response: { 200: Event, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateEvent(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/cancel',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'cancelEvent',\n tags: ['Events'],\n summary: 'Cancel an event',\n description: `Requires \\`events:write\\`. Canceling is final. 409 \\`canceled\\` when it is already canceled.`,\n params: EventIdParam,\n response: { 200: Event, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.cancelEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteEvent',\n tags: ['Events'],\n summary: 'Delete an event',\n description: `Requires \\`events:delete\\`. Its attendees lose it; its library files survive.`,\n params: EventIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.deleteEvent(req.params.eventId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEventAssets',\n tags: ['Events'],\n summary: \"List an event's files in order\",\n description: 'Requires `events:read`.',\n params: EventIdParam,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listEventAssets(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addEventAsset',\n tags: ['Events'],\n summary: 'Attach a file to an event',\n description: `Requires \\`events:write\\`. Added last. 409 when the file is not a ready library file or is already attached. A file not uploaded for the event needs \\`media:read\\` (403 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: AddEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.addEventAsset(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/events/:eventId/assets/order',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderEventAssets',\n tags: ['Events'],\n summary: \"Reorder an event's files\",\n description: `Requires \\`events:write\\`. Send every attached file id exactly once (409 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: ReorderEventAssets,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.reorderEventAssets(req.params.eventId, req.body.assetIds, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'renameEventAsset',\n tags: ['Events'],\n summary: \"Set a file's display name within an event\",\n description: `Requires \\`events:write\\`. Null falls back to the filename. Returns the event's files.`,\n params: EventAssetParams,\n body: RenameEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.renameEventAsset(\n req.params.eventId,\n req.params.assetId,\n req.body.displayName,\n actor,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeEventAsset',\n tags: ['Events'],\n summary: 'Remove a file from an event',\n description: `Requires \\`events:write\\`. The library file itself survives. Returns the event's files.`,\n params: EventAssetParams,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.removeEventAsset(req.params.eventId, req.params.assetId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addEventAttendee',\n tags: ['Events'],\n summary: 'Add an attendee to an event',\n description:\n 'Requires `access:write`, or `events:write` on the event or the org. Grants the event to the person, with source `manual`. Adding someone who already attends changes nothing. 404 when the event or the person does not exist; 409 `not_published`, `ended` or `full`.',\n params: EventIdParam,\n body: AddEventAttendee,\n response: { 204: z.void(), ...refusals(400, 403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.grant(\n {\n orgUserId: req.body.orgUserId,\n contentId: req.params.eventId,\n contentType: 'event',\n expiresAt: null,\n },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId/attendees/:orgUserId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeEventAttendee',\n tags: ['Events'],\n summary: 'Remove an attendee from an event',\n description:\n \"Requires `access:write`, or `events:write` on the event or the org. Revokes the person's direct grant to the event. 404 when they have no direct active grant; someone who attends only through a bundle has none.\",\n params: EventAttendeeParams,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.revoke(\n { orgUserId: req.params.orgUserId, contentId: req.params.eventId, contentType: 'event' },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEventAttendees',\n tags: ['Events'],\n summary: \"List an event's attendees\",\n description: `Requires \\`events:read\\`. On the event or the org. The people attending, directly or through a bundle, by grant date. \\`source\\` is \\`bundle\\` for someone who attends only through a bundle.`,\n params: EventIdParam,\n querystring: PageQuery,\n response: { 200: EventAttendeesPage, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.eventAttendees(req.params.eventId, req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEventAnalytics',\n tags: ['Events'],\n summary: \"An event's attendance figures\",\n description: `Requires \\`events:read\\`. On the event or the org. The attendees now and by source, the capacity, the revoked direct grants, and the current direct grants by grant date before the start.`,\n params: EventIdParam,\n response: { 200: EventAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.eventAnalytics(req.params.eventId, actor);\n },\n });\n}\n"],"mappings":";;;;;;AAyBA,eAAsB,aAAa,KAAsB,WAAqC;CAC5F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,UAAU,UAAU;CAE1B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAY,GAAG,SAAS,KAAK,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,WAAW,IAAI,OAAO,KAAK;EAC5C;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAO,GAAG,SAAS,KAAK,GAAG;GAAE;EAChD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,SAAS,IAAI,OAAO,SAAS,KAAK;EACnD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAO,GAAG,SAAS,KAAK,GAAG;GAAE;EAChD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,MAAM,QAAQ,YAAY,IAAI,MAAM,KAAK;GACvD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK;EACnC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAO,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC1D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,IAAI,OAAO,SAAS,IAAI,MAAM,KAAK;EAChE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAO,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,IAAI,OAAO,SAAS,KAAK;EACtD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,YAAY,IAAI,OAAO,SAAS,KAAK;GACnD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,MAAM,SAAS;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,gBAAgB,IAAI,OAAO,SAAS,KAAK;EAC1D;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,SAAS;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAClE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,cAAc,IAAI,OAAO,SAAS,IAAI,MAAM,KAAK;EAClE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,SAAS;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAClE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,mBAAmB,IAAI,OAAO,SAAS,IAAI,KAAK,UAAU,KAAK;EAChF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,SAAS;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,iBACb,IAAI,OAAO,SACX,IAAI,OAAO,SACX,IAAI,KAAK,aACT,KACF;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,MAAM,SAAS;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,iBAAiB,IAAI,OAAO,SAAS,IAAI,OAAO,SAAS,KAAK;EAC/E;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aACE;GACF,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,aAAa,MAC3B;IACE,WAAW,IAAI,KAAK;IACpB,WAAW,IAAI,OAAO;IACtB,aAAa;IACb,WAAW;GACb,GACA,KACF;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,aAAa,OAC3B;IAAE,WAAW,IAAI,OAAO;IAAW,WAAW,IAAI,OAAO;IAAS,aAAa;GAAQ,GACvF,KACF;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,aAAa;GACb,UAAU;IAAE,KAAK;IAAoB,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAClE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,eAAe,IAAI,OAAO,SAAS,IAAI,OAAO,KAAK;EACtF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,eAAe,IAAI,OAAO,SAAS,KAAK;EAC3E;CACF,CAAC;AACH"}
|
|
@@ -1,77 +1,61 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { ActionInvocationResult, AvailableIntegrationsList, ConfigureRequest, ConnectRequest, Connection, ConnectionActionParams, ConnectionIdParam, ConnectionsList, InvokeActionRequest, ReconnectRequest } from "../schemas/integrations.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
6
|
//#region src/http/routes/integrations.ts
|
|
8
7
|
function toApi(connection) {
|
|
9
8
|
const { credentialRef: _credentialRef, ...rest } = connection;
|
|
10
9
|
return rest;
|
|
11
10
|
}
|
|
12
|
-
/** Resolve the session's active org to the domain org id, or 400 and return null. */
|
|
13
|
-
async function resolveOrgId(req, reply) {
|
|
14
|
-
if (!req.orgId) {
|
|
15
|
-
await reply.code(400).send({
|
|
16
|
-
error: "no_active_org",
|
|
17
|
-
message: "No active organization"
|
|
18
|
-
});
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
return req.orgId;
|
|
22
|
-
}
|
|
23
11
|
async function integrationsRoutes(app, container) {
|
|
24
12
|
const r = app.withTypeProvider();
|
|
25
13
|
const integrations = container.integrations;
|
|
26
14
|
const tags = ["Integrations"];
|
|
27
|
-
const staff = async (req) => {
|
|
28
|
-
await resolveScope(container, req);
|
|
29
|
-
};
|
|
30
15
|
r.route({
|
|
31
16
|
method: "GET",
|
|
32
17
|
url: "/api/integrations/available",
|
|
33
|
-
preHandler: staff,
|
|
34
18
|
schema: {
|
|
35
19
|
operationId: "listAvailableIntegrations",
|
|
36
20
|
tags,
|
|
37
|
-
summary: "List the integrations this deployment supports
|
|
38
|
-
description:
|
|
21
|
+
summary: "List the integrations this deployment supports",
|
|
22
|
+
description: `Requires \`integrations:read\`. Each with the JSON Schemas of its config, its secrets and its actions.`,
|
|
39
23
|
response: {
|
|
40
24
|
200: AvailableIntegrationsList,
|
|
41
25
|
...refusals(403)
|
|
42
26
|
}
|
|
43
27
|
},
|
|
44
|
-
handler: async () =>
|
|
28
|
+
handler: async (req) => {
|
|
29
|
+
const actor = await resolveActor(container, req);
|
|
30
|
+
return integrations.available(actor);
|
|
31
|
+
}
|
|
45
32
|
});
|
|
46
33
|
r.route({
|
|
47
34
|
method: "GET",
|
|
48
35
|
url: "/api/integrations",
|
|
49
|
-
preHandler: staff,
|
|
50
36
|
schema: {
|
|
51
37
|
operationId: "listConnections",
|
|
52
38
|
tags,
|
|
53
|
-
summary: "List the organization's integration connections
|
|
54
|
-
description:
|
|
39
|
+
summary: "List the organization's integration connections",
|
|
40
|
+
description: `Requires \`integrations:read\`. Secrets are never returned.`,
|
|
55
41
|
response: {
|
|
56
42
|
200: ConnectionsList,
|
|
57
43
|
...refusals(403)
|
|
58
44
|
}
|
|
59
45
|
},
|
|
60
|
-
handler: async (req
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
return (await integrations.list(orgId)).map(toApi);
|
|
46
|
+
handler: async (req) => {
|
|
47
|
+
const actor = await resolveActor(container, req);
|
|
48
|
+
return (await integrations.list(actor)).map(toApi);
|
|
64
49
|
}
|
|
65
50
|
});
|
|
66
51
|
r.route({
|
|
67
52
|
method: "POST",
|
|
68
53
|
url: "/api/integrations",
|
|
69
|
-
preHandler: staff,
|
|
70
54
|
schema: {
|
|
71
55
|
operationId: "connectIntegration",
|
|
72
56
|
tags,
|
|
73
|
-
summary: "Connect an integration
|
|
74
|
-
description:
|
|
57
|
+
summary: "Connect an integration",
|
|
58
|
+
description: `Requires \`integrations:write\`. Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.`,
|
|
75
59
|
body: ConnectRequest,
|
|
76
60
|
response: {
|
|
77
61
|
201: Connection,
|
|
@@ -79,44 +63,38 @@ async function integrationsRoutes(app, container) {
|
|
|
79
63
|
}
|
|
80
64
|
},
|
|
81
65
|
handler: async (req, reply) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const connection = await integrations.connect(orgId, req.body);
|
|
66
|
+
const actor = await resolveActor(container, req);
|
|
67
|
+
const connection = await integrations.connect(req.body, actor);
|
|
85
68
|
return reply.code(201).send(toApi(connection));
|
|
86
69
|
}
|
|
87
70
|
});
|
|
88
71
|
r.route({
|
|
89
72
|
method: "GET",
|
|
90
73
|
url: "/api/integrations/:id",
|
|
91
|
-
preHandler: staff,
|
|
92
74
|
schema: {
|
|
93
75
|
operationId: "getConnection",
|
|
94
76
|
tags,
|
|
95
|
-
summary: "Get a connection
|
|
96
|
-
description:
|
|
77
|
+
summary: "Get a connection",
|
|
78
|
+
description: `Requires \`integrations:read\`. Secrets are never returned.`,
|
|
97
79
|
params: ConnectionIdParam,
|
|
98
80
|
response: {
|
|
99
81
|
200: Connection,
|
|
100
82
|
...refusals(403, 404)
|
|
101
83
|
}
|
|
102
84
|
},
|
|
103
|
-
handler: async (req
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const connection = await integrations.get(orgId, req.params.id);
|
|
107
|
-
if (!connection) throw new NotFoundError("Connection", req.params.id);
|
|
108
|
-
return toApi(connection);
|
|
85
|
+
handler: async (req) => {
|
|
86
|
+
const actor = await resolveActor(container, req);
|
|
87
|
+
return toApi(await integrations.get(req.params.id, actor));
|
|
109
88
|
}
|
|
110
89
|
});
|
|
111
90
|
r.route({
|
|
112
91
|
method: "PATCH",
|
|
113
92
|
url: "/api/integrations/:id",
|
|
114
|
-
preHandler: staff,
|
|
115
93
|
schema: {
|
|
116
94
|
operationId: "configureConnection",
|
|
117
95
|
tags,
|
|
118
|
-
summary: "Change a connection's configuration or active flag
|
|
119
|
-
description:
|
|
96
|
+
summary: "Change a connection's configuration or active flag",
|
|
97
|
+
description: `Requires \`integrations:write\`. 400 when the config is invalid for the integration.`,
|
|
120
98
|
params: ConnectionIdParam,
|
|
121
99
|
body: ConfigureRequest,
|
|
122
100
|
response: {
|
|
@@ -124,23 +102,19 @@ async function integrationsRoutes(app, container) {
|
|
|
124
102
|
...refusals(400, 403, 404)
|
|
125
103
|
}
|
|
126
104
|
},
|
|
127
|
-
handler: async (req
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
const connection = await integrations.configure(orgId, req.params.id, req.body);
|
|
131
|
-
if (!connection) throw new NotFoundError("Connection", req.params.id);
|
|
132
|
-
return toApi(connection);
|
|
105
|
+
handler: async (req) => {
|
|
106
|
+
const actor = await resolveActor(container, req);
|
|
107
|
+
return toApi(await integrations.configure(req.params.id, req.body, actor));
|
|
133
108
|
}
|
|
134
109
|
});
|
|
135
110
|
r.route({
|
|
136
111
|
method: "POST",
|
|
137
112
|
url: "/api/integrations/:id/reconnect",
|
|
138
|
-
preHandler: staff,
|
|
139
113
|
schema: {
|
|
140
114
|
operationId: "reconnectIntegration",
|
|
141
115
|
tags,
|
|
142
|
-
summary: "Replace a connection's secrets
|
|
143
|
-
description:
|
|
116
|
+
summary: "Replace a connection's secrets",
|
|
117
|
+
description: `Requires \`integrations:write\`. The new secrets replace the stored ones.`,
|
|
144
118
|
params: ConnectionIdParam,
|
|
145
119
|
body: ReconnectRequest,
|
|
146
120
|
response: {
|
|
@@ -148,23 +122,19 @@ async function integrationsRoutes(app, container) {
|
|
|
148
122
|
...refusals(403, 404)
|
|
149
123
|
}
|
|
150
124
|
},
|
|
151
|
-
handler: async (req
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);
|
|
155
|
-
if (!connection) throw new NotFoundError("Connection", req.params.id);
|
|
156
|
-
return toApi(connection);
|
|
125
|
+
handler: async (req) => {
|
|
126
|
+
const actor = await resolveActor(container, req);
|
|
127
|
+
return toApi(await integrations.reconnect(req.params.id, req.body.secrets, actor));
|
|
157
128
|
}
|
|
158
129
|
});
|
|
159
130
|
r.route({
|
|
160
131
|
method: "POST",
|
|
161
132
|
url: "/api/integrations/:id/actions/:actionId",
|
|
162
|
-
preHandler: staff,
|
|
163
133
|
schema: {
|
|
164
134
|
operationId: "invokeConnectionAction",
|
|
165
135
|
tags,
|
|
166
|
-
summary: "Invoke one of a connection's actions
|
|
167
|
-
description:
|
|
136
|
+
summary: "Invoke one of a connection's actions",
|
|
137
|
+
description: `Requires \`integrations:write\`. Runs the action, such as a listing that feeds a picker, with \`input\` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.`,
|
|
168
138
|
params: ConnectionActionParams,
|
|
169
139
|
body: InvokeActionRequest,
|
|
170
140
|
response: {
|
|
@@ -172,21 +142,19 @@ async function integrationsRoutes(app, container) {
|
|
|
172
142
|
...refusals(400, 403, 404, 409)
|
|
173
143
|
}
|
|
174
144
|
},
|
|
175
|
-
handler: async (req
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
return { output: await integrations.invoke(orgId, req.params.id, req.params.actionId, req.body.input ?? {}) };
|
|
145
|
+
handler: async (req) => {
|
|
146
|
+
const actor = await resolveActor(container, req);
|
|
147
|
+
return { output: await integrations.invoke(req.params.id, req.params.actionId, req.body.input ?? {}, actor) };
|
|
179
148
|
}
|
|
180
149
|
});
|
|
181
150
|
r.route({
|
|
182
151
|
method: "DELETE",
|
|
183
152
|
url: "/api/integrations/:id",
|
|
184
|
-
preHandler: staff,
|
|
185
153
|
schema: {
|
|
186
154
|
operationId: "disconnectIntegration",
|
|
187
155
|
tags,
|
|
188
|
-
summary: "Disconnect an integration
|
|
189
|
-
description:
|
|
156
|
+
summary: "Disconnect an integration",
|
|
157
|
+
description: `Requires \`integrations:delete\`. Destroys its stored secrets.`,
|
|
190
158
|
params: ConnectionIdParam,
|
|
191
159
|
response: {
|
|
192
160
|
204: z.void(),
|
|
@@ -194,9 +162,8 @@ async function integrationsRoutes(app, container) {
|
|
|
194
162
|
}
|
|
195
163
|
},
|
|
196
164
|
handler: async (req, reply) => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
if (!await integrations.disconnect(orgId, req.params.id)) throw new NotFoundError("Connection", req.params.id);
|
|
165
|
+
const actor = await resolveActor(container, req);
|
|
166
|
+
await integrations.disconnect(req.params.id, actor);
|
|
200
167
|
return reply.code(204).send();
|
|
201
168
|
}
|
|
202
169
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.js","names":[],"sources":["../../../src/http/routes/integrations.ts"],"sourcesContent":["// HTTP routes for integrations (an org's connections to external services).\n// All require a session with an active organization. Credentials are\n// write-only: accepted on connect/reconnect, never present in a response —\n// `toApi` strips the credentialRef so not even the reference leaks.\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActionInvocationResult,\n AvailableIntegrationsList,\n ConfigureRequest,\n ConnectRequest,\n Connection,\n ConnectionActionParams,\n ConnectionIdParam,\n ConnectionsList,\n InvokeActionRequest,\n ReconnectRequest,\n refusals,\n} from '../schemas/index.js';\nimport type { Connection as DomainConnection } from '@headless-lms/core/integrations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nfunction toApi(connection: DomainConnection): Connection {\n const { credentialRef: _credentialRef, ...rest } = connection;\n return rest;\n}\n\n/** Resolve the session's active org to the domain org id, or 400 and return null. */\nasync function resolveOrgId(req: FastifyRequest, reply: FastifyReply): Promise<string | null> {\n if (!req.orgId) {\n await reply.code(400).send({ error: 'no_active_org', message: 'No active organization' });\n return null;\n }\n return req.orgId;\n}\n\nexport async function integrationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const integrations = container.integrations;\n const tags = ['Integrations'];\n const staff = async (req: FastifyRequest) => {\n await resolveScope(container, req);\n };\n\n r.route({\n method: 'GET',\n url: '/api/integrations/available',\n preHandler: staff,\n schema: {\n operationId: 'listAvailableIntegrations',\n tags,\n summary: 'List the integrations this deployment supports (staff)',\n description:\n 'Staff only (owner, admin, instructor). Each with the JSON Schemas of its config, its secrets and its actions.',\n response: { 200: AvailableIntegrationsList, ...refusals(403) },\n },\n handler: async () => integrations.available(),\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations',\n preHandler: staff,\n schema: {\n operationId: 'listConnections',\n tags,\n summary: \"List the organization's integration connections (staff)\",\n description: 'Staff only (owner, admin, instructor). Secrets are never returned.',\n response: { 200: ConnectionsList, ...refusals(403) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n return (await integrations.list(orgId)).map(toApi);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations',\n preHandler: staff,\n schema: {\n operationId: 'connectIntegration',\n tags,\n summary: 'Connect an integration (staff)',\n description:\n 'Staff only (owner, admin, instructor). Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.',\n body: ConnectRequest,\n response: { 201: Connection, ...refusals(400, 403, 409) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.connect(orgId, req.body);\n return reply.code(201).send(toApi(connection));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'getConnection',\n tags,\n summary: 'Get a connection (staff)',\n description: 'Staff only (owner, admin, instructor). Secrets are never returned.',\n params: ConnectionIdParam,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.get(orgId, req.params.id);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'configureConnection',\n tags,\n summary: \"Change a connection's configuration or active flag (staff)\",\n description:\n 'Staff only (owner, admin, instructor). 400 when the config is invalid for the integration.',\n params: ConnectionIdParam,\n body: ConfigureRequest,\n response: { 200: Connection, ...refusals(400, 403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.configure(orgId, req.params.id, req.body);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/reconnect',\n preHandler: staff,\n schema: {\n operationId: 'reconnectIntegration',\n tags,\n summary: \"Replace a connection's secrets (staff)\",\n description:\n 'Staff only (owner, admin, instructor). The new secrets replace the stored ones.',\n params: ConnectionIdParam,\n body: ReconnectRequest,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/actions/:actionId',\n preHandler: staff,\n schema: {\n operationId: 'invokeConnectionAction',\n tags,\n summary: \"Invoke one of a connection's actions (staff)\",\n description:\n 'Staff only (owner, admin, instructor). Runs the action, such as a listing that feeds a picker, with `input` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.',\n params: ConnectionActionParams,\n body: InvokeActionRequest,\n response: { 200: ActionInvocationResult, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const output = await integrations.invoke(\n orgId,\n req.params.id,\n req.params.actionId,\n req.body.input ?? {},\n );\n return { output: output as Record<string, unknown> };\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'disconnectIntegration',\n tags,\n summary: 'Disconnect an integration (staff)',\n description: 'Staff only (owner, admin, instructor). Destroys its stored secrets.',\n params: ConnectionIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const removed = await integrations.disconnect(orgId, req.params.id);\n if (!removed) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"integrations.js","names":[],"sources":["../../../src/http/routes/integrations.ts"],"sourcesContent":["// HTTP routes for integrations (an org's connections to external services).\n// Credentials are write-only: accepted on connect/reconnect, never present in a\n// response — `toApi` strips the credentialRef so not even the reference leaks.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActionInvocationResult,\n AvailableIntegrationsList,\n ConfigureRequest,\n ConnectRequest,\n Connection,\n ConnectionActionParams,\n ConnectionIdParam,\n ConnectionsList,\n InvokeActionRequest,\n ReconnectRequest,\n refusals,\n} from '../schemas/index.js';\nimport type { Connection as DomainConnection } from '@headless-lms/core/integrations';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nfunction toApi(connection: DomainConnection): Connection {\n const { credentialRef: _credentialRef, ...rest } = connection;\n return rest;\n}\n\nexport async function integrationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const integrations = container.integrations;\n const tags = ['Integrations'];\n\n r.route({\n method: 'GET',\n url: '/api/integrations/available',\n schema: {\n operationId: 'listAvailableIntegrations',\n tags,\n summary: 'List the integrations this deployment supports',\n description: `Requires \\`integrations:read\\`. Each with the JSON Schemas of its config, its secrets and its actions.`,\n response: { 200: AvailableIntegrationsList, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return integrations.available(actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations',\n schema: {\n operationId: 'listConnections',\n tags,\n summary: \"List the organization's integration connections\",\n description: `Requires \\`integrations:read\\`. Secrets are never returned.`,\n response: { 200: ConnectionsList, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return (await integrations.list(actor)).map(toApi);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations',\n schema: {\n operationId: 'connectIntegration',\n tags,\n summary: 'Connect an integration',\n description: `Requires \\`integrations:write\\`. Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.`,\n body: ConnectRequest,\n response: { 201: Connection, ...refusals(400, 403, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const connection = await integrations.connect(req.body, actor);\n return reply.code(201).send(toApi(connection));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'getConnection',\n tags,\n summary: 'Get a connection',\n description: `Requires \\`integrations:read\\`. Secrets are never returned.`,\n params: ConnectionIdParam,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.get(req.params.id, actor));\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'configureConnection',\n tags,\n summary: \"Change a connection's configuration or active flag\",\n description: `Requires \\`integrations:write\\`. 400 when the config is invalid for the integration.`,\n params: ConnectionIdParam,\n body: ConfigureRequest,\n response: { 200: Connection, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.configure(req.params.id, req.body, actor));\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/reconnect',\n schema: {\n operationId: 'reconnectIntegration',\n tags,\n summary: \"Replace a connection's secrets\",\n description: `Requires \\`integrations:write\\`. The new secrets replace the stored ones.`,\n params: ConnectionIdParam,\n body: ReconnectRequest,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.reconnect(req.params.id, req.body.secrets, actor));\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/actions/:actionId',\n schema: {\n operationId: 'invokeConnectionAction',\n tags,\n summary: \"Invoke one of a connection's actions\",\n description: `Requires \\`integrations:write\\`. Runs the action, such as a listing that feeds a picker, with \\`input\\` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.`,\n params: ConnectionActionParams,\n body: InvokeActionRequest,\n response: { 200: ActionInvocationResult, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n const output = await integrations.invoke(\n req.params.id,\n req.params.actionId,\n req.body.input ?? {},\n actor,\n );\n return { output: output as Record<string, unknown> };\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'disconnectIntegration',\n tags,\n summary: 'Disconnect an integration',\n description: `Requires \\`integrations:delete\\`. Destroys its stored secrets.`,\n params: ConnectionIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await integrations.disconnect(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
|