@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.
Files changed (223) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +5 -3
  3. package/dist/app/community-mail.js +60 -12
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +24 -3
  6. package/dist/app/container.js +102 -38
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.js +1 -0
  9. package/dist/app/notification-channels.js.map +1 -1
  10. package/dist/app/notification-reads.d.ts +3 -3
  11. package/dist/app/notification-reads.js +7 -28
  12. package/dist/app/notification-reads.js.map +1 -1
  13. package/dist/app/platform.d.ts +14 -0
  14. package/dist/app/platform.js +14 -0
  15. package/dist/app/platform.js.map +1 -0
  16. package/dist/http/config.d.ts +4 -0
  17. package/dist/http/errors.js +9 -4
  18. package/dist/http/errors.js.map +1 -1
  19. package/dist/http/fastify.d.ts +8 -0
  20. package/dist/http/mcp/catalogue.d.ts +11 -9
  21. package/dist/http/mcp/catalogue.js +15 -21
  22. package/dist/http/mcp/catalogue.js.map +1 -1
  23. package/dist/http/mcp/challenge.js +3 -3
  24. package/dist/http/mcp/challenge.js.map +1 -1
  25. package/dist/http/mcp/route.js +7 -7
  26. package/dist/http/mcp/route.js.map +1 -1
  27. package/dist/http/mcp/test-support.d.ts +6 -5
  28. package/dist/http/mcp/test-support.js +32 -11
  29. package/dist/http/mcp/test-support.js.map +1 -1
  30. package/dist/http/mcp/tool.d.ts +3 -9
  31. package/dist/http/mcp/tool.js +14 -6
  32. package/dist/http/mcp/tool.js.map +1 -1
  33. package/dist/http/mcp/tools/automations.js +10 -20
  34. package/dist/http/mcp/tools/automations.js.map +1 -1
  35. package/dist/http/mcp/tools/catalog.js +25 -42
  36. package/dist/http/mcp/tools/catalog.js.map +1 -1
  37. package/dist/http/mcp/tools/comments.js +9 -19
  38. package/dist/http/mcp/tools/comments.js.map +1 -1
  39. package/dist/http/mcp/tools/communities.js +16 -31
  40. package/dist/http/mcp/tools/communities.js.map +1 -1
  41. package/dist/http/mcp/tools/community-members.js +9 -16
  42. package/dist/http/mcp/tools/community-members.js.map +1 -1
  43. package/dist/http/mcp/tools/community-moderation.js +13 -25
  44. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  45. package/dist/http/mcp/tools/courses.js +11 -21
  46. package/dist/http/mcp/tools/courses.js.map +1 -1
  47. package/dist/http/mcp/tools/events.d.ts +6 -0
  48. package/dist/http/mcp/tools/events.js +170 -0
  49. package/dist/http/mcp/tools/events.js.map +1 -0
  50. package/dist/http/mcp/tools/index.d.ts +5 -1
  51. package/dist/http/mcp/tools/index.js +45 -10
  52. package/dist/http/mcp/tools/index.js.map +1 -1
  53. package/dist/http/mcp/tools/organization.d.ts +6 -0
  54. package/dist/http/mcp/tools/organization.js +214 -0
  55. package/dist/http/mcp/tools/organization.js.map +1 -0
  56. package/dist/http/mcp/tools/people.js +30 -49
  57. package/dist/http/mcp/tools/people.js.map +1 -1
  58. package/dist/http/plugins/auth.js +37 -8
  59. package/dist/http/plugins/auth.js.map +1 -1
  60. package/dist/http/plugins/error-handler.d.ts +5 -1
  61. package/dist/http/plugins/error-handler.js +10 -2
  62. package/dist/http/plugins/error-handler.js.map +1 -1
  63. package/dist/http/plugins/openapi.js +18 -2
  64. package/dist/http/plugins/openapi.js.map +1 -1
  65. package/dist/http/plugins/platform.d.ts +7 -0
  66. package/dist/http/plugins/platform.js +13 -0
  67. package/dist/http/plugins/platform.js.map +1 -0
  68. package/dist/http/routes/access.js +54 -36
  69. package/dist/http/routes/access.js.map +1 -1
  70. package/dist/http/routes/activities.js +45 -45
  71. package/dist/http/routes/activities.js.map +1 -1
  72. package/dist/http/routes/assets.js +30 -65
  73. package/dist/http/routes/assets.js.map +1 -1
  74. package/dist/http/routes/automations.js +37 -42
  75. package/dist/http/routes/automations.js.map +1 -1
  76. package/dist/http/routes/bundles.js +33 -36
  77. package/dist/http/routes/bundles.js.map +1 -1
  78. package/dist/http/routes/communities.js +93 -145
  79. package/dist/http/routes/communities.js.map +1 -1
  80. package/dist/http/routes/community-moderation.js +83 -137
  81. package/dist/http/routes/community-moderation.js.map +1 -1
  82. package/dist/http/routes/course-analytics.js +32 -15
  83. package/dist/http/routes/course-analytics.js.map +1 -1
  84. package/dist/http/routes/courses.js +25 -32
  85. package/dist/http/routes/courses.js.map +1 -1
  86. package/dist/http/routes/dashboard.js +9 -9
  87. package/dist/http/routes/dashboard.js.map +1 -1
  88. package/dist/http/routes/discussion.js +21 -39
  89. package/dist/http/routes/discussion.js.map +1 -1
  90. package/dist/http/routes/downloads.js +41 -47
  91. package/dist/http/routes/downloads.js.map +1 -1
  92. package/dist/http/routes/entitlements.js +19 -23
  93. package/dist/http/routes/entitlements.js.map +1 -1
  94. package/dist/http/routes/events.d.ts +7 -0
  95. package/dist/http/routes/events.js +333 -0
  96. package/dist/http/routes/events.js.map +1 -0
  97. package/dist/http/routes/integrations.js +40 -73
  98. package/dist/http/routes/integrations.js.map +1 -1
  99. package/dist/http/routes/member/assets.js +10 -13
  100. package/dist/http/routes/member/assets.js.map +1 -1
  101. package/dist/http/routes/member/chats.js +170 -47
  102. package/dist/http/routes/member/chats.js.map +1 -1
  103. package/dist/http/routes/member/comments.js +65 -26
  104. package/dist/http/routes/member/comments.js.map +1 -1
  105. package/dist/http/routes/member/community.js +45 -52
  106. package/dist/http/routes/member/community.js.map +1 -1
  107. package/dist/http/routes/member/courses.js +12 -19
  108. package/dist/http/routes/member/courses.js.map +1 -1
  109. package/dist/http/routes/member/devices.js +9 -10
  110. package/dist/http/routes/member/devices.js.map +1 -1
  111. package/dist/http/routes/member/downloads.js +6 -9
  112. package/dist/http/routes/member/downloads.js.map +1 -1
  113. package/dist/http/routes/member/events.d.ts +7 -0
  114. package/dist/http/routes/member/events.js +386 -0
  115. package/dist/http/routes/member/events.js.map +1 -0
  116. package/dist/http/routes/member/files.js +71 -46
  117. package/dist/http/routes/member/files.js.map +1 -1
  118. package/dist/http/routes/member/index.js +2 -0
  119. package/dist/http/routes/member/index.js.map +1 -1
  120. package/dist/http/routes/member/notification-settings.js +15 -23
  121. package/dist/http/routes/member/notification-settings.js.map +1 -1
  122. package/dist/http/routes/member/notifications.js +16 -16
  123. package/dist/http/routes/member/notifications.js.map +1 -1
  124. package/dist/http/routes/member/org.js +5 -15
  125. package/dist/http/routes/member/org.js.map +1 -1
  126. package/dist/http/routes/member/posts.js +67 -68
  127. package/dist/http/routes/member/posts.js.map +1 -1
  128. package/dist/http/routes/member/profile.js +15 -6
  129. package/dist/http/routes/member/profile.js.map +1 -1
  130. package/dist/http/routes/member/progress.js +7 -11
  131. package/dist/http/routes/member/progress.js.map +1 -1
  132. package/dist/http/routes/organizations.js +319 -135
  133. package/dist/http/routes/organizations.js.map +1 -1
  134. package/dist/http/routes/platform.d.ts +7 -0
  135. package/dist/http/routes/platform.js +61 -0
  136. package/dist/http/routes/platform.js.map +1 -0
  137. package/dist/http/routes/students.js +12 -75
  138. package/dist/http/routes/students.js.map +1 -1
  139. package/dist/http/routes/webhooks.js +30 -39
  140. package/dist/http/routes/webhooks.js.map +1 -1
  141. package/dist/http/routes.js +9 -0
  142. package/dist/http/routes.js.map +1 -1
  143. package/dist/http/schemas/access.d.ts +346 -64
  144. package/dist/http/schemas/access.js +39 -12
  145. package/dist/http/schemas/access.js.map +1 -1
  146. package/dist/http/schemas/assets.d.ts +19 -14
  147. package/dist/http/schemas/assets.js +6 -5
  148. package/dist/http/schemas/assets.js.map +1 -1
  149. package/dist/http/schemas/automations.d.ts +4 -4
  150. package/dist/http/schemas/chats.d.ts +211 -32
  151. package/dist/http/schemas/chats.js +13 -4
  152. package/dist/http/schemas/chats.js.map +1 -1
  153. package/dist/http/schemas/community.d.ts +91 -44
  154. package/dist/http/schemas/community.js +21 -8
  155. package/dist/http/schemas/community.js.map +1 -1
  156. package/dist/http/schemas/course-analytics.d.ts +51 -1
  157. package/dist/http/schemas/course-analytics.js +19 -1
  158. package/dist/http/schemas/course-analytics.js.map +1 -1
  159. package/dist/http/schemas/dashboard.d.ts +1 -0
  160. package/dist/http/schemas/dashboard.js +2 -1
  161. package/dist/http/schemas/dashboard.js.map +1 -1
  162. package/dist/http/schemas/discussion.d.ts +22 -72
  163. package/dist/http/schemas/discussion.js +2 -2
  164. package/dist/http/schemas/discussion.js.map +1 -1
  165. package/dist/http/schemas/entitlements.d.ts +9 -12
  166. package/dist/http/schemas/entitlements.js +11 -5
  167. package/dist/http/schemas/entitlements.js.map +1 -1
  168. package/dist/http/schemas/events.d.ts +527 -0
  169. package/dist/http/schemas/events.js +64 -0
  170. package/dist/http/schemas/events.js.map +1 -0
  171. package/dist/http/schemas/files.d.ts +15 -5
  172. package/dist/http/schemas/files.js +10 -2
  173. package/dist/http/schemas/files.js.map +1 -1
  174. package/dist/http/schemas/index.d.ts +11 -9
  175. package/dist/http/schemas/index.js +11 -9
  176. package/dist/http/schemas/integrations.d.ts +4 -4
  177. package/dist/http/schemas/invites.d.ts +3 -47
  178. package/dist/http/schemas/invites.js +4 -25
  179. package/dist/http/schemas/invites.js.map +1 -1
  180. package/dist/http/schemas/member.d.ts +94 -7
  181. package/dist/http/schemas/member.js +7 -3
  182. package/dist/http/schemas/member.js.map +1 -1
  183. package/dist/http/schemas/members.d.ts +333 -69
  184. package/dist/http/schemas/members.js +67 -25
  185. package/dist/http/schemas/members.js.map +1 -1
  186. package/dist/http/schemas/notifications.d.ts +57 -53
  187. package/dist/http/schemas/platform.d.ts +37 -0
  188. package/dist/http/schemas/platform.js +19 -0
  189. package/dist/http/schemas/platform.js.map +1 -0
  190. package/dist/http/schemas/posts.d.ts +55 -35
  191. package/dist/http/schemas/posts.js +4 -4
  192. package/dist/http/schemas/posts.js.map +1 -1
  193. package/dist/http/schemas/profile.d.ts +1 -1
  194. package/dist/http/schemas/shared.d.ts +1 -11
  195. package/dist/http/schemas/shared.js +1 -10
  196. package/dist/http/schemas/shared.js.map +1 -1
  197. package/dist/http/schemas/students.d.ts +25 -3
  198. package/dist/http/schemas/students.js +3 -1
  199. package/dist/http/schemas/students.js.map +1 -1
  200. package/dist/http/scope.d.ts +9 -13
  201. package/dist/http/scope.js +29 -40
  202. package/dist/http/scope.js.map +1 -1
  203. package/dist/http/server.js +9 -0
  204. package/dist/http/server.js.map +1 -1
  205. package/dist/http/shutdown.d.ts +11 -0
  206. package/dist/http/shutdown.js +40 -0
  207. package/dist/http/shutdown.js.map +1 -0
  208. package/dist/http/sse.d.ts +19 -0
  209. package/dist/http/sse.js +104 -0
  210. package/dist/http/sse.js.map +1 -0
  211. package/dist/http/test-caller.d.ts +13 -0
  212. package/dist/http/test-caller.js +14 -0
  213. package/dist/http/test-caller.js.map +1 -0
  214. package/dist/http/well-known.js +2 -2
  215. package/dist/http/well-known.js.map +1 -1
  216. package/dist/index.d.ts +4 -2
  217. package/dist/index.js +4 -2
  218. package/dist/index.js.map +1 -1
  219. package/dist/packages/server/package.js +1 -1
  220. package/package.json +7 -6
  221. package/dist/http/routes/member/actor.d.ts +0 -14
  222. package/dist/http/routes/member/actor.js +0 -11
  223. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"communities.js","names":[],"sources":["../../../src/http/routes/communities.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveConfiguratorScope, resolveScope } from '../scope.js';\nimport {\n Community,\n CommunityAnalytics,\n CommunityIdParam,\n CommunitySpaceParam,\n CommunitiesPage,\n CommunitiesQuery,\n CreateCommunity,\n CreateSpace,\n MessageQueue,\n NotificationCategoryList,\n OrderedIds,\n PatchCommunitySettings,\n ReplaceNotificationCategories,\n SetSpaceTopics,\n SpaceDetail,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communitiesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities',\n schema: {\n operationId: 'listCommunities',\n tags,\n summary: 'List communities (staff)',\n description:\n 'Staff only (owner, admin, instructor). Pages every community in the organization, drafts included, filtered by `status` and searched by title.',\n querystring: CommunitiesQuery,\n response: { 200: CommunitiesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listCommunities(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities',\n schema: {\n operationId: 'createCommunity',\n tags,\n summary: 'Create a community (staff)',\n description:\n 'Owner or admin only. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.',\n body: CreateCommunity,\n response: { 201: Community, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const community = await content.createCommunity(scope.orgId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n req.log.info({ communityId: community.id }, 'community created');\n return reply.code(201).send(community);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'getCommunity',\n tags,\n summary: 'Get a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Any community of the organization, drafts included.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const community = await content.getCommunity(scope.orgId, req.params.communityId);\n if (!community) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return community;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'updateCommunity',\n tags,\n summary: 'Update a community (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.',\n params: CommunityIdParam,\n body: UpdateCommunity,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateCommunity(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'deleteCommunity',\n tags,\n summary: 'Delete a community (staff)',\n description:\n 'Owner or admin only. Links to the community from other spaces are removed with it.',\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/settings',\n schema: {\n operationId: 'patchCommunitySettings',\n tags,\n summary: 'Change the direct-message and digest settings of a community (staff)',\n description:\n 'Owner or admin only. Changes only the settings sent; the rest keep their values. The direct-message settings decide who may message whom from then on. `digest` sets when the community digest goes out, or `null` stops it; the first digest after a change goes out at the next slot.',\n params: CommunityIdParam,\n body: PatchCommunitySettings,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.patchCommunitySettings(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'getCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Get a community's notification categories (staff)\",\n description:\n 'Owner or admin only. The list in use for the community, with `source` saying whether it is the default or a stored list.',\n params: CommunityIdParam,\n response: { 200: NotificationCategoryList, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveConfiguratorScope(container, req);\n return container.notificationCategories.community.get(scope.orgId, req.params.communityId);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'replaceCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Replace a community's notification categories (staff)\",\n description:\n 'Owner or admin only. Stores the whole list for this community; other communities keep theirs. 422 when a key repeats, a type cannot be in a community list, a reason is not allowed for its type, a channel is not configured, `perSpace` is set on a type that cannot occur in a space, `spaceDefaults` appears without `perSpace` or names a channel the category does not offer, or earlier categories already cover everything a category covers.',\n params: CommunityIdParam,\n body: ReplaceNotificationCategories,\n response: { 200: NotificationCategoryList, ...refusals(400, 403, 404, 422) },\n },\n handler: async (req) => {\n const scope = await resolveConfiguratorScope(container, req);\n return container.notificationCategories.community.replace(\n scope.orgId,\n req.params.communityId,\n req.body.categories,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'resetCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: 'Go back to the default notification categories for a community (staff)',\n description: 'Owner or admin only. Removes the stored list; the default applies again.',\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveConfiguratorScope(container, req);\n await container.notificationCategories.community.reset(scope.orgId, req.params.communityId);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/publish',\n schema: {\n operationId: 'publishCommunity',\n tags,\n summary: 'Publish a community (staff)',\n description: 'Owner or admin only. Makes the community visible to the people who hold it.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.publishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/unpublish',\n schema: {\n operationId: 'unpublishCommunity',\n tags,\n summary: 'Unpublish a community (staff)',\n description:\n 'Owner or admin only. Hides the community from members again; nothing in it is deleted.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.unpublishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/analytics',\n schema: {\n operationId: 'getCommunityAnalytics',\n tags,\n summary: 'Staff analytics for a community',\n description:\n 'Staff only (owner, admin, instructor). Membership, activity and moderation counts for one community.',\n params: CommunityIdParam,\n response: { 200: CommunityAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const stats = await container.reporting.admin.communityAnalytics(\n scope.orgId,\n req.params.communityId,\n );\n if (!stats) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return stats;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/message-queue',\n schema: {\n operationId: 'getCommunityMessageQueue',\n tags,\n summary: 'Open message flags in a community (staff)',\n description:\n \"Staff only (owner, admin, instructor). The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.\",\n params: CommunityIdParam,\n response: { 200: MessageQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.chats.messageQueue(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'listCommunitySpaces',\n tags,\n summary: 'List the spaces in a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Every space in order, with its topics and links.',\n params: CommunityIdParam,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listSpaces(scope.orgId, req.params.communityId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'createSpace',\n tags,\n summary: 'Create a space (staff)',\n description:\n 'Owner or admin only. The space is added last. An `all_members` space cannot have a capacity (409).',\n params: CommunityIdParam,\n body: CreateSpace,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.createSpace(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/order',\n schema: {\n operationId: 'reorderSpaces',\n tags,\n summary: 'Reorder the spaces in a community (staff)',\n description:\n 'Owner or admin only. Send every space id of the community exactly once (409 otherwise).',\n params: CommunityIdParam,\n body: OrderedIds,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaces(scope.orgId, req.params.communityId, req.body.ids, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'getSpace',\n tags,\n summary: 'Get a space (staff)',\n description: 'Staff only (owner, admin, instructor). The space with its topics and links.',\n params: CommunitySpaceParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const space = await content.getCommunitySpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n );\n if (!space) {\n throw new NotFoundError('Space', req.params.spaceId);\n }\n return space;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'updateSpace',\n tags,\n summary: 'Update a space (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent; `settings` merges into the current settings. An `all_members` space cannot have a capacity (409).',\n params: CommunitySpaceParam,\n body: UpdateSpace,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateSpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'deleteSpace',\n tags,\n summary: 'Delete a space (staff)',\n description: 'Owner or admin only. The space and everything in it go.',\n params: CommunitySpaceParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteSpace(scope.orgId, req.params.communityId, req.params.spaceId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/topics',\n schema: {\n operationId: 'setSpaceTopics',\n tags,\n summary: 'Replace the topics on a space (staff)',\n description:\n 'Owner or admin only. The list replaces the current topics: an entry with an `id` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).',\n params: CommunitySpaceParam,\n body: SetSpaceTopics,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.setSpaceTopics(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/links',\n schema: {\n operationId: 'addSpaceLink',\n tags,\n summary: 'Link a course, download, or community into a space (staff)',\n description:\n 'Owner or admin only. The link is added last. Linking the same content twice is a 409.',\n params: CommunitySpaceParam,\n body: SpaceLinkBody,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.addSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.contentId,\n { id: scope.orgUserId, role: scope.role },\n );\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId/links/:linkId',\n schema: {\n operationId: 'removeSpaceLink',\n tags,\n summary: 'Remove a link from a space (staff)',\n description: 'Owner or admin only. The linked content itself is untouched.',\n params: SpaceLinkParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.removeSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.params.linkId,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/links/order',\n schema: {\n operationId: 'reorderSpaceLinks',\n tags,\n summary: 'Reorder the links in a space (staff)',\n description:\n 'Owner or admin only. Send every link id of the space exactly once (409 otherwise).',\n params: CommunitySpaceParam,\n body: OrderedIds,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaceLinks(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.ids,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"communities.js","names":[],"sources":["../../../src/http/routes/communities.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\nimport {\n Community,\n CommunityAnalytics,\n CommunityIdParam,\n CommunitySpaceParam,\n CommunitiesPage,\n CommunitiesQuery,\n CreateCommunity,\n CreateSpace,\n MessageQueue,\n NotificationCategoryList,\n OrderedIds,\n PatchCommunitySettings,\n ReplaceNotificationCategories,\n SetSpaceTopics,\n SpaceDetail,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communitiesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities',\n schema: {\n operationId: 'listCommunities',\n tags,\n summary: 'List communities',\n description: `Requires one of \\`communities:read\\`, \\`communities:moderate\\`. Pages every community in the organization, drafts included, filtered by \\`status\\` and searched by title.`,\n querystring: CommunitiesQuery,\n response: { 200: CommunitiesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listCommunities(req.query, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities',\n schema: {\n operationId: 'createCommunity',\n tags,\n summary: 'Create a community',\n description: `Requires \\`communities:write\\`. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.`,\n body: CreateCommunity,\n response: { 201: Community, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const community = await content.createCommunity(req.body, actor);\n req.log.info({ communityId: community.id }, 'community created');\n return reply.code(201).send(community);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'getCommunity',\n tags,\n summary: 'Get a community',\n description: `Requires one of \\`communities:read\\`, \\`communities:moderate\\`. Any community of the organization, drafts included.`,\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.getCommunity(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'updateCommunity',\n tags,\n summary: 'Update a community',\n description: `Requires \\`communities:write\\`. Changes only the fields sent. \\`thumbnailAssetId\\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise).`,\n params: CommunityIdParam,\n body: UpdateCommunity,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateCommunity(req.params.communityId, req.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'deleteCommunity',\n tags,\n summary: 'Delete a community',\n description: `Requires \\`communities:delete\\`. Links to the community from other spaces are removed with it.`,\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.deleteCommunity(req.params.communityId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/settings',\n schema: {\n operationId: 'patchCommunitySettings',\n tags,\n summary: 'Change the direct-message and digest settings of a community',\n description: `Requires \\`communities:write\\`. Changes only the settings sent; the rest keep their values. The direct-message settings decide who may message whom from then on. \\`digest\\` sets when the community digest goes out, or \\`null\\` stops it; the first digest after a change goes out at the next slot.`,\n params: CommunityIdParam,\n body: PatchCommunitySettings,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.patchCommunitySettings(req.params.communityId, req.body, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'getCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Get a community's notification categories\",\n description: `Requires \\`communities:read\\`. On the community. The list in use for the community, with \\`source\\` saying whether it is the default or a stored list.`,\n params: CommunityIdParam,\n response: { 200: NotificationCategoryList, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCategories.community.get(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'replaceCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Replace a community's notification categories\",\n description: `Requires \\`communities:write\\`. On the community. Stores the whole list for this community; other communities keep theirs. 422 when a key repeats, a type cannot be in a community list, a reason is not allowed for its type, a channel is not configured, \\`perSpace\\` is set on a type that cannot occur in a space, \\`spaceDefaults\\` appears without \\`perSpace\\` or names a channel the category does not offer, or earlier categories already cover everything a category covers.`,\n params: CommunityIdParam,\n body: ReplaceNotificationCategories,\n response: { 200: NotificationCategoryList, ...refusals(400, 403, 404, 422) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCategories.community.replace(\n req.params.communityId,\n req.body.categories,\n actor,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'resetCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: 'Go back to the default notification categories for a community',\n description: `Requires \\`communities:write\\`. On the community. Removes the stored list; the default applies again.`,\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.notificationCategories.community.reset(req.params.communityId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/publish',\n schema: {\n operationId: 'publishCommunity',\n tags,\n summary: 'Publish a community',\n description: `Requires \\`communities:write\\`. Makes the community visible to the people who hold it.`,\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.publishCommunity(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/unpublish',\n schema: {\n operationId: 'unpublishCommunity',\n tags,\n summary: 'Unpublish a community',\n description: `Requires \\`communities:write\\`. Hides the community from members again; nothing in it is deleted.`,\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.unpublishCommunity(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/analytics',\n schema: {\n operationId: 'getCommunityAnalytics',\n tags,\n summary: 'Analytics for a community',\n description: `Requires \\`communities:read\\`. Membership, activity and moderation counts for one community.`,\n params: CommunityIdParam,\n response: { 200: CommunityAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.communityAnalytics(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/message-queue',\n schema: {\n operationId: 'getCommunityMessageQueue',\n tags,\n summary: 'Open message flags in a community',\n description: `Requires \\`communities:moderate\\`. The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.`,\n params: CommunityIdParam,\n response: { 200: MessageQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.chats.messageQueue(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'listCommunitySpaces',\n tags,\n summary: 'List the spaces in a community',\n description: `Requires one of \\`communities:read\\`, \\`communities:moderate\\`. Every space in order, with its topics and links.`,\n params: CommunityIdParam,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listSpaces(req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'createSpace',\n tags,\n summary: 'Create a space',\n description: `Requires \\`communities:write\\`. The space is added last. An \\`all_members\\` space cannot have a capacity (409).`,\n params: CommunityIdParam,\n body: CreateSpace,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const space = await content.createSpace(req.params.communityId, req.body, actor);\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/order',\n schema: {\n operationId: 'reorderSpaces',\n tags,\n summary: 'Reorder the spaces in a community',\n description: `Requires \\`communities:write\\`. Send every space id of the community exactly once (409 otherwise).`,\n params: CommunityIdParam,\n body: OrderedIds,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.reorderSpaces(req.params.communityId, req.body.ids, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'getSpace',\n tags,\n summary: 'Get a space',\n description: `Requires \\`communities:read\\`. The space with its topics and links.`,\n params: CommunitySpaceParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.getCommunitySpace(req.params.communityId, req.params.spaceId, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'updateSpace',\n tags,\n summary: 'Update a space',\n description: `Requires \\`communities:write\\`. Changes only the fields sent; \\`settings\\` merges into the current settings. An \\`all_members\\` space cannot have a capacity (409).`,\n params: CommunitySpaceParam,\n body: UpdateSpace,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateSpace(req.params.communityId, req.params.spaceId, req.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'deleteSpace',\n tags,\n summary: 'Delete a space',\n description: `Requires \\`communities:write\\`. The space and everything in it go.`,\n params: CommunitySpaceParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.deleteSpace(req.params.communityId, req.params.spaceId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/topics',\n schema: {\n operationId: 'setSpaceTopics',\n tags,\n summary: 'Replace the topics on a space',\n description: `Requires \\`communities:write\\`. The list replaces the current topics: an entry with an \\`id\\` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).`,\n params: CommunitySpaceParam,\n body: SetSpaceTopics,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.setSpaceTopics(req.params.communityId, req.params.spaceId, req.body, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/links',\n schema: {\n operationId: 'addSpaceLink',\n tags,\n summary: 'Link a course, download, or community into a space',\n description: `Requires \\`communities:write\\`. The link is added last. Linking the same content twice is a 409.`,\n params: CommunitySpaceParam,\n body: SpaceLinkBody,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const space = await content.addSpaceLink(\n req.params.communityId,\n req.params.spaceId,\n req.body.contentId,\n actor,\n );\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId/links/:linkId',\n schema: {\n operationId: 'removeSpaceLink',\n tags,\n summary: 'Remove a link from a space',\n description: `Requires \\`communities:write\\`. The linked content itself is untouched.`,\n params: SpaceLinkParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.removeSpaceLink(\n req.params.communityId,\n req.params.spaceId,\n req.params.linkId,\n actor,\n );\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/links/order',\n schema: {\n operationId: 'reorderSpaceLinks',\n tags,\n summary: 'Reorder the links in a space',\n description: `Requires \\`communities:write\\`. Send every link id of the space exactly once (409 otherwise).`,\n params: CommunitySpaceParam,\n body: OrderedIds,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.reorderSpaceLinks(\n req.params.communityId,\n req.params.spaceId,\n req.body.ids,\n actor,\n );\n },\n });\n}\n"],"mappings":";;;;;;;;AA4BA,eAAsB,kBAAkB,KAAsB,WAAqC;CACjG,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,UAAU,UAAU;CAC1B,MAAM,OAAO,CAAC,aAAa;CAE3B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAiB,GAAG,SAAS,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,gBAAgB,IAAI,OAAO,KAAK;EACjD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,YAAY,MAAM,QAAQ,gBAAgB,IAAI,MAAM,KAAK;GAC/D,IAAI,IAAI,KAAK,EAAE,aAAa,UAAU,GAAG,GAAG,mBAAmB;GAC/D,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,SAAS;EACvC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,aAAa,IAAI,OAAO,aAAa,KAAK;EAC3D;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,gBAAgB,IAAI,OAAO,aAAa,IAAI,MAAM,KAAK;EACxE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,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,gBAAgB,IAAI,OAAO,aAAa,KAAK;GAC3D,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,uBAAuB,IAAI,OAAO,aAAa,IAAI,MAAM,KAAK;EAC/E;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,KAAK,GAAG;GAAE;EACnE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,uBAAuB,UAAU,IAAI,IAAI,OAAO,aAAa,KAAK;EACrF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC7E;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,uBAAuB,UAAU,QAChD,IAAI,OAAO,aACX,IAAI,KAAK,YACT,KACF;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,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,UAAU,uBAAuB,UAAU,MAAM,IAAI,OAAO,aAAa,KAAK;GACpF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,iBAAiB,IAAI,OAAO,aAAa,KAAK;EAC/D;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAW,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,mBAAmB,IAAI,OAAO,aAAa,KAAK;EACjE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAoB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,mBAAmB,IAAI,OAAO,aAAa,KAAK;EACnF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAc,GAAG,SAAS,KAAK,GAAG;GAAE;EACvD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,MAAM,aAAa,IAAI,OAAO,aAAa,KAAK;EACnE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,MAAM,WAAW;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAC/D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,WAAW,IAAI,OAAO,aAAa,KAAK;EACzD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,MAAM,QAAQ,YAAY,IAAI,OAAO,aAAa,IAAI,MAAM,KAAK;GAC/E,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK;EACnC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,WAAW;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACpE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,cAAc,IAAI,OAAO,aAAa,IAAI,KAAK,KAAK,KAAK;EAC1E;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,kBAAkB,IAAI,OAAO,aAAa,IAAI,OAAO,SAAS,KAAK;EACpF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,IAAI,OAAO,aAAa,IAAI,OAAO,SAAS,IAAI,MAAM,KAAK;EACxF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,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,aAAa,IAAI,OAAO,SAAS,KAAK;GAC3E,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,eAAe,IAAI,OAAO,aAAa,IAAI,OAAO,SAAS,IAAI,MAAM,KAAK;EAC3F;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,MAAM,QAAQ,aAC1B,IAAI,OAAO,aACX,IAAI,OAAO,SACX,IAAI,KAAK,WACT,KACF;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK;EACnC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,gBACb,IAAI,OAAO,aACX,IAAI,OAAO,SACX,IAAI,OAAO,QACX,KACF;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,kBACb,IAAI,OAAO,aACX,IAAI,OAAO,SACX,IAAI,KAAK,KACT,KACF;EACF;CACF,CAAC;AACH"}
@@ -1,12 +1,11 @@
1
1
  import { PageQuery, refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveActor } from "../scope.js";
3
3
  import { CommentView, ResolveCommentFlags } from "../schemas/discussion.js";
4
4
  import { CommunityChatParam, CommunityCommentParam, CommunityIdParam, CommunityMembersPage, CommunityMembersQuery, CommunityMessageParam, CommunityPostParam, CommunitySpaceParam, SpaceMember, SpaceMemberParam, SpaceMembersPage } from "../schemas/community.js";
5
5
  import { PostView, ResolvePostFlags, SpaceQueue } from "../schemas/posts.js";
6
6
  import { Chat, OrgUserBody, ResolveMessageFlags } from "../schemas/chats.js";
7
7
  import "../schemas/index.js";
8
8
  import { z } from "zod";
9
- import { NotFoundError } from "@headless-lms/core/shared/errors";
10
9
  //#region src/http/routes/community-moderation.ts
11
10
  async function communityModerationRoutes(app, container) {
12
11
  const r = app.withTypeProvider();
@@ -17,8 +16,8 @@ async function communityModerationRoutes(app, container) {
17
16
  schema: {
18
17
  operationId: "listCommunityMembers",
19
18
  tags,
20
- summary: "List the people who hold a community (staff)",
21
- description: "Staff only (owner, admin, instructor). Everyone with active access to the community, granted directly or through a bundle, paged and searched by name or email.",
19
+ summary: "List the people who hold a community",
20
+ description: `Requires \`access:read\`. Everyone with active access to the community, granted directly or through a bundle, paged and searched by name or email.`,
22
21
  params: CommunityIdParam,
23
22
  querystring: CommunityMembersQuery,
24
23
  response: {
@@ -27,10 +26,8 @@ async function communityModerationRoutes(app, container) {
27
26
  }
28
27
  },
29
28
  handler: async (req) => {
30
- const scope = await resolveScope(container, req);
31
- const page = await container.reporting.admin.communityMembers(scope.orgId, req.params.communityId, req.query);
32
- if (!page) throw new NotFoundError("Community", req.params.communityId);
33
- return page;
29
+ const actor = await resolveActor(container, req);
30
+ return container.reporting.admin.communityMembers(req.params.communityId, req.query, actor);
34
31
  }
35
32
  });
36
33
  r.route({
@@ -39,8 +36,8 @@ async function communityModerationRoutes(app, container) {
39
36
  schema: {
40
37
  operationId: "listCommunitySpaceMembers",
41
38
  tags,
42
- summary: "List the members, requests and invitations of a space (staff)",
43
- description: "Staff only (owner, admin, instructor). The space's members, its open requests to join and its open invitations.",
39
+ summary: "List the members, requests and invitations of a space",
40
+ description: `For a person who can open the space. Its members; a holder of \`communities:moderate\` on the community also sees its open requests to join and open invitations. People who open every space of the community through their role are not listed. 404 when you cannot open the space.`,
44
41
  params: CommunitySpaceParam,
45
42
  querystring: PageQuery,
46
43
  response: {
@@ -49,11 +46,8 @@ async function communityModerationRoutes(app, container) {
49
46
  }
50
47
  },
51
48
  handler: async (req) => {
52
- const scope = await resolveScope(container, req);
53
- return container.entitlements.listSpaceMembers(scope.orgId, {
54
- id: scope.orgUserId,
55
- role: scope.role
56
- }, req.params.communityId, req.params.spaceId, req.query);
49
+ const actor = await resolveActor(container, req);
50
+ return container.entitlements.listSpaceMembers(req.params.communityId, req.params.spaceId, req.query, actor);
57
51
  }
58
52
  });
59
53
  r.route({
@@ -62,8 +56,8 @@ async function communityModerationRoutes(app, container) {
62
56
  schema: {
63
57
  operationId: "addCommunitySpaceMember",
64
58
  tags,
65
- summary: "Add a person to a space (staff)",
66
- description: "Staff only (owner, admin, instructor). Places a member of the community straight into the space, with no request or invitation. 409 when the space is open to all members, the person is already placed, or the space is full.",
59
+ summary: "Add a person to a space",
60
+ description: `Requires \`communities:moderate\`. Places a member of the community straight into the space, with no request or invitation. 409 when the space is open to all members, the person is already placed, or the space is full.`,
67
61
  params: CommunitySpaceParam,
68
62
  body: OrgUserBody,
69
63
  response: {
@@ -72,11 +66,8 @@ async function communityModerationRoutes(app, container) {
72
66
  }
73
67
  },
74
68
  handler: async (req, reply) => {
75
- const scope = await resolveScope(container, req);
76
- const member = await container.entitlements.addSpaceMember(scope.orgId, {
77
- id: scope.orgUserId,
78
- role: scope.role
79
- }, req.params.communityId, req.params.spaceId, req.body.orgUserId);
69
+ const actor = await resolveActor(container, req);
70
+ const member = await container.entitlements.addSpaceMember(req.params.communityId, req.params.spaceId, req.body.orgUserId, actor);
80
71
  return reply.code(201).send(member);
81
72
  }
82
73
  });
@@ -86,8 +77,8 @@ async function communityModerationRoutes(app, container) {
86
77
  schema: {
87
78
  operationId: "inviteCommunitySpaceMember",
88
79
  tags,
89
- summary: "Invite a person into a space (staff)",
90
- description: "Staff only (owner, admin, instructor). Invites a member of the community into a `by_invite` space; they join when they accept. 403 when the person is staff or does not hold the community; 409 when the space is not invite-only or the person is already placed.",
80
+ summary: "Invite a person into a space",
81
+ description: `Requires \`communities:moderate\`. Invites a member of the community into a \`by_invite\` space; they join when they accept. 403 when the person opens every space or is not a member of the community; 409 when the space is not invite-only or the person is already placed.`,
91
82
  params: CommunitySpaceParam,
92
83
  body: OrgUserBody,
93
84
  response: {
@@ -96,22 +87,19 @@ async function communityModerationRoutes(app, container) {
96
87
  }
97
88
  },
98
89
  handler: async (req, reply) => {
99
- const scope = await resolveScope(container, req);
100
- const member = await container.entitlements.invite(scope.orgId, {
101
- id: scope.orgUserId,
102
- role: scope.role
103
- }, req.params.communityId, req.params.spaceId, req.body.orgUserId);
90
+ const actor = await resolveActor(container, req);
91
+ const member = await container.entitlements.invite(req.params.communityId, req.params.spaceId, req.body.orgUserId, actor);
104
92
  return reply.code(201).send(member);
105
93
  }
106
94
  });
107
95
  r.route({
108
96
  method: "POST",
109
- url: "/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/approval",
97
+ url: "/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/approve",
110
98
  schema: {
111
99
  operationId: "approveCommunitySpaceRequest",
112
100
  tags,
113
- summary: "Approve a person's request to join a space (staff)",
114
- description: "Staff only (owner, admin, instructor). Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.",
101
+ summary: "Approve a person's request to join a space",
102
+ description: `Requires \`communities:moderate\`. Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.`,
115
103
  params: SpaceMemberParam,
116
104
  response: {
117
105
  200: SpaceMember,
@@ -119,11 +107,8 @@ async function communityModerationRoutes(app, container) {
119
107
  }
120
108
  },
121
109
  handler: async (req) => {
122
- const scope = await resolveScope(container, req);
123
- return container.entitlements.approve(scope.orgId, {
124
- id: scope.orgUserId,
125
- role: scope.role
126
- }, req.params.communityId, req.params.spaceId, req.params.orgUserId);
110
+ const actor = await resolveActor(container, req);
111
+ return container.entitlements.approve(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
127
112
  }
128
113
  });
129
114
  r.route({
@@ -132,8 +117,8 @@ async function communityModerationRoutes(app, container) {
132
117
  schema: {
133
118
  operationId: "declineCommunitySpaceRequest",
134
119
  tags,
135
- summary: "Decline a person's request to join a space (staff)",
136
- description: "Staff only (owner, admin, instructor). Closes the open request. 404 when the person has no open request; 409 when they are already a member.",
120
+ summary: "Decline a person's request to join a space",
121
+ description: `Requires \`communities:moderate\`. Closes the open request. 404 when the person has no open request; 409 when they are already a member.`,
137
122
  params: SpaceMemberParam,
138
123
  response: {
139
124
  200: SpaceMember,
@@ -141,21 +126,18 @@ async function communityModerationRoutes(app, container) {
141
126
  }
142
127
  },
143
128
  handler: async (req) => {
144
- const scope = await resolveScope(container, req);
145
- return container.entitlements.decline(scope.orgId, {
146
- id: scope.orgUserId,
147
- role: scope.role
148
- }, req.params.communityId, req.params.spaceId, req.params.orgUserId);
129
+ const actor = await resolveActor(container, req);
130
+ return container.entitlements.decline(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
149
131
  }
150
132
  });
151
133
  r.route({
152
134
  method: "POST",
153
- url: "/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/removal",
135
+ url: "/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/remove",
154
136
  schema: {
155
137
  operationId: "removeCommunitySpaceMember",
156
138
  tags,
157
- summary: "Remove a person from a space (staff)",
158
- description: "Staff only (owner, admin, instructor). Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.",
139
+ summary: "Remove a person from a space",
140
+ description: `Requires \`communities:moderate\`. Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.`,
159
141
  params: SpaceMemberParam,
160
142
  response: {
161
143
  200: SpaceMember,
@@ -163,11 +145,8 @@ async function communityModerationRoutes(app, container) {
163
145
  }
164
146
  },
165
147
  handler: async (req) => {
166
- const scope = await resolveScope(container, req);
167
- return container.entitlements.remove(scope.orgId, {
168
- id: scope.orgUserId,
169
- role: scope.role
170
- }, req.params.communityId, req.params.spaceId, req.params.orgUserId);
148
+ const actor = await resolveActor(container, req);
149
+ return container.entitlements.remove(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
171
150
  }
172
151
  });
173
152
  r.route({
@@ -176,8 +155,8 @@ async function communityModerationRoutes(app, container) {
176
155
  schema: {
177
156
  operationId: "getCommunitySpaceQueue",
178
157
  tags,
179
- summary: "The moderation queue of a space (staff)",
180
- description: "Staff only (owner, admin, instructor). Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.",
158
+ summary: "The moderation queue of a space",
159
+ description: `Requires \`communities:moderate\`. Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.`,
181
160
  params: CommunitySpaceParam,
182
161
  response: {
183
162
  200: SpaceQueue,
@@ -185,21 +164,18 @@ async function communityModerationRoutes(app, container) {
185
164
  }
186
165
  },
187
166
  handler: async (req) => {
188
- const scope = await resolveScope(container, req);
189
- return container.posts.moderationQueue(scope.orgId, {
190
- id: scope.orgUserId,
191
- role: scope.role
192
- }, req.params.communityId, req.params.spaceId);
167
+ const actor = await resolveActor(container, req);
168
+ return container.posts.moderationQueue(req.params.communityId, req.params.spaceId, actor);
193
169
  }
194
170
  });
195
171
  r.route({
196
172
  method: "POST",
197
- url: "/api/communities/:communityId/posts/:postId/approval",
173
+ url: "/api/communities/:communityId/posts/:postId/approve",
198
174
  schema: {
199
175
  operationId: "approveCommunityPost",
200
176
  tags,
201
- summary: "Publish a post that is awaiting review (staff)",
202
- description: "Staff only (owner, admin, instructor). 409 when the post is not awaiting review.",
177
+ summary: "Publish a post that is awaiting review",
178
+ description: `Requires \`communities:moderate\`. 409 when the post is not awaiting review.`,
203
179
  params: CommunityPostParam,
204
180
  response: {
205
181
  200: PostView,
@@ -207,21 +183,18 @@ async function communityModerationRoutes(app, container) {
207
183
  }
208
184
  },
209
185
  handler: async (req) => {
210
- const scope = await resolveScope(container, req);
211
- return container.posts.approve(scope.orgId, req.params.postId, {
212
- id: scope.orgUserId,
213
- role: scope.role
214
- }, req.params.communityId);
186
+ const actor = await resolveActor(container, req);
187
+ return container.posts.approve(req.params.postId, req.params.communityId, actor);
215
188
  }
216
189
  });
217
190
  r.route({
218
191
  method: "POST",
219
- url: "/api/communities/:communityId/posts/:postId/removal",
192
+ url: "/api/communities/:communityId/posts/:postId/remove",
220
193
  schema: {
221
194
  operationId: "removeCommunityPost",
222
195
  tags,
223
- summary: "Remove a post (staff)",
224
- description: "Staff only (owner, admin, instructor). Takes a published or pending post down and resolves its open flags as `removed`. 409 when it is already removed or was never submitted.",
196
+ summary: "Remove a post",
197
+ description: `The author can remove their own post; anyone else needs \`communities:moderate\` on the community. Takes a published or pending post down; removing someone else's post resolves its open flags as \`removed\`. 409 when it is already removed or was never submitted.`,
225
198
  params: CommunityPostParam,
226
199
  response: {
227
200
  200: PostView,
@@ -229,21 +202,18 @@ async function communityModerationRoutes(app, container) {
229
202
  }
230
203
  },
231
204
  handler: async (req) => {
232
- const scope = await resolveScope(container, req);
233
- return container.posts.remove(scope.orgId, req.params.postId, {
234
- id: scope.orgUserId,
235
- role: scope.role
236
- }, req.params.communityId);
205
+ const actor = await resolveActor(container, req);
206
+ return container.posts.remove(req.params.postId, req.params.communityId, actor);
237
207
  }
238
208
  });
239
209
  r.route({
240
210
  method: "POST",
241
- url: "/api/communities/:communityId/posts/:postId/restoration",
211
+ url: "/api/communities/:communityId/posts/:postId/restore",
242
212
  schema: {
243
213
  operationId: "restoreCommunityPost",
244
214
  tags,
245
- summary: "Restore a removed post (staff)",
246
- description: "Staff only (owner, admin, instructor). 409 when the post is not removed.",
215
+ summary: "Restore a removed post",
216
+ description: `Requires \`communities:moderate\`. 409 when the post is not removed.`,
247
217
  params: CommunityPostParam,
248
218
  response: {
249
219
  200: PostView,
@@ -251,21 +221,18 @@ async function communityModerationRoutes(app, container) {
251
221
  }
252
222
  },
253
223
  handler: async (req) => {
254
- const scope = await resolveScope(container, req);
255
- return container.posts.restore(scope.orgId, req.params.postId, {
256
- id: scope.orgUserId,
257
- role: scope.role
258
- }, req.params.communityId);
224
+ const actor = await resolveActor(container, req);
225
+ return container.posts.restore(req.params.postId, req.params.communityId, actor);
259
226
  }
260
227
  });
261
228
  r.route({
262
229
  method: "POST",
263
- url: "/api/communities/:communityId/posts/:postId/flags/resolution",
230
+ url: "/api/communities/:communityId/posts/:postId/flags/resolve",
264
231
  schema: {
265
232
  operationId: "resolveCommunityPostFlags",
266
233
  tags,
267
- summary: "Dismiss the open flags on a post (staff)",
268
- description: "Staff only (owner, admin, instructor). Closes every open flag on the post and leaves it up.",
234
+ summary: "Dismiss the open flags on a post",
235
+ description: `Requires \`communities:moderate\`. Closes every open flag on the post and leaves it up.`,
269
236
  params: CommunityPostParam,
270
237
  body: ResolvePostFlags,
271
238
  response: {
@@ -274,22 +241,19 @@ async function communityModerationRoutes(app, container) {
274
241
  }
275
242
  },
276
243
  handler: async (req, reply) => {
277
- const scope = await resolveScope(container, req);
278
- await container.posts.resolveFlags(scope.orgId, req.params.postId, {
279
- id: scope.orgUserId,
280
- role: scope.role
281
- }, req.body.resolution, req.params.communityId);
244
+ const actor = await resolveActor(container, req);
245
+ await container.posts.resolveFlags(req.params.postId, req.body.resolution, req.params.communityId, actor);
282
246
  return reply.code(204).send();
283
247
  }
284
248
  });
285
249
  r.route({
286
250
  method: "POST",
287
- url: "/api/communities/:communityId/comments/:commentId/removal",
251
+ url: "/api/communities/:communityId/comments/:commentId/remove",
288
252
  schema: {
289
253
  operationId: "removeCommunityComment",
290
254
  tags,
291
- summary: "Remove a comment on a post (staff)",
292
- description: "Staff only (owner, admin, instructor). Hides the comment and resolves its open flags as `removed`. Removing a removed comment returns it unchanged.",
255
+ summary: "Remove a comment on a post",
256
+ description: `The author can remove their own comment; anyone else needs \`communities:moderate\` on the community. Hides the comment; removing someone else's comment resolves its open flags as \`removed\`. Removing a removed comment returns it unchanged.`,
293
257
  params: CommunityCommentParam,
294
258
  response: {
295
259
  200: CommentView,
@@ -297,21 +261,18 @@ async function communityModerationRoutes(app, container) {
297
261
  }
298
262
  },
299
263
  handler: async (req) => {
300
- const scope = await resolveScope(container, req);
301
- return container.discussion.remove(scope.orgId, req.params.commentId, {
302
- id: scope.orgUserId,
303
- role: scope.role
304
- }, req.params.communityId);
264
+ const actor = await resolveActor(container, req);
265
+ return container.discussion.remove(req.params.commentId, { communityId: req.params.communityId }, actor);
305
266
  }
306
267
  });
307
268
  r.route({
308
269
  method: "POST",
309
- url: "/api/communities/:communityId/comments/:commentId/restoration",
270
+ url: "/api/communities/:communityId/comments/:commentId/restore",
310
271
  schema: {
311
272
  operationId: "restoreCommunityComment",
312
273
  tags,
313
- summary: "Restore a removed comment on a post (staff)",
314
- description: "Staff only (owner, admin, instructor). Publishes the comment again. Restoring a comment that is not removed returns it unchanged.",
274
+ summary: "Restore a removed comment on a post",
275
+ description: `Requires one of \`courses:moderate\`, \`communities:moderate\`. Publishes the comment again. Restoring a comment that is not removed returns it unchanged.`,
315
276
  params: CommunityCommentParam,
316
277
  response: {
317
278
  200: CommentView,
@@ -319,21 +280,18 @@ async function communityModerationRoutes(app, container) {
319
280
  }
320
281
  },
321
282
  handler: async (req) => {
322
- const scope = await resolveScope(container, req);
323
- return container.discussion.restore(scope.orgId, req.params.commentId, {
324
- id: scope.orgUserId,
325
- role: scope.role
326
- }, req.params.communityId);
283
+ const actor = await resolveActor(container, req);
284
+ return container.discussion.restore(req.params.commentId, { communityId: req.params.communityId }, actor);
327
285
  }
328
286
  });
329
287
  r.route({
330
288
  method: "POST",
331
- url: "/api/communities/:communityId/comments/:commentId/flags/resolution",
289
+ url: "/api/communities/:communityId/comments/:commentId/flags/resolve",
332
290
  schema: {
333
291
  operationId: "resolveCommunityCommentFlags",
334
292
  tags,
335
- summary: "Dismiss the open flags on a comment (staff)",
336
- description: "Staff only (owner, admin, instructor). Closes every open flag on the comment and leaves it up.",
293
+ summary: "Dismiss the open flags on a comment",
294
+ description: `Requires one of \`courses:moderate\`, \`communities:moderate\`. Closes every open flag on the comment and leaves it up.`,
337
295
  params: CommunityCommentParam,
338
296
  body: ResolveCommentFlags,
339
297
  response: {
@@ -342,22 +300,19 @@ async function communityModerationRoutes(app, container) {
342
300
  }
343
301
  },
344
302
  handler: async (req, reply) => {
345
- const scope = await resolveScope(container, req);
346
- await container.discussion.resolveReports(scope.orgId, req.params.commentId, {
347
- id: scope.orgUserId,
348
- role: scope.role
349
- }, req.params.communityId);
303
+ const actor = await resolveActor(container, req);
304
+ await container.discussion.resolveReports(req.params.commentId, { communityId: req.params.communityId }, actor);
350
305
  return reply.code(204).send();
351
306
  }
352
307
  });
353
308
  r.route({
354
309
  method: "POST",
355
- url: "/api/communities/:communityId/messages/:messageId/flags/resolution",
310
+ url: "/api/communities/:communityId/messages/:messageId/flags/resolve",
356
311
  schema: {
357
312
  operationId: "resolveCommunityMessageFlags",
358
313
  tags,
359
- summary: "Dismiss the open flags on a message (staff)",
360
- description: "Staff only (owner, admin, instructor). Closes every open flag on the message and leaves it up.",
314
+ summary: "Dismiss the open flags on a message",
315
+ description: `Requires \`communities:moderate\`. Closes every open flag on the message and leaves it up.`,
361
316
  params: CommunityMessageParam,
362
317
  body: ResolveMessageFlags,
363
318
  response: {
@@ -366,11 +321,8 @@ async function communityModerationRoutes(app, container) {
366
321
  }
367
322
  },
368
323
  handler: async (req, reply) => {
369
- const scope = await resolveScope(container, req);
370
- await container.chats.resolveMessageFlags(scope.orgId, {
371
- id: scope.orgUserId,
372
- role: scope.role
373
- }, req.params.messageId, req.body.resolution, req.params.communityId);
324
+ const actor = await resolveActor(container, req);
325
+ await container.chats.resolveMessageFlags(req.params.messageId, req.body.resolution, req.params.communityId, actor);
374
326
  return reply.code(204).send();
375
327
  }
376
328
  });
@@ -380,8 +332,8 @@ async function communityModerationRoutes(app, container) {
380
332
  schema: {
381
333
  operationId: "lockCommunityChat",
382
334
  tags,
383
- summary: "Lock a chat (staff)",
384
- description: "Staff only (owner, admin, instructor). Nobody can send or edit messages until it is unlocked; its open message flags are resolved as `locked`. Locking a locked chat returns it unchanged.",
335
+ summary: "Lock a chat",
336
+ description: `Requires \`communities:moderate\`. Nobody can send or edit messages until it is unlocked; its open message flags are resolved as \`locked\`. Locking a locked chat returns it unchanged.`,
385
337
  params: CommunityChatParam,
386
338
  response: {
387
339
  200: Chat,
@@ -389,11 +341,8 @@ async function communityModerationRoutes(app, container) {
389
341
  }
390
342
  },
391
343
  handler: async (req) => {
392
- const scope = await resolveScope(container, req);
393
- return container.chats.lock(scope.orgId, {
394
- id: scope.orgUserId,
395
- role: scope.role
396
- }, req.params.chatId, req.params.communityId);
344
+ const actor = await resolveActor(container, req);
345
+ return container.chats.lock(req.params.chatId, req.params.communityId, actor);
397
346
  }
398
347
  });
399
348
  r.route({
@@ -402,8 +351,8 @@ async function communityModerationRoutes(app, container) {
402
351
  schema: {
403
352
  operationId: "unlockCommunityChat",
404
353
  tags,
405
- summary: "Unlock a chat (staff)",
406
- description: "Staff only (owner, admin, instructor). Lets participants send messages again. Unlocking an unlocked chat returns it unchanged.",
354
+ summary: "Unlock a chat",
355
+ description: `Requires \`communities:moderate\`. Lets participants send messages again. Unlocking an unlocked chat returns it unchanged.`,
407
356
  params: CommunityChatParam,
408
357
  response: {
409
358
  200: Chat,
@@ -411,11 +360,8 @@ async function communityModerationRoutes(app, container) {
411
360
  }
412
361
  },
413
362
  handler: async (req) => {
414
- const scope = await resolveScope(container, req);
415
- return container.chats.unlock(scope.orgId, {
416
- id: scope.orgUserId,
417
- role: scope.role
418
- }, req.params.chatId, req.params.communityId);
363
+ const actor = await resolveActor(container, req);
364
+ return container.chats.unlock(req.params.chatId, req.params.communityId, actor);
419
365
  }
420
366
  });
421
367
  }