@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
|
@@ -1,16 +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
|
+
import { NotificationCategoryDefaults, NotificationCategoryList, NotificationChannelInfos, NotificationListScopeQuery, NotificationTypeInfos, ReplaceNotificationCategories } from "../schemas/notifications.js";
|
|
6
7
|
import "../schemas/index.js";
|
|
7
8
|
import { z } from "zod";
|
|
8
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
9
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
|
+
}
|
|
10
20
|
async function organizationsRoutes(app, container) {
|
|
11
21
|
const r = app.withTypeProvider();
|
|
12
22
|
const organizations = container.organizations;
|
|
13
23
|
const tags = ["Organizations"];
|
|
24
|
+
const peopleTags = ["People"];
|
|
25
|
+
const roleTags = ["Roles"];
|
|
14
26
|
r.route({
|
|
15
27
|
method: "POST",
|
|
16
28
|
url: "/api/organizations",
|
|
@@ -24,7 +36,7 @@ async function organizationsRoutes(app, container) {
|
|
|
24
36
|
response: { 201: Organization }
|
|
25
37
|
},
|
|
26
38
|
handler: async (req, reply) => {
|
|
27
|
-
const org = await
|
|
39
|
+
const org = await organizations.createOrganization(req.body, req.headers);
|
|
28
40
|
return reply.code(201).send({
|
|
29
41
|
name: org.name,
|
|
30
42
|
slug: org.slug,
|
|
@@ -40,102 +52,272 @@ async function organizationsRoutes(app, container) {
|
|
|
40
52
|
schema: {
|
|
41
53
|
operationId: "updateOrganization",
|
|
42
54
|
tags,
|
|
43
|
-
summary: "Update the active organization
|
|
44
|
-
description:
|
|
55
|
+
summary: "Update the active organization",
|
|
56
|
+
description: `Requires \`organizations:write\`. Changes its name or slug; 409 when the slug is taken.`,
|
|
45
57
|
body: UpdateOrganization,
|
|
46
58
|
response: {
|
|
47
59
|
200: Organization,
|
|
48
60
|
...refusals(403, 409)
|
|
49
61
|
}
|
|
50
62
|
},
|
|
51
|
-
handler: async (req
|
|
52
|
-
const
|
|
53
|
-
const org = await
|
|
54
|
-
return
|
|
63
|
+
handler: async (req) => {
|
|
64
|
+
const actor = await resolveActor(container, req);
|
|
65
|
+
const org = await organizations.updateOrganization(req.body, actor);
|
|
66
|
+
return {
|
|
55
67
|
id: org.id,
|
|
56
68
|
name: org.name,
|
|
57
69
|
slug: org.slug,
|
|
58
70
|
createdAt: org.createdAt.toISOString()
|
|
59
|
-
}
|
|
71
|
+
};
|
|
60
72
|
}
|
|
61
73
|
});
|
|
62
74
|
r.route({
|
|
63
|
-
method: "
|
|
64
|
-
url: "/api/organizations/
|
|
75
|
+
method: "POST",
|
|
76
|
+
url: "/api/organizations/transfer-ownership",
|
|
65
77
|
preHandler: app.requireOrgSession,
|
|
66
78
|
schema: {
|
|
67
|
-
operationId: "
|
|
79
|
+
operationId: "transferOwnership",
|
|
68
80
|
tags,
|
|
69
|
-
summary: "
|
|
70
|
-
description: "
|
|
71
|
-
|
|
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,
|
|
72
84
|
response: {
|
|
73
|
-
|
|
74
|
-
...refusals(403)
|
|
85
|
+
204: z.void(),
|
|
86
|
+
...refusals(403, 404, 409)
|
|
75
87
|
}
|
|
76
88
|
},
|
|
77
|
-
handler: async (req) => {
|
|
78
|
-
const
|
|
79
|
-
|
|
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();
|
|
80
93
|
}
|
|
81
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
|
+
});
|
|
82
128
|
r.route({
|
|
83
129
|
method: "PATCH",
|
|
84
|
-
url: "/api/organizations/
|
|
130
|
+
url: "/api/organizations/roles/:key",
|
|
85
131
|
preHandler: app.requireOrgSession,
|
|
86
132
|
schema: {
|
|
87
|
-
operationId: "
|
|
88
|
-
tags,
|
|
89
|
-
summary: "
|
|
90
|
-
description:
|
|
91
|
-
params:
|
|
92
|
-
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,
|
|
139
|
+
response: {
|
|
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,
|
|
93
156
|
response: {
|
|
94
|
-
|
|
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,
|
|
95
175
|
...refusals(403, 404, 409)
|
|
96
176
|
}
|
|
97
177
|
},
|
|
98
|
-
handler: async (req) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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();
|
|
111
198
|
}
|
|
112
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
|
+
});
|
|
113
270
|
r.route({
|
|
114
271
|
method: "DELETE",
|
|
115
|
-
url: "/api/organizations/
|
|
272
|
+
url: "/api/organizations/people/:orgUserId/roles/content/:contentId",
|
|
116
273
|
preHandler: app.requireOrgSession,
|
|
117
274
|
schema: {
|
|
118
|
-
operationId: "
|
|
119
|
-
tags,
|
|
120
|
-
summary: "Remove
|
|
121
|
-
description: "
|
|
122
|
-
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,
|
|
123
314
|
response: {
|
|
124
315
|
204: z.void(),
|
|
125
316
|
...refusals(403, 404, 409)
|
|
126
317
|
}
|
|
127
318
|
},
|
|
128
319
|
handler: async (req, reply) => {
|
|
129
|
-
|
|
130
|
-
const ctx = {
|
|
131
|
-
orgId: scope.orgId,
|
|
132
|
-
authOrgId: scope.authOrgId,
|
|
133
|
-
headers: req.headers
|
|
134
|
-
};
|
|
135
|
-
if (!await organizations.removeMember(ctx, {
|
|
136
|
-
id: scope.orgUserId,
|
|
137
|
-
role: scope.role
|
|
138
|
-
}, req.params.id)) throw new NotFoundError("Member", req.params.id);
|
|
320
|
+
await organizations.removePerson(req.params.orgUserId, await resolveActor(container, req));
|
|
139
321
|
return reply.code(204).send();
|
|
140
322
|
}
|
|
141
323
|
});
|
|
@@ -145,38 +327,60 @@ async function organizationsRoutes(app, container) {
|
|
|
145
327
|
preHandler: app.requireOrgSession,
|
|
146
328
|
schema: {
|
|
147
329
|
operationId: "createInvite",
|
|
148
|
-
tags,
|
|
149
|
-
summary: "Invite a
|
|
150
|
-
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).`,
|
|
151
333
|
body: CreateInvite,
|
|
152
334
|
response: {
|
|
153
335
|
201: Invite,
|
|
154
|
-
...refusals(403, 409)
|
|
336
|
+
...refusals(403, 404, 409)
|
|
155
337
|
}
|
|
156
338
|
},
|
|
157
339
|
handler: async (req, reply) => {
|
|
158
|
-
const
|
|
159
|
-
const invite = await organizations.createInvite(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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();
|
|
180
384
|
}
|
|
181
385
|
});
|
|
182
386
|
r.route({
|
|
@@ -184,7 +388,7 @@ async function organizationsRoutes(app, container) {
|
|
|
184
388
|
url: "/api/organizations/invites/:token",
|
|
185
389
|
schema: {
|
|
186
390
|
operationId: "getInvite",
|
|
187
|
-
tags,
|
|
391
|
+
tags: peopleTags,
|
|
188
392
|
summary: "Read an invite by its token",
|
|
189
393
|
description: "Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.",
|
|
190
394
|
params: InviteTokenParam,
|
|
@@ -193,16 +397,7 @@ async function organizationsRoutes(app, container) {
|
|
|
193
397
|
...refusals(404)
|
|
194
398
|
}
|
|
195
399
|
},
|
|
196
|
-
handler: async (req) =>
|
|
197
|
-
const invite = await organizations.peekInvite(req.params.token);
|
|
198
|
-
if (!invite) throw new NotFoundError("Invite", "token");
|
|
199
|
-
const person = await container.identity.getUserByEmail(invite.email);
|
|
200
|
-
return {
|
|
201
|
-
email: invite.email,
|
|
202
|
-
firstName: person?.firstName ?? null,
|
|
203
|
-
lastName: person?.lastName ?? null
|
|
204
|
-
};
|
|
205
|
-
}
|
|
400
|
+
handler: async (req) => organizations.describeInvite(req.params.token)
|
|
206
401
|
});
|
|
207
402
|
r.route({
|
|
208
403
|
method: "POST",
|
|
@@ -210,9 +405,9 @@ async function organizationsRoutes(app, container) {
|
|
|
210
405
|
preHandler: app.requireSession,
|
|
211
406
|
schema: {
|
|
212
407
|
operationId: "acceptInvite",
|
|
213
|
-
tags,
|
|
408
|
+
tags: peopleTags,
|
|
214
409
|
summary: "Accept an invite with the signed-in account",
|
|
215
|
-
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.",
|
|
216
411
|
body: AcceptInvite,
|
|
217
412
|
response: {
|
|
218
413
|
200: AcceptInviteResult,
|
|
@@ -220,23 +415,133 @@ async function organizationsRoutes(app, container) {
|
|
|
220
415
|
}
|
|
221
416
|
},
|
|
222
417
|
handler: async (req) => {
|
|
223
|
-
|
|
224
|
-
if (!user) throw new NotFoundError("User", req.authUser.id);
|
|
225
|
-
const accepted = await container.organizations.acceptInvite({
|
|
418
|
+
await organizations.acceptInvite({
|
|
226
419
|
token: req.body.token,
|
|
227
|
-
|
|
420
|
+
authUserId: req.authUser.id,
|
|
228
421
|
email: req.authUser.email
|
|
229
|
-
});
|
|
230
|
-
const org = await container.organizations.getById(accepted.orgId);
|
|
231
|
-
if (!org) throw new NotFoundError("Organization", accepted.orgId);
|
|
232
|
-
const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);
|
|
233
|
-
req.log.info({
|
|
234
|
-
accepted,
|
|
235
|
-
stamped
|
|
236
|
-
}, "accepted invite");
|
|
422
|
+
}, req.headers);
|
|
237
423
|
return {};
|
|
238
424
|
}
|
|
239
425
|
});
|
|
426
|
+
const notificationTags = ["Notifications"];
|
|
427
|
+
r.route({
|
|
428
|
+
method: "GET",
|
|
429
|
+
url: "/api/notification-types",
|
|
430
|
+
preHandler: app.requireOrgSession,
|
|
431
|
+
schema: {
|
|
432
|
+
operationId: "listNotificationTypes",
|
|
433
|
+
tags: notificationTags,
|
|
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.`,
|
|
436
|
+
querystring: NotificationListScopeQuery,
|
|
437
|
+
response: {
|
|
438
|
+
200: NotificationTypeInfos,
|
|
439
|
+
...refusals(403)
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
handler: async (req) => {
|
|
443
|
+
const actor = await resolveActor(container, req);
|
|
444
|
+
return container.notificationCatalog.types(actor, req.query.scope);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
r.route({
|
|
448
|
+
method: "GET",
|
|
449
|
+
url: "/api/notification-channels",
|
|
450
|
+
preHandler: app.requireOrgSession,
|
|
451
|
+
schema: {
|
|
452
|
+
operationId: "listNotificationChannels",
|
|
453
|
+
tags: notificationTags,
|
|
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.`,
|
|
456
|
+
response: {
|
|
457
|
+
200: NotificationChannelInfos,
|
|
458
|
+
...refusals(403)
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
handler: async (req) => {
|
|
462
|
+
const actor = await resolveActor(container, req);
|
|
463
|
+
return container.notificationCatalog.channels(actor);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
r.route({
|
|
467
|
+
method: "GET",
|
|
468
|
+
url: "/api/notification-categories",
|
|
469
|
+
preHandler: app.requireOrgSession,
|
|
470
|
+
schema: {
|
|
471
|
+
operationId: "getOrgNotificationCategories",
|
|
472
|
+
tags: notificationTags,
|
|
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.`,
|
|
475
|
+
response: {
|
|
476
|
+
200: NotificationCategoryList,
|
|
477
|
+
...refusals(403)
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
handler: async (req) => {
|
|
481
|
+
const actor = await resolveActor(container, req);
|
|
482
|
+
return container.notificationCategories.org.get(actor.orgId, actor);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
r.route({
|
|
486
|
+
method: "PUT",
|
|
487
|
+
url: "/api/notification-categories",
|
|
488
|
+
preHandler: app.requireOrgSession,
|
|
489
|
+
schema: {
|
|
490
|
+
operationId: "replaceOrgNotificationCategories",
|
|
491
|
+
tags: notificationTags,
|
|
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.`,
|
|
494
|
+
body: ReplaceNotificationCategories,
|
|
495
|
+
response: {
|
|
496
|
+
200: NotificationCategoryList,
|
|
497
|
+
...refusals(400, 403, 422)
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
handler: async (req) => {
|
|
501
|
+
const actor = await resolveActor(container, req);
|
|
502
|
+
return container.notificationCategories.org.replace(actor.orgId, req.body.categories, actor);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
r.route({
|
|
506
|
+
method: "DELETE",
|
|
507
|
+
url: "/api/notification-categories",
|
|
508
|
+
preHandler: app.requireOrgSession,
|
|
509
|
+
schema: {
|
|
510
|
+
operationId: "resetOrgNotificationCategories",
|
|
511
|
+
tags: notificationTags,
|
|
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.`,
|
|
514
|
+
response: {
|
|
515
|
+
204: z.void(),
|
|
516
|
+
...refusals(403)
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
handler: async (req, reply) => {
|
|
520
|
+
const actor = await resolveActor(container, req);
|
|
521
|
+
await container.notificationCategories.org.reset(actor.orgId, actor);
|
|
522
|
+
return reply.code(204).send();
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
r.route({
|
|
526
|
+
method: "GET",
|
|
527
|
+
url: "/api/notification-categories/defaults",
|
|
528
|
+
preHandler: app.requireOrgSession,
|
|
529
|
+
schema: {
|
|
530
|
+
operationId: "getDefaultNotificationCategories",
|
|
531
|
+
tags: notificationTags,
|
|
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.`,
|
|
534
|
+
querystring: NotificationListScopeQuery,
|
|
535
|
+
response: {
|
|
536
|
+
200: NotificationCategoryDefaults,
|
|
537
|
+
...refusals(403)
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
handler: async (req) => {
|
|
541
|
+
const actor = await resolveActor(container, req);
|
|
542
|
+
return { categories: container.notificationCatalog.defaults(actor, req.query.scope) };
|
|
543
|
+
}
|
|
544
|
+
});
|
|
240
545
|
}
|
|
241
546
|
//#endregion
|
|
242
547
|
export { organizationsRoutes };
|