@headless-lms/server 0.4.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 +5 -3
- package/dist/app/community-mail.js +60 -12
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +24 -3
- package/dist/app/container.js +102 -38
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.js +1 -0
- package/dist/app/notification-channels.js.map +1 -1
- package/dist/app/notification-reads.d.ts +3 -3
- package/dist/app/notification-reads.js +7 -28
- package/dist/app/notification-reads.js.map +1 -1
- 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.js +9 -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 +10 -2
- 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 +93 -145
- 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 +170 -47
- 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 +45 -52
- 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.js +9 -10
- package/dist/http/routes/member/devices.js.map +1 -1
- 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 +2 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.js +15 -23
- package/dist/http/routes/member/notification-settings.js.map +1 -1
- package/dist/http/routes/member/notifications.js +16 -16
- 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 +67 -68
- 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 +319 -135
- 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 +19 -14
- 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 -4
- package/dist/http/schemas/chats.d.ts +211 -32
- package/dist/http/schemas/chats.js +13 -4
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +91 -44
- package/dist/http/schemas/community.js +21 -8
- 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 +22 -72
- package/dist/http/schemas/discussion.js +2 -2
- 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 +11 -9
- package/dist/http/schemas/index.js +11 -9
- package/dist/http/schemas/integrations.d.ts +4 -4
- 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 +333 -69
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +57 -53
- 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 +55 -35
- 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 +1 -11
- package/dist/http/schemas/shared.js +1 -10
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +25 -3
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -13
- package/dist/http/scope.js +29 -40
- 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 +4 -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
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor, resolveCaller } from "../scope.js";
|
|
3
|
+
import { AssignmentList, ContentIdParam, CreateRole, MembersPage, MembersQuery, OrgRoleList, OrgRoleView, OrgUserContentParam, OrgUserIdParam, RoleKeyParam, SetRole, TransferOwnership, UpdateRole } from "../schemas/members.js";
|
|
3
4
|
import { CreateOrganization, Organization, UpdateOrganization } from "../schemas/organizations.js";
|
|
4
|
-
import { MemberIdParam, MemberRoleChange, MembersPage, MembersQuery, UpdateMemberRole } from "../schemas/members.js";
|
|
5
5
|
import { AcceptInvite, AcceptInviteResult, CreateInvite, GetInviteResult, Invite, InviteTokenParam } from "../schemas/invites.js";
|
|
6
6
|
import { NotificationCategoryDefaults, NotificationCategoryList, NotificationChannelInfos, NotificationListScopeQuery, NotificationTypeInfos, ReplaceNotificationCategories } from "../schemas/notifications.js";
|
|
7
7
|
import "../schemas/index.js";
|
|
8
|
-
import { typesForList } from "@headless-lms/core/shared/notification-categories";
|
|
9
8
|
import { z } from "zod";
|
|
10
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
11
9
|
//#region src/http/routes/organizations.ts
|
|
10
|
+
function inviteView(invite) {
|
|
11
|
+
return {
|
|
12
|
+
id: invite.id,
|
|
13
|
+
email: invite.email,
|
|
14
|
+
roleKey: invite.roleKey,
|
|
15
|
+
status: invite.status,
|
|
16
|
+
expiresAt: invite.expiresAt,
|
|
17
|
+
createdAt: invite.createdAt
|
|
18
|
+
};
|
|
19
|
+
}
|
|
12
20
|
async function organizationsRoutes(app, container) {
|
|
13
21
|
const r = app.withTypeProvider();
|
|
14
22
|
const organizations = container.organizations;
|
|
15
23
|
const tags = ["Organizations"];
|
|
24
|
+
const peopleTags = ["People"];
|
|
25
|
+
const roleTags = ["Roles"];
|
|
16
26
|
r.route({
|
|
17
27
|
method: "POST",
|
|
18
28
|
url: "/api/organizations",
|
|
@@ -26,7 +36,7 @@ async function organizationsRoutes(app, container) {
|
|
|
26
36
|
response: { 201: Organization }
|
|
27
37
|
},
|
|
28
38
|
handler: async (req, reply) => {
|
|
29
|
-
const org = await
|
|
39
|
+
const org = await organizations.createOrganization(req.body, req.headers);
|
|
30
40
|
return reply.code(201).send({
|
|
31
41
|
name: org.name,
|
|
32
42
|
slug: org.slug,
|
|
@@ -42,102 +52,272 @@ async function organizationsRoutes(app, container) {
|
|
|
42
52
|
schema: {
|
|
43
53
|
operationId: "updateOrganization",
|
|
44
54
|
tags,
|
|
45
|
-
summary: "Update the active organization
|
|
46
|
-
description:
|
|
55
|
+
summary: "Update the active organization",
|
|
56
|
+
description: `Requires \`organizations:write\`. Changes its name or slug; 409 when the slug is taken.`,
|
|
47
57
|
body: UpdateOrganization,
|
|
48
58
|
response: {
|
|
49
59
|
200: Organization,
|
|
50
60
|
...refusals(403, 409)
|
|
51
61
|
}
|
|
52
62
|
},
|
|
53
|
-
handler: async (req
|
|
54
|
-
const
|
|
55
|
-
const org = await
|
|
56
|
-
return
|
|
63
|
+
handler: async (req) => {
|
|
64
|
+
const actor = await resolveActor(container, req);
|
|
65
|
+
const org = await organizations.updateOrganization(req.body, actor);
|
|
66
|
+
return {
|
|
57
67
|
id: org.id,
|
|
58
68
|
name: org.name,
|
|
59
69
|
slug: org.slug,
|
|
60
70
|
createdAt: org.createdAt.toISOString()
|
|
61
|
-
}
|
|
71
|
+
};
|
|
62
72
|
}
|
|
63
73
|
});
|
|
64
74
|
r.route({
|
|
65
|
-
method: "
|
|
66
|
-
url: "/api/organizations/
|
|
75
|
+
method: "POST",
|
|
76
|
+
url: "/api/organizations/transfer-ownership",
|
|
67
77
|
preHandler: app.requireOrgSession,
|
|
68
78
|
schema: {
|
|
69
|
-
operationId: "
|
|
79
|
+
operationId: "transferOwnership",
|
|
70
80
|
tags,
|
|
71
|
-
summary: "
|
|
72
|
-
description: "
|
|
73
|
-
|
|
81
|
+
summary: "Transfer ownership of the organization",
|
|
82
|
+
description: "Owner only. The new owner must be an active member. The previous owner takes the named org role, Member by default. 403 for anyone else, including the owner's API keys.",
|
|
83
|
+
body: TransferOwnership,
|
|
74
84
|
response: {
|
|
75
|
-
|
|
76
|
-
...refusals(403)
|
|
85
|
+
204: z.void(),
|
|
86
|
+
...refusals(403, 404, 409)
|
|
77
87
|
}
|
|
78
88
|
},
|
|
79
|
-
handler: async (req) => {
|
|
80
|
-
const
|
|
81
|
-
|
|
89
|
+
handler: async (req, reply) => {
|
|
90
|
+
const actor = await resolveActor(container, req);
|
|
91
|
+
await organizations.transferOwnership(req.body, actor);
|
|
92
|
+
return reply.code(204).send();
|
|
82
93
|
}
|
|
83
94
|
});
|
|
95
|
+
r.route({
|
|
96
|
+
method: "GET",
|
|
97
|
+
url: "/api/organizations/roles",
|
|
98
|
+
preHandler: app.requireOrgSession,
|
|
99
|
+
schema: {
|
|
100
|
+
operationId: "listRoles",
|
|
101
|
+
tags: roleTags,
|
|
102
|
+
summary: "The organization's roles",
|
|
103
|
+
description: `Requires \`roles:read\`. Each role with its levels, permissions and the number of people who hold it.`,
|
|
104
|
+
response: {
|
|
105
|
+
200: OrgRoleList,
|
|
106
|
+
...refusals(403)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
handler: async (req) => organizations.listRoles(await resolveActor(container, req))
|
|
110
|
+
});
|
|
111
|
+
r.route({
|
|
112
|
+
method: "GET",
|
|
113
|
+
url: "/api/organizations/roles/:key",
|
|
114
|
+
preHandler: app.requireOrgSession,
|
|
115
|
+
schema: {
|
|
116
|
+
operationId: "getRole",
|
|
117
|
+
tags: roleTags,
|
|
118
|
+
summary: "Read a role",
|
|
119
|
+
description: "Requires `roles:read`.",
|
|
120
|
+
params: RoleKeyParam,
|
|
121
|
+
response: {
|
|
122
|
+
200: OrgRoleView,
|
|
123
|
+
...refusals(403, 404)
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
handler: async (req) => organizations.getRole(req.params.key, await resolveActor(container, req))
|
|
127
|
+
});
|
|
84
128
|
r.route({
|
|
85
129
|
method: "PATCH",
|
|
86
|
-
url: "/api/organizations/
|
|
130
|
+
url: "/api/organizations/roles/:key",
|
|
87
131
|
preHandler: app.requireOrgSession,
|
|
88
132
|
schema: {
|
|
89
|
-
operationId: "
|
|
90
|
-
tags,
|
|
91
|
-
summary: "
|
|
92
|
-
description:
|
|
93
|
-
params:
|
|
94
|
-
body:
|
|
133
|
+
operationId: "updateRole",
|
|
134
|
+
tags: roleTags,
|
|
135
|
+
summary: "Rename or change a role",
|
|
136
|
+
description: `Requires \`roles:write\`. A rename needs nothing else. Changing permissions, levels or the team flag needs every permission the role holds before and after the change (403 names the missing ones). Owner and Member can only be renamed (409). Names are unique in the organization (409).`,
|
|
137
|
+
params: RoleKeyParam,
|
|
138
|
+
body: UpdateRole,
|
|
95
139
|
response: {
|
|
96
|
-
200:
|
|
140
|
+
200: OrgRoleView,
|
|
141
|
+
...refusals(400, 403, 404, 409)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
handler: async (req) => organizations.updateRole(req.params.key, req.body, await resolveActor(container, req))
|
|
145
|
+
});
|
|
146
|
+
r.route({
|
|
147
|
+
method: "POST",
|
|
148
|
+
url: "/api/organizations/roles",
|
|
149
|
+
preHandler: app.requireOrgSession,
|
|
150
|
+
schema: {
|
|
151
|
+
operationId: "createRole",
|
|
152
|
+
tags: roleTags,
|
|
153
|
+
summary: "Add a role",
|
|
154
|
+
description: `Requires \`roles:write\`. The role starts from a deployment role (\`base\`) and needs every permission it holds.`,
|
|
155
|
+
body: CreateRole,
|
|
156
|
+
response: {
|
|
157
|
+
201: OrgRoleView,
|
|
158
|
+
...refusals(400, 403, 409)
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
handler: async (req, reply) => reply.code(201).send(await organizations.createRole(req.body, await resolveActor(container, req)))
|
|
162
|
+
});
|
|
163
|
+
r.route({
|
|
164
|
+
method: "DELETE",
|
|
165
|
+
url: "/api/organizations/roles/:key/customization",
|
|
166
|
+
preHandler: app.requireOrgSession,
|
|
167
|
+
schema: {
|
|
168
|
+
operationId: "resetRole",
|
|
169
|
+
tags: roleTags,
|
|
170
|
+
summary: "Reset a role to the deployment's version",
|
|
171
|
+
description: `Requires \`roles:write\`. Needs every permission the role holds before and after.`,
|
|
172
|
+
params: RoleKeyParam,
|
|
173
|
+
response: {
|
|
174
|
+
200: OrgRoleView,
|
|
97
175
|
...refusals(403, 404, 409)
|
|
98
176
|
}
|
|
99
177
|
},
|
|
100
|
-
handler: async (req) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
handler: async (req) => organizations.resetRole(req.params.key, await resolveActor(container, req))
|
|
179
|
+
});
|
|
180
|
+
r.route({
|
|
181
|
+
method: "DELETE",
|
|
182
|
+
url: "/api/organizations/roles/:key",
|
|
183
|
+
preHandler: app.requireOrgSession,
|
|
184
|
+
schema: {
|
|
185
|
+
operationId: "deleteRole",
|
|
186
|
+
tags: roleTags,
|
|
187
|
+
summary: "Delete a role the organization added",
|
|
188
|
+
description: `Requires \`roles:delete\`. Refused while anyone holds it (409, with the count).`,
|
|
189
|
+
params: RoleKeyParam,
|
|
190
|
+
response: {
|
|
191
|
+
204: z.void(),
|
|
192
|
+
...refusals(403, 404, 409)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
handler: async (req, reply) => {
|
|
196
|
+
await organizations.deleteRole(req.params.key, await resolveActor(container, req));
|
|
197
|
+
return reply.code(204).send();
|
|
113
198
|
}
|
|
114
199
|
});
|
|
200
|
+
r.route({
|
|
201
|
+
method: "GET",
|
|
202
|
+
url: "/api/organizations/people",
|
|
203
|
+
preHandler: app.requireOrgSession,
|
|
204
|
+
schema: {
|
|
205
|
+
operationId: "listTeam",
|
|
206
|
+
tags: peopleTags,
|
|
207
|
+
summary: "The organization's team",
|
|
208
|
+
description: `Requires \`people:read\`. Everyone with an org role other than Member or a role on a content item, invited people included.`,
|
|
209
|
+
querystring: MembersQuery,
|
|
210
|
+
response: {
|
|
211
|
+
200: MembersPage,
|
|
212
|
+
...refusals(403)
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
handler: async (req) => organizations.listTeam(req.query, await resolveActor(container, req))
|
|
216
|
+
});
|
|
217
|
+
r.route({
|
|
218
|
+
method: "GET",
|
|
219
|
+
url: "/api/organizations/people/:orgUserId/roles",
|
|
220
|
+
preHandler: app.requireOrgSession,
|
|
221
|
+
schema: {
|
|
222
|
+
operationId: "listPersonRoles",
|
|
223
|
+
tags: peopleTags,
|
|
224
|
+
summary: "A person's roles",
|
|
225
|
+
description: `Requires \`people:read\`. Their org role and every role they hold on a content item.`,
|
|
226
|
+
params: OrgUserIdParam,
|
|
227
|
+
response: {
|
|
228
|
+
200: AssignmentList,
|
|
229
|
+
...refusals(403)
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
handler: async (req) => organizations.listAssignments(req.params.orgUserId, await resolveActor(container, req))
|
|
233
|
+
});
|
|
234
|
+
r.route({
|
|
235
|
+
method: "PUT",
|
|
236
|
+
url: "/api/organizations/people/:orgUserId/roles/org",
|
|
237
|
+
preHandler: app.requireOrgSession,
|
|
238
|
+
schema: {
|
|
239
|
+
operationId: "setOrgRole",
|
|
240
|
+
tags: peopleTags,
|
|
241
|
+
summary: "Set a person's org role",
|
|
242
|
+
description: `Requires \`people:write\`. Needs every permission the old and the new role hold. Nobody changes their own roles, and the owner's role changes only by transfer.`,
|
|
243
|
+
params: OrgUserIdParam,
|
|
244
|
+
body: SetRole,
|
|
245
|
+
response: {
|
|
246
|
+
200: AssignmentList,
|
|
247
|
+
...refusals(403, 404, 409)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
handler: async (req) => organizations.setOrgRole(req.params.orgUserId, req.body.roleKey, await resolveActor(container, req))
|
|
251
|
+
});
|
|
252
|
+
r.route({
|
|
253
|
+
method: "PUT",
|
|
254
|
+
url: "/api/organizations/people/:orgUserId/roles/content/:contentId",
|
|
255
|
+
preHandler: app.requireOrgSession,
|
|
256
|
+
schema: {
|
|
257
|
+
operationId: "assignRole",
|
|
258
|
+
tags: peopleTags,
|
|
259
|
+
summary: "Give a person a role on a content item",
|
|
260
|
+
description: `Requires \`people:write\`. The role must list the item's type as a level. Needs every permission the old and the new role give there.`,
|
|
261
|
+
params: OrgUserContentParam,
|
|
262
|
+
body: SetRole,
|
|
263
|
+
response: {
|
|
264
|
+
200: AssignmentList,
|
|
265
|
+
...refusals(403, 404, 409)
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
handler: async (req) => organizations.assignRole(req.params.orgUserId, req.params.contentId, req.body.roleKey, await resolveActor(container, req))
|
|
269
|
+
});
|
|
115
270
|
r.route({
|
|
116
271
|
method: "DELETE",
|
|
117
|
-
url: "/api/organizations/
|
|
272
|
+
url: "/api/organizations/people/:orgUserId/roles/content/:contentId",
|
|
118
273
|
preHandler: app.requireOrgSession,
|
|
119
274
|
schema: {
|
|
120
|
-
operationId: "
|
|
121
|
-
tags,
|
|
122
|
-
summary: "Remove
|
|
123
|
-
description: "
|
|
124
|
-
params:
|
|
275
|
+
operationId: "unassignRole",
|
|
276
|
+
tags: peopleTags,
|
|
277
|
+
summary: "Remove a person's role on a content item",
|
|
278
|
+
description: "Requires `people:write`.",
|
|
279
|
+
params: OrgUserContentParam,
|
|
280
|
+
response: {
|
|
281
|
+
200: AssignmentList,
|
|
282
|
+
...refusals(403, 404)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
handler: async (req) => organizations.unassignRole(req.params.orgUserId, req.params.contentId, await resolveActor(container, req))
|
|
286
|
+
});
|
|
287
|
+
r.route({
|
|
288
|
+
method: "GET",
|
|
289
|
+
url: "/api/organizations/content/:contentId/roles",
|
|
290
|
+
preHandler: app.requireOrgSession,
|
|
291
|
+
schema: {
|
|
292
|
+
operationId: "listRolesOn",
|
|
293
|
+
tags: peopleTags,
|
|
294
|
+
summary: "The people with a role on a content item",
|
|
295
|
+
description: "Requires `people:read`.",
|
|
296
|
+
params: ContentIdParam,
|
|
297
|
+
response: {
|
|
298
|
+
200: AssignmentList,
|
|
299
|
+
...refusals(403)
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
handler: async (req) => organizations.listAssignmentsOn(req.params.contentId, await resolveActor(container, req))
|
|
303
|
+
});
|
|
304
|
+
r.route({
|
|
305
|
+
method: "DELETE",
|
|
306
|
+
url: "/api/organizations/people/:orgUserId",
|
|
307
|
+
preHandler: app.requireOrgSession,
|
|
308
|
+
schema: {
|
|
309
|
+
operationId: "removePerson",
|
|
310
|
+
tags: peopleTags,
|
|
311
|
+
summary: "Remove a person from the organization",
|
|
312
|
+
description: `Requires \`people:delete\`. Needs every permission the person holds, where they hold it. Revokes their API keys and connected apps here. The owner can't be removed (409); neither can someone whose posts must be kept (409).`,
|
|
313
|
+
params: OrgUserIdParam,
|
|
125
314
|
response: {
|
|
126
315
|
204: z.void(),
|
|
127
316
|
...refusals(403, 404, 409)
|
|
128
317
|
}
|
|
129
318
|
},
|
|
130
319
|
handler: async (req, reply) => {
|
|
131
|
-
|
|
132
|
-
const ctx = {
|
|
133
|
-
orgId: scope.orgId,
|
|
134
|
-
authOrgId: scope.authOrgId,
|
|
135
|
-
headers: req.headers
|
|
136
|
-
};
|
|
137
|
-
if (!await organizations.removeMember(ctx, {
|
|
138
|
-
id: scope.orgUserId,
|
|
139
|
-
role: scope.role
|
|
140
|
-
}, req.params.id)) throw new NotFoundError("Member", req.params.id);
|
|
320
|
+
await organizations.removePerson(req.params.orgUserId, await resolveActor(container, req));
|
|
141
321
|
return reply.code(204).send();
|
|
142
322
|
}
|
|
143
323
|
});
|
|
@@ -147,38 +327,60 @@ async function organizationsRoutes(app, container) {
|
|
|
147
327
|
preHandler: app.requireOrgSession,
|
|
148
328
|
schema: {
|
|
149
329
|
operationId: "createInvite",
|
|
150
|
-
tags,
|
|
151
|
-
summary: "Invite a
|
|
152
|
-
description:
|
|
330
|
+
tags: peopleTags,
|
|
331
|
+
summary: "Invite a person with an org role",
|
|
332
|
+
description: `Requires \`people:write\`. The person is created by this call, so access and item roles can be given before they accept. Needs every permission the role holds; never \`owner\` (409).`,
|
|
153
333
|
body: CreateInvite,
|
|
154
334
|
response: {
|
|
155
335
|
201: Invite,
|
|
156
|
-
...refusals(403, 409)
|
|
336
|
+
...refusals(403, 404, 409)
|
|
157
337
|
}
|
|
158
338
|
},
|
|
159
339
|
handler: async (req, reply) => {
|
|
160
|
-
const
|
|
161
|
-
const invite = await organizations.createInvite(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
340
|
+
const caller = await resolveCaller(container, req);
|
|
341
|
+
const invite = await organizations.createInvite(req.body, caller.actor, caller.userId);
|
|
342
|
+
return reply.code(201).send(inviteView(invite));
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
r.route({
|
|
346
|
+
method: "POST",
|
|
347
|
+
url: "/api/organizations/people/:orgUserId/invite/resend",
|
|
348
|
+
preHandler: app.requireOrgSession,
|
|
349
|
+
schema: {
|
|
350
|
+
operationId: "resendInvite",
|
|
351
|
+
tags: peopleTags,
|
|
352
|
+
summary: "Resend a person's pending invite",
|
|
353
|
+
description: `Requires \`people:write\`. Issues a new link; the resender becomes the inviter.`,
|
|
354
|
+
params: OrgUserIdParam,
|
|
355
|
+
response: {
|
|
356
|
+
204: z.void(),
|
|
357
|
+
...refusals(403, 404)
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
handler: async (req, reply) => {
|
|
361
|
+
const caller = await resolveCaller(container, req);
|
|
362
|
+
await organizations.resendInvite(req.params.orgUserId, caller.actor, caller.userId);
|
|
363
|
+
return reply.code(204).send();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
r.route({
|
|
367
|
+
method: "DELETE",
|
|
368
|
+
url: "/api/organizations/people/:orgUserId/invite",
|
|
369
|
+
preHandler: app.requireOrgSession,
|
|
370
|
+
schema: {
|
|
371
|
+
operationId: "cancelInvite",
|
|
372
|
+
tags: peopleTags,
|
|
373
|
+
summary: "Cancel a person's pending invite",
|
|
374
|
+
description: `Requires \`people:write\`. Someone still invited who holds no access and no item role is removed with it.`,
|
|
375
|
+
params: OrgUserIdParam,
|
|
376
|
+
response: {
|
|
377
|
+
204: z.void(),
|
|
378
|
+
...refusals(403, 404)
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
handler: async (req, reply) => {
|
|
382
|
+
await organizations.cancelInvite(req.params.orgUserId, await resolveActor(container, req));
|
|
383
|
+
return reply.code(204).send();
|
|
182
384
|
}
|
|
183
385
|
});
|
|
184
386
|
r.route({
|
|
@@ -186,7 +388,7 @@ async function organizationsRoutes(app, container) {
|
|
|
186
388
|
url: "/api/organizations/invites/:token",
|
|
187
389
|
schema: {
|
|
188
390
|
operationId: "getInvite",
|
|
189
|
-
tags,
|
|
391
|
+
tags: peopleTags,
|
|
190
392
|
summary: "Read an invite by its token",
|
|
191
393
|
description: "Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.",
|
|
192
394
|
params: InviteTokenParam,
|
|
@@ -195,16 +397,7 @@ async function organizationsRoutes(app, container) {
|
|
|
195
397
|
...refusals(404)
|
|
196
398
|
}
|
|
197
399
|
},
|
|
198
|
-
handler: async (req) =>
|
|
199
|
-
const invite = await organizations.peekInvite(req.params.token);
|
|
200
|
-
if (!invite) throw new NotFoundError("Invite", "token");
|
|
201
|
-
const person = await container.identity.getUserByEmail(invite.email);
|
|
202
|
-
return {
|
|
203
|
-
email: invite.email,
|
|
204
|
-
firstName: person?.firstName ?? null,
|
|
205
|
-
lastName: person?.lastName ?? null
|
|
206
|
-
};
|
|
207
|
-
}
|
|
400
|
+
handler: async (req) => organizations.describeInvite(req.params.token)
|
|
208
401
|
});
|
|
209
402
|
r.route({
|
|
210
403
|
method: "POST",
|
|
@@ -212,9 +405,9 @@ async function organizationsRoutes(app, container) {
|
|
|
212
405
|
preHandler: app.requireSession,
|
|
213
406
|
schema: {
|
|
214
407
|
operationId: "acceptInvite",
|
|
215
|
-
tags,
|
|
408
|
+
tags: peopleTags,
|
|
216
409
|
summary: "Accept an invite with the signed-in account",
|
|
217
|
-
description: "For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired,
|
|
410
|
+
description: "For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired, the email does not match, or the inviter no longer holds the role.",
|
|
218
411
|
body: AcceptInvite,
|
|
219
412
|
response: {
|
|
220
413
|
200: AcceptInviteResult,
|
|
@@ -222,20 +415,11 @@ async function organizationsRoutes(app, container) {
|
|
|
222
415
|
}
|
|
223
416
|
},
|
|
224
417
|
handler: async (req) => {
|
|
225
|
-
|
|
226
|
-
if (!user) throw new NotFoundError("User", req.authUser.id);
|
|
227
|
-
const accepted = await container.organizations.acceptInvite({
|
|
418
|
+
await organizations.acceptInvite({
|
|
228
419
|
token: req.body.token,
|
|
229
|
-
|
|
420
|
+
authUserId: req.authUser.id,
|
|
230
421
|
email: req.authUser.email
|
|
231
|
-
});
|
|
232
|
-
const org = await container.organizations.getById(accepted.orgId);
|
|
233
|
-
if (!org) throw new NotFoundError("Organization", accepted.orgId);
|
|
234
|
-
const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);
|
|
235
|
-
req.log.info({
|
|
236
|
-
accepted,
|
|
237
|
-
stamped
|
|
238
|
-
}, "accepted invite");
|
|
422
|
+
}, req.headers);
|
|
239
423
|
return {};
|
|
240
424
|
}
|
|
241
425
|
});
|
|
@@ -247,8 +431,8 @@ async function organizationsRoutes(app, container) {
|
|
|
247
431
|
schema: {
|
|
248
432
|
operationId: "listNotificationTypes",
|
|
249
433
|
tags: notificationTags,
|
|
250
|
-
summary: "List the notification types a category list can cover
|
|
251
|
-
description:
|
|
434
|
+
summary: "List the notification types a category list can cover",
|
|
435
|
+
description: `Requires one of \`notification_categories:read\`, \`communities:read\`. \`scope=org\` lists the types the org list can cover and needs \`notification_categories:read\`; \`scope=community\` lists those a community list can cover and also takes \`communities:read\` on any community. Each type carries all its scopes, so an editor can tell which types can be per space.`,
|
|
252
436
|
querystring: NotificationListScopeQuery,
|
|
253
437
|
response: {
|
|
254
438
|
200: NotificationTypeInfos,
|
|
@@ -256,8 +440,8 @@ async function organizationsRoutes(app, container) {
|
|
|
256
440
|
}
|
|
257
441
|
},
|
|
258
442
|
handler: async (req) => {
|
|
259
|
-
await
|
|
260
|
-
return
|
|
443
|
+
const actor = await resolveActor(container, req);
|
|
444
|
+
return container.notificationCatalog.types(actor, req.query.scope);
|
|
261
445
|
}
|
|
262
446
|
});
|
|
263
447
|
r.route({
|
|
@@ -267,16 +451,16 @@ async function organizationsRoutes(app, container) {
|
|
|
267
451
|
schema: {
|
|
268
452
|
operationId: "listNotificationChannels",
|
|
269
453
|
tags: notificationTags,
|
|
270
|
-
summary: "List the notification channels this deployment can deliver on
|
|
271
|
-
description:
|
|
454
|
+
summary: "List the notification channels this deployment can deliver on",
|
|
455
|
+
description: `Requires one of \`notification_categories:read\`, \`communities:read\`. \`communities:read\` on any community is enough. \`inApp\` is always first.`,
|
|
272
456
|
response: {
|
|
273
457
|
200: NotificationChannelInfos,
|
|
274
458
|
...refusals(403)
|
|
275
459
|
}
|
|
276
460
|
},
|
|
277
461
|
handler: async (req) => {
|
|
278
|
-
await
|
|
279
|
-
return container.
|
|
462
|
+
const actor = await resolveActor(container, req);
|
|
463
|
+
return container.notificationCatalog.channels(actor);
|
|
280
464
|
}
|
|
281
465
|
});
|
|
282
466
|
r.route({
|
|
@@ -286,16 +470,16 @@ async function organizationsRoutes(app, container) {
|
|
|
286
470
|
schema: {
|
|
287
471
|
operationId: "getOrgNotificationCategories",
|
|
288
472
|
tags: notificationTags,
|
|
289
|
-
summary: "Get the organization's notification categories
|
|
290
|
-
description:
|
|
473
|
+
summary: "Get the organization's notification categories",
|
|
474
|
+
description: `Requires \`notification_categories:read\`. The list in use for notifications outside any community, with \`source\` saying whether it is the default or a stored list.`,
|
|
291
475
|
response: {
|
|
292
476
|
200: NotificationCategoryList,
|
|
293
477
|
...refusals(403)
|
|
294
478
|
}
|
|
295
479
|
},
|
|
296
480
|
handler: async (req) => {
|
|
297
|
-
const
|
|
298
|
-
return container.notificationCategories.org.get(
|
|
481
|
+
const actor = await resolveActor(container, req);
|
|
482
|
+
return container.notificationCategories.org.get(actor.orgId, actor);
|
|
299
483
|
}
|
|
300
484
|
});
|
|
301
485
|
r.route({
|
|
@@ -305,8 +489,8 @@ async function organizationsRoutes(app, container) {
|
|
|
305
489
|
schema: {
|
|
306
490
|
operationId: "replaceOrgNotificationCategories",
|
|
307
491
|
tags: notificationTags,
|
|
308
|
-
summary: "Replace the organization's notification categories
|
|
309
|
-
description:
|
|
492
|
+
summary: "Replace the organization's notification categories",
|
|
493
|
+
description: `Requires \`notification_categories:write\`. Stores the whole list. 422 when a key repeats, a type cannot be in the org list, a reason is not allowed for its type, a channel is not configured, or earlier categories already cover everything a category covers.`,
|
|
310
494
|
body: ReplaceNotificationCategories,
|
|
311
495
|
response: {
|
|
312
496
|
200: NotificationCategoryList,
|
|
@@ -314,8 +498,8 @@ async function organizationsRoutes(app, container) {
|
|
|
314
498
|
}
|
|
315
499
|
},
|
|
316
500
|
handler: async (req) => {
|
|
317
|
-
const
|
|
318
|
-
return container.notificationCategories.org.replace(
|
|
501
|
+
const actor = await resolveActor(container, req);
|
|
502
|
+
return container.notificationCategories.org.replace(actor.orgId, req.body.categories, actor);
|
|
319
503
|
}
|
|
320
504
|
});
|
|
321
505
|
r.route({
|
|
@@ -325,16 +509,16 @@ async function organizationsRoutes(app, container) {
|
|
|
325
509
|
schema: {
|
|
326
510
|
operationId: "resetOrgNotificationCategories",
|
|
327
511
|
tags: notificationTags,
|
|
328
|
-
summary: "Go back to the default notification categories for the organization
|
|
329
|
-
description:
|
|
512
|
+
summary: "Go back to the default notification categories for the organization",
|
|
513
|
+
description: `Requires \`notification_categories:write\`. Removes the stored list; the default applies again.`,
|
|
330
514
|
response: {
|
|
331
515
|
204: z.void(),
|
|
332
516
|
...refusals(403)
|
|
333
517
|
}
|
|
334
518
|
},
|
|
335
519
|
handler: async (req, reply) => {
|
|
336
|
-
const
|
|
337
|
-
await container.notificationCategories.org.reset(
|
|
520
|
+
const actor = await resolveActor(container, req);
|
|
521
|
+
await container.notificationCategories.org.reset(actor.orgId, actor);
|
|
338
522
|
return reply.code(204).send();
|
|
339
523
|
}
|
|
340
524
|
});
|
|
@@ -345,8 +529,8 @@ async function organizationsRoutes(app, container) {
|
|
|
345
529
|
schema: {
|
|
346
530
|
operationId: "getDefaultNotificationCategories",
|
|
347
531
|
tags: notificationTags,
|
|
348
|
-
summary: "Get the default notification categories
|
|
349
|
-
description:
|
|
532
|
+
summary: "Get the default notification categories",
|
|
533
|
+
description: `Requires one of \`notification_categories:read\`, \`communities:read\`. The default org list (\`scope=org\`, which needs \`notification_categories:read\`) or community list (\`scope=community\`, which also takes \`communities:read\` on any community), offering only the configured channels.`,
|
|
350
534
|
querystring: NotificationListScopeQuery,
|
|
351
535
|
response: {
|
|
352
536
|
200: NotificationCategoryDefaults,
|
|
@@ -354,8 +538,8 @@ async function organizationsRoutes(app, container) {
|
|
|
354
538
|
}
|
|
355
539
|
},
|
|
356
540
|
handler: async (req) => {
|
|
357
|
-
await
|
|
358
|
-
return { categories: (req.query.scope
|
|
541
|
+
const actor = await resolveActor(container, req);
|
|
542
|
+
return { categories: container.notificationCatalog.defaults(actor, req.query.scope) };
|
|
359
543
|
}
|
|
360
544
|
});
|
|
361
545
|
}
|