@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../../../src/http/routes/member/events.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AddEventAsset,\n AddEventAttendee,\n CreateEvent,\n Event,\n EventAssetParams,\n EventAttendeeParams,\n EventAttendeesPage,\n EventFile,\n EventIdParam,\n MemberEvent,\n MemberEventsPage,\n MemberEventsQuery,\n PageQuery,\n RenameEventAsset,\n ReorderEventAssets,\n UpdateEvent,\n refusals,\n} from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { resolveActor } from '../../scope.js';\n\nexport async function memberEventsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n const member = container.reporting.member;\n\n r.route({\n method: 'GET',\n url: '/api/member/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberEvents',\n tags: ['Events'],\n summary: 'List the events you can see',\n description:\n 'For a member of the organization. The published `public` events, and the published and canceled events you attend; with `events:read`, every published and canceled event it covers. Each as the event read returns it.',\n querystring: MemberEventsQuery,\n response: { 200: MemberEventsPage, ...refusals(400, 403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return member.listEvents(req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberEvent',\n tags: ['Events'],\n summary: 'Get an event, with what you can do with it',\n description:\n 'With `events:read` on the event or the org: the event in any status. As an attendee, directly or through a bundle: the published or canceled event, with the recording after the end when replay is on. Otherwise a published `public` event without its location, files and recording. 404 for anything else.',\n params: EventIdParam,\n response: { 200: MemberEvent, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return member.getEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId/calendar',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEventCalendar',\n tags: ['Events'],\n summary: 'Download an event as a calendar file',\n description:\n 'An iCalendar file with the title, the times and the event page. The location and the meeting link are included when the event read returns them. 404 when the event read does.',\n params: EventIdParam,\n response: {\n 200: z.string().describe('`text/calendar`, one VEVENT'),\n ...refusals(403, 404),\n },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const body = await member.eventCalendar(req.params.eventId, actor);\n return reply\n .header('content-type', 'text/calendar; charset=utf-8')\n .header('content-disposition', `attachment; filename=\"${req.params.eventId}.ics\"`)\n .send(body);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberEventAttendees',\n tags: ['Events'],\n summary: 'List the attendees of an event you run',\n description: `Requires \\`events:read\\`. On the event or the org. The people attending, directly or through a bundle, by grant date. \\`source\\` is \\`bundle\\` for someone who attends only through a bundle.`,\n params: EventIdParam,\n querystring: PageQuery,\n response: { 200: EventAttendeesPage, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return member.eventAttendees(req.params.eventId, req.query, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createMemberEvent',\n tags: ['Events'],\n summary: 'Create an event from the member app',\n description: `Requires \\`events:write\\`. The event starts as a draft. 400 when it ends before it starts.`,\n body: CreateEvent,\n response: { 201: Event, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const event = await content.createEvent(req.body, actor);\n return reply.code(201).send(event);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateMemberEvent',\n tags: ['Events'],\n summary: 'Update an event you run',\n description: `Requires \\`events:write\\`. Changes only the fields sent; \\`status\\` publishes or unpublishes it. \\`thumbnailAssetId\\` must be a ready library image (409 \\`conflict\\` otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise). 409 \\`canceled\\` when the event is canceled.`,\n params: EventIdParam,\n body: UpdateEvent,\n response: { 200: Event, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateEvent(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events/:eventId/cancel',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'cancelMemberEvent',\n tags: ['Events'],\n summary: 'Cancel an event you run',\n description: `Requires \\`events:write\\`. Canceling is final. 409 \\`canceled\\` when it is already canceled.`,\n params: EventIdParam,\n response: { 200: Event, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.cancelEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteMemberEvent',\n tags: ['Events'],\n summary: 'Delete an event you can delete',\n description: `Requires \\`events:delete\\`. Its attendees lose it; its library files survive.`,\n params: EventIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.deleteEvent(req.params.eventId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberEventAssets',\n tags: ['Events'],\n summary: 'List the files of an event you run, in order',\n description: 'Requires `events:read`.',\n params: EventIdParam,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listEventAssets(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addMemberEventAsset',\n tags: ['Events'],\n summary: 'Attach a file to an event you run',\n description: `Requires \\`events:write\\`. Added last. 409 when the file is not a ready library file or is already attached. A file not uploaded for the event needs \\`media:read\\` (403 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: AddEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.addEventAsset(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/events/:eventId/assets/order',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderMemberEventAssets',\n tags: ['Events'],\n summary: 'Reorder the files of an event you run',\n description: `Requires \\`events:write\\`. Send every attached file id exactly once (409 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: ReorderEventAssets,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.reorderEventAssets(req.params.eventId, req.body.assetIds, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'renameMemberEventAsset',\n tags: ['Events'],\n summary: \"Set a file's display name within an event you run\",\n description: `Requires \\`events:write\\`. Null falls back to the filename. Returns the event's files.`,\n params: EventAssetParams,\n body: RenameEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.renameEventAsset(\n req.params.eventId,\n req.params.assetId,\n req.body.displayName,\n actor,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeMemberEventAsset',\n tags: ['Events'],\n summary: 'Remove a file from an event you run',\n description: `Requires \\`events:write\\`. The library file itself survives. Returns the event's files.`,\n params: EventAssetParams,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.removeEventAsset(req.params.eventId, req.params.assetId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events/:eventId/register',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'registerForEvent',\n tags: ['Events'],\n summary: 'Register for an event',\n description:\n 'Any active member of the organization registers themselves for a published `public` event that has not started, with source `self`. 403 when the event is `private`; 404 when it does not exist or is not published; 409 `already_registered`, `started` or `full`.',\n params: EventIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.grant(\n {\n orgUserId: actor.id,\n contentId: req.params.eventId,\n contentType: 'event',\n expiresAt: null,\n },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events/:eventId/unregister',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'cancelEventRegistration',\n tags: ['Events'],\n summary: 'Cancel your registration for an event',\n description:\n 'Any active member of the organization revokes their own direct grant to the event, which frees the place. 404 when the caller has no direct active grant.',\n params: EventIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.revoke(\n { orgUserId: actor.id, contentId: req.params.eventId, contentType: 'event' },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addMemberEventAttendee',\n tags: ['Events'],\n summary: 'Add an attendee to an event you run',\n description:\n 'Requires `access:write`, or `events:write` on the event or the org. Grants the event to the person, with source `manual`. Adding someone who already attends changes nothing. 404 when the event or the person does not exist; 409 `not_published`, `ended` or `full`.',\n params: EventIdParam,\n body: AddEventAttendee,\n response: { 204: z.void(), ...refusals(400, 403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.grant(\n {\n orgUserId: req.body.orgUserId,\n contentId: req.params.eventId,\n contentType: 'event',\n expiresAt: null,\n },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/events/:eventId/attendees/:orgUserId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeMemberEventAttendee',\n tags: ['Events'],\n summary: 'Remove an attendee from an event you run',\n description:\n \"Requires `access:write`, or `events:write` on the event or the org. Revokes the person's direct grant to the event. 404 when they have no direct active grant; someone who attends only through a bundle has none.\",\n params: EventAttendeeParams,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.revoke(\n { orgUserId: req.params.orgUserId, contentId: req.params.eventId, contentType: 'event' },\n actor,\n );\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
@@ -1,29 +1,20 @@
1
1
  import { refusals } from "../../schemas/shared.js";
2
- import { ActivityFileParams, CommunityThumbnailParams, CourseThumbnailParams, DownloadFileParams, DownloadThumbnailParams, MessageAttachmentParams, PersonImageParams, PostCoverParams, UploadFileParams } from "../../schemas/files.js";
2
+ import { resolveActor } from "../../scope.js";
3
+ import { ActivityFileParams, CommunityThumbnailParams, CourseThumbnailParams, DownloadFileParams, DownloadThumbnailParams, EventFileParams, EventThumbnailParams, MessageAttachmentParams, PostAssetParams, PostCoverParams, UploadFileParams } from "../../schemas/files.js";
3
4
  import "../../schemas/index.js";
4
- import { actorOf } from "./actor.js";
5
5
  import { z } from "zod";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
6
  //#region src/http/routes/member/files.ts
8
7
  const tags = ["Files"];
9
8
  const responses = {
10
9
  302: z.void(),
11
10
  ...refusals(403, 404)
12
11
  };
13
- function redirect(reply, url, what, id) {
14
- if (!url) throw new NotFoundError(what, id);
12
+ function redirect(reply, url) {
15
13
  return reply.header("cache-control", "no-store").header("referrer-policy", "no-referrer").redirect(url, 302);
16
14
  }
17
15
  async function memberFileRoutes(app, container) {
18
16
  const r = app.withTypeProvider();
19
17
  const member = container.reporting.member;
20
- async function actorOf$1(orgId, userId) {
21
- const orgUser = await actorOf(container, orgId, userId);
22
- return {
23
- id: orgUser.id,
24
- role: orgUser.role
25
- };
26
- }
27
18
  r.route({
28
19
  method: "GET",
29
20
  url: "/api/member/courses/:courseId/thumbnail",
@@ -32,13 +23,47 @@ async function memberFileRoutes(app, container) {
32
23
  operationId: "getCourseThumbnail",
33
24
  tags,
34
25
  summary: "Redirect to a course's thumbnail",
35
- description: "Any org user may fetch the thumbnail of a published course; staff may fetch any. 404 when the course has no thumbnail.",
26
+ description: "For an unpublished course, which only a holder of `courses:read` on it may fetch: a published course carries a public `thumbnailUrl` instead. 404 when the course has no thumbnail.",
36
27
  params: CourseThumbnailParams,
37
28
  response: responses
38
29
  },
39
30
  handler: async (req, reply) => {
40
- const actor = await actorOf$1(req.orgId, req.userId);
41
- return redirect(reply, await member.thumbnailFile(req.orgId, actor, "course", req.params.courseId), "Course thumbnail", req.params.courseId);
31
+ const actor = await resolveActor(container, req);
32
+ return redirect(reply, await member.thumbnailFile("course", req.params.courseId, actor));
33
+ }
34
+ });
35
+ r.route({
36
+ method: "GET",
37
+ url: "/api/member/events/:eventId/thumbnail",
38
+ preHandler: app.requireOrgSession,
39
+ schema: {
40
+ operationId: "getEventThumbnail",
41
+ tags,
42
+ summary: "Redirect to an event's cover",
43
+ description: "For a draft event, which only a holder of `events:read` on it may fetch: a published or canceled event carries a public `thumbnailUrl` instead. 404 when the event read returns 404 or the event has no cover.",
44
+ params: EventThumbnailParams,
45
+ response: responses
46
+ },
47
+ handler: async (req, reply) => {
48
+ const actor = await resolveActor(container, req);
49
+ return redirect(reply, await member.thumbnailFile("event", req.params.eventId, actor));
50
+ }
51
+ });
52
+ r.route({
53
+ method: "GET",
54
+ url: "/api/member/events/:eventId/assets/:assetId",
55
+ preHandler: app.requireOrgSession,
56
+ schema: {
57
+ operationId: "getEventFile",
58
+ tags,
59
+ summary: "Redirect to a file attached to an event",
60
+ description: "For a holder of `events:read` on the event or the org, or an attendee of the published or canceled event. Saved under its display name. 404 otherwise, or when the file is not attached to the event.",
61
+ params: EventFileParams,
62
+ response: responses
63
+ },
64
+ handler: async (req, reply) => {
65
+ const actor = await resolveActor(container, req);
66
+ return redirect(reply, await member.eventFile(req.params.eventId, req.params.assetId, actor));
42
67
  }
43
68
  });
44
69
  r.route({
@@ -49,13 +74,13 @@ async function memberFileRoutes(app, container) {
49
74
  operationId: "getDownloadThumbnail",
50
75
  tags,
51
76
  summary: "Redirect to a download's thumbnail",
52
- description: "Any org user may fetch the thumbnail of a published download; staff may fetch any. 404 when the download has no thumbnail.",
77
+ description: "For an unpublished download, which only a holder of `downloads:read` on it may fetch: a published download carries a public `thumbnailUrl` instead. 404 when the download has no thumbnail.",
53
78
  params: DownloadThumbnailParams,
54
79
  response: responses
55
80
  },
56
81
  handler: async (req, reply) => {
57
- const actor = await actorOf$1(req.orgId, req.userId);
58
- return redirect(reply, await member.thumbnailFile(req.orgId, actor, "download", req.params.downloadId), "Download thumbnail", req.params.downloadId);
82
+ const actor = await resolveActor(container, req);
83
+ return redirect(reply, await member.thumbnailFile("download", req.params.downloadId, actor));
59
84
  }
60
85
  });
61
86
  r.route({
@@ -66,13 +91,13 @@ async function memberFileRoutes(app, container) {
66
91
  operationId: "getCommunityThumbnail",
67
92
  tags,
68
93
  summary: "Redirect to a community's thumbnail",
69
- description: "Any org user may fetch the thumbnail of a published community; staff may fetch any. 404 when the community has no thumbnail.",
94
+ description: "For an unpublished community, which only a holder of `communities:read` on it may fetch: a published community carries a public `thumbnailUrl` instead. 404 when the community has no thumbnail.",
70
95
  params: CommunityThumbnailParams,
71
96
  response: responses
72
97
  },
73
98
  handler: async (req, reply) => {
74
- const actor = await actorOf$1(req.orgId, req.userId);
75
- return redirect(reply, await member.thumbnailFile(req.orgId, actor, "community", req.params.communityId), "Community thumbnail", req.params.communityId);
99
+ const actor = await resolveActor(container, req);
100
+ return redirect(reply, await member.thumbnailFile("community", req.params.communityId, actor));
76
101
  }
77
102
  });
78
103
  r.route({
@@ -83,13 +108,13 @@ async function memberFileRoutes(app, container) {
83
108
  operationId: "getActivityFile",
84
109
  tags,
85
110
  summary: "Redirect to a file used in a lesson",
86
- description: "Served when the student can open the course, the course and the lesson are published, and the lesson lists the file. Staff may fetch any file a lesson lists. Use the URL as an img, video or audio src.",
111
+ description: "Served when the person can open the course, the course and the lesson are published, and the lesson lists the file. A holder of `courses:read` on the course may fetch any file a lesson lists. Use the URL as an img, video or audio src.",
87
112
  params: ActivityFileParams,
88
113
  response: responses
89
114
  },
90
115
  handler: async (req, reply) => {
91
- const actor = await actorOf$1(req.orgId, req.userId);
92
- return redirect(reply, await member.activityFile(req.orgId, actor, req.params.activityId, req.params.assetId), "Lesson file", req.params.assetId);
116
+ const actor = await resolveActor(container, req);
117
+ return redirect(reply, await member.activityFile(req.params.activityId, req.params.assetId, actor));
93
118
  }
94
119
  });
95
120
  r.route({
@@ -100,13 +125,13 @@ async function memberFileRoutes(app, container) {
100
125
  operationId: "getDownloadFile",
101
126
  tags,
102
127
  summary: "Redirect to a file in a download, saved under its display name",
103
- description: "Served when the student can open the download, it is published, and it lists the file. Staff may fetch any file a download lists. The storage link lives DELIVERY_URL_EXPIRY seconds.",
128
+ description: "Served when the person can open the download, it is published, and it lists the file. A holder of `downloads:read` on the download may fetch it unpublished. The storage link lives DELIVERY_URL_EXPIRY seconds.",
104
129
  params: DownloadFileParams,
105
130
  response: responses
106
131
  },
107
132
  handler: async (req, reply) => {
108
- const actor = await actorOf$1(req.orgId, req.userId);
109
- return redirect(reply, await member.downloadFile(req.orgId, actor, req.params.downloadId, req.params.assetId), "Download file", req.params.assetId);
133
+ const actor = await resolveActor(container, req);
134
+ return redirect(reply, await member.downloadFile(req.params.downloadId, req.params.assetId, actor));
110
135
  }
111
136
  });
112
137
  r.route({
@@ -117,47 +142,47 @@ async function memberFileRoutes(app, container) {
117
142
  operationId: "getPostCover",
118
143
  tags,
119
144
  summary: "Redirect to a post's cover image",
120
- description: "Served to whoever can read the post. 404 when the post has no cover.",
145
+ description: "For a post that is not published, served to whoever can read it: a published post carries a public `coverUrl` instead. 404 when the post has no cover.",
121
146
  params: PostCoverParams,
122
147
  response: responses
123
148
  },
124
149
  handler: async (req, reply) => {
125
- const actor = await actorOf$1(req.orgId, req.userId);
126
- return redirect(reply, await container.posts.coverFile(req.orgId, actor, req.params.postId), "Post cover", req.params.postId);
150
+ const actor = await resolveActor(container, req);
151
+ return redirect(reply, await container.posts.coverFile(req.params.postId, actor));
127
152
  }
128
153
  });
129
154
  r.route({
130
155
  method: "GET",
131
- url: "/api/member/chats/:chatId/messages/:messageId/attachments/:assetId",
156
+ url: "/api/member/posts/:postId/assets/:assetId",
132
157
  preHandler: app.requireOrgSession,
133
158
  schema: {
134
- operationId: "getMessageAttachment",
159
+ operationId: "getPostAsset",
135
160
  tags,
136
- summary: "Redirect to an image attached to a chat message",
137
- description: "Served to the chat's participants while the message stands (staff also after it is deleted), and to moderators while the message is flagged.",
138
- params: MessageAttachmentParams,
161
+ summary: "Redirect to a file in a post's body",
162
+ description: "Served to whoever can read the post while its body shows the file. Images, video, audio and PDFs open in place; any other file downloads under its name.",
163
+ params: PostAssetParams,
139
164
  response: responses
140
165
  },
141
166
  handler: async (req, reply) => {
142
- const actor = await actorOf$1(req.orgId, req.userId);
143
- return redirect(reply, await container.chats.attachmentFile(req.orgId, actor, req.params.chatId, req.params.messageId, req.params.assetId), "Message attachment", req.params.assetId);
167
+ const actor = await resolveActor(container, req);
168
+ return redirect(reply, await container.posts.assetFile(req.params.postId, req.params.assetId, actor));
144
169
  }
145
170
  });
146
171
  r.route({
147
172
  method: "GET",
148
- url: "/api/member/people/:orgUserId/image",
173
+ url: "/api/member/chats/:chatId/messages/:messageId/attachments/:assetId",
149
174
  preHandler: app.requireOrgSession,
150
175
  schema: {
151
- operationId: "getPersonImage",
176
+ operationId: "getMessageAttachment",
152
177
  tags,
153
- summary: "Redirect to an org user's profile photo",
154
- description: "Served to org users of the same organization when the photo belongs to it. 404 when the person has no photo here.",
155
- params: PersonImageParams,
178
+ summary: "Redirect to an image attached to a chat message",
179
+ description: "Served to the chat's participants while the message stands, to holders of `communities:moderate` on the community also after it is deleted, and to them while the message is flagged.",
180
+ params: MessageAttachmentParams,
156
181
  response: responses
157
182
  },
158
183
  handler: async (req, reply) => {
159
- await actorOf$1(req.orgId, req.userId);
160
- return redirect(reply, await member.personImage(req.orgId, req.params.orgUserId), "Profile photo", req.params.orgUserId);
184
+ const actor = await resolveActor(container, req);
185
+ return redirect(reply, await container.chats.attachmentFile(req.params.chatId, req.params.messageId, req.params.assetId, actor));
161
186
  }
162
187
  });
163
188
  r.route({
@@ -173,8 +198,8 @@ async function memberFileRoutes(app, container) {
173
198
  response: responses
174
199
  },
175
200
  handler: async (req, reply) => {
176
- const actor = await actorOf$1(req.orgId, req.userId);
177
- return redirect(reply, await container.assets.ownUploadFile(req.orgId, req.params.id, actor.id), "Upload", req.params.id);
201
+ const actor = await resolveActor(container, req);
202
+ return redirect(reply, await container.assets.ownUploadFile(req.params.id, actor));
178
203
  }
179
204
  });
180
205
  }
@@ -1 +1 @@
1
- {"version":3,"file":"files.js","names":["actorOf","memberOf"],"sources":["../../../../src/http/routes/member/files.ts"],"sourcesContent":["// Member file proxies. Each route checks access with the context that owns the\n// file, then redirects to a short-lived signed storage URL. Every refusal is a\n// 404, so a response never confirms that a file exists. A 403 only means the\n// caller is not a member of the organization.\nimport type { FastifyInstance, FastifyReply } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport {\n ActivityFileParams,\n CommunityThumbnailParams,\n CourseThumbnailParams,\n DownloadFileParams,\n DownloadThumbnailParams,\n MessageAttachmentParams,\n PersonImageParams,\n PostCoverParams,\n UploadFileParams,\n refusals,\n} from '../../schemas/index.js';\nimport { actorOf as memberOf } from './actor.js';\nimport type { Container } from '../../../app/container.js';\n\nconst tags = ['Files'];\nconst responses = { 302: z.void(), ...refusals(403, 404) };\n\nfunction redirect(reply: FastifyReply, url: string | null, what: string, id: string) {\n if (!url) {\n throw new NotFoundError(what, id);\n }\n return reply\n .header('cache-control', 'no-store')\n .header('referrer-policy', 'no-referrer')\n .redirect(url, 302);\n}\n\nexport async function memberFileRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const member = container.reporting.member;\n\n async function actorOf(orgId: string, userId: string) {\n const orgUser = await memberOf(container, orgId, userId);\n return { id: orgUser.id, role: orgUser.role };\n }\n\n r.route({\n method: 'GET',\n url: '/api/member/courses/:courseId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourseThumbnail',\n tags,\n summary: \"Redirect to a course's thumbnail\",\n description:\n 'Any org user may fetch the thumbnail of a published course; staff may fetch any. 404 when the course has no thumbnail.',\n params: CourseThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await member.thumbnailFile(req.orgId, actor, 'course', req.params.courseId);\n return redirect(reply, url, 'Course thumbnail', req.params.courseId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/downloads/:downloadId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownloadThumbnail',\n tags,\n summary: \"Redirect to a download's thumbnail\",\n description:\n 'Any org user may fetch the thumbnail of a published download; staff may fetch any. 404 when the download has no thumbnail.',\n params: DownloadThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await member.thumbnailFile(req.orgId, actor, 'download', req.params.downloadId);\n return redirect(reply, url, 'Download thumbnail', req.params.downloadId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCommunityThumbnail',\n tags,\n summary: \"Redirect to a community's thumbnail\",\n description:\n 'Any org user may fetch the thumbnail of a published community; staff may fetch any. 404 when the community has no thumbnail.',\n params: CommunityThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await member.thumbnailFile(req.orgId, actor, 'community', req.params.communityId);\n return redirect(reply, url, 'Community thumbnail', req.params.communityId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/activities/:activityId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getActivityFile',\n tags,\n summary: 'Redirect to a file used in a lesson',\n description:\n 'Served when the student can open the course, the course and the lesson are published, and the lesson lists the file. Staff may fetch any file a lesson lists. Use the URL as an img, video or audio src.',\n params: ActivityFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await member.activityFile(\n req.orgId,\n actor,\n req.params.activityId,\n req.params.assetId,\n );\n return redirect(reply, url, 'Lesson file', req.params.assetId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownloadFile',\n tags,\n summary: 'Redirect to a file in a download, saved under its display name',\n description:\n 'Served when the student can open the download, it is published, and it lists the file. Staff may fetch any file a download lists. The storage link lives DELIVERY_URL_EXPIRY seconds.',\n params: DownloadFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await member.downloadFile(\n req.orgId,\n actor,\n req.params.downloadId,\n req.params.assetId,\n );\n return redirect(reply, url, 'Download file', req.params.assetId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId/cover',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPostCover',\n tags,\n summary: \"Redirect to a post's cover image\",\n description: 'Served to whoever can read the post. 404 when the post has no cover.',\n params: PostCoverParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await container.posts.coverFile(req.orgId, actor, req.params.postId);\n return redirect(reply, url, 'Post cover', req.params.postId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/chats/:chatId/messages/:messageId/attachments/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMessageAttachment',\n tags,\n summary: 'Redirect to an image attached to a chat message',\n description:\n \"Served to the chat's participants while the message stands (staff also after it is deleted), and to moderators while the message is flagged.\",\n params: MessageAttachmentParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await container.chats.attachmentFile(\n req.orgId,\n actor,\n req.params.chatId,\n req.params.messageId,\n req.params.assetId,\n );\n return redirect(reply, url, 'Message attachment', req.params.assetId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/people/:orgUserId/image',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPersonImage',\n tags,\n summary: \"Redirect to an org user's profile photo\",\n description:\n 'Served to org users of the same organization when the photo belongs to it. 404 when the person has no photo here.',\n params: PersonImageParams,\n response: responses,\n },\n handler: async (req, reply) => {\n await actorOf(req.orgId, req.userId);\n const url = await member.personImage(req.orgId, req.params.orgUserId);\n return redirect(reply, url, 'Profile photo', req.params.orgUserId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/uploads/:id/file',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberUploadFile',\n tags,\n summary: 'Redirect to an image you uploaded',\n description: 'Served only to the person who uploaded it, once it is finalized.',\n params: UploadFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await actorOf(req.orgId, req.userId);\n const url = await container.assets.ownUploadFile(req.orgId, req.params.id, actor.id);\n return redirect(reply, url, 'Upload', req.params.id);\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"files.js","names":[],"sources":["../../../../src/http/routes/member/files.ts"],"sourcesContent":["// Member file proxies. Each route asks the context that owns the file, then\n// redirects to a short-lived signed storage URL. Every refusal is a 404, so a\n// response never confirms that a file exists. A 403 only means the caller is\n// not an active member of the organization.\nimport type { FastifyInstance, FastifyReply } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActivityFileParams,\n CommunityThumbnailParams,\n CourseThumbnailParams,\n DownloadFileParams,\n DownloadThumbnailParams,\n EventFileParams,\n EventThumbnailParams,\n MessageAttachmentParams,\n PostCoverParams,\n PostAssetParams,\n UploadFileParams,\n refusals,\n} from '../../schemas/index.js';\nimport { resolveActor } from '../../scope.js';\nimport type { Container } from '../../../app/container.js';\n\nconst tags = ['Files'];\nconst responses = { 302: z.void(), ...refusals(403, 404) };\n\nfunction redirect(reply: FastifyReply, url: string) {\n return reply\n .header('cache-control', 'no-store')\n .header('referrer-policy', 'no-referrer')\n .redirect(url, 302);\n}\n\nexport async function memberFileRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const member = container.reporting.member;\n\n r.route({\n method: 'GET',\n url: '/api/member/courses/:courseId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourseThumbnail',\n tags,\n summary: \"Redirect to a course's thumbnail\",\n description:\n 'For an unpublished course, which only a holder of `courses:read` on it may fetch: a published course carries a public `thumbnailUrl` instead. 404 when the course has no thumbnail.',\n params: CourseThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.thumbnailFile('course', req.params.courseId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEventThumbnail',\n tags,\n summary: \"Redirect to an event's cover\",\n description:\n 'For a draft event, which only a holder of `events:read` on it may fetch: a published or canceled event carries a public `thumbnailUrl` instead. 404 when the event read returns 404 or the event has no cover.',\n params: EventThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.thumbnailFile('event', req.params.eventId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEventFile',\n tags,\n summary: 'Redirect to a file attached to an event',\n description:\n 'For a holder of `events:read` on the event or the org, or an attendee of the published or canceled event. Saved under its display name. 404 otherwise, or when the file is not attached to the event.',\n params: EventFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.eventFile(req.params.eventId, req.params.assetId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/downloads/:downloadId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownloadThumbnail',\n tags,\n summary: \"Redirect to a download's thumbnail\",\n description:\n 'For an unpublished download, which only a holder of `downloads:read` on it may fetch: a published download carries a public `thumbnailUrl` instead. 404 when the download has no thumbnail.',\n params: DownloadThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.thumbnailFile('download', req.params.downloadId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/thumbnail',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCommunityThumbnail',\n tags,\n summary: \"Redirect to a community's thumbnail\",\n description:\n 'For an unpublished community, which only a holder of `communities:read` on it may fetch: a published community carries a public `thumbnailUrl` instead. 404 when the community has no thumbnail.',\n params: CommunityThumbnailParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.thumbnailFile('community', req.params.communityId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/activities/:activityId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getActivityFile',\n tags,\n summary: 'Redirect to a file used in a lesson',\n description:\n 'Served when the person can open the course, the course and the lesson are published, and the lesson lists the file. A holder of `courses:read` on the course may fetch any file a lesson lists. Use the URL as an img, video or audio src.',\n params: ActivityFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.activityFile(req.params.activityId, req.params.assetId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownloadFile',\n tags,\n summary: 'Redirect to a file in a download, saved under its display name',\n description:\n 'Served when the person can open the download, it is published, and it lists the file. A holder of `downloads:read` on the download may fetch it unpublished. The storage link lives DELIVERY_URL_EXPIRY seconds.',\n params: DownloadFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await member.downloadFile(req.params.downloadId, req.params.assetId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId/cover',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPostCover',\n tags,\n summary: \"Redirect to a post's cover image\",\n description:\n 'For a post that is not published, served to whoever can read it: a published post carries a public `coverUrl` instead. 404 when the post has no cover.',\n params: PostCoverParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await container.posts.coverFile(req.params.postId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPostAsset',\n tags,\n summary: \"Redirect to a file in a post's body\",\n description:\n 'Served to whoever can read the post while its body shows the file. Images, video, audio and PDFs open in place; any other file downloads under its name.',\n params: PostAssetParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await container.posts.assetFile(req.params.postId, req.params.assetId, actor);\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/chats/:chatId/messages/:messageId/attachments/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMessageAttachment',\n tags,\n summary: 'Redirect to an image attached to a chat message',\n description:\n \"Served to the chat's participants while the message stands, to holders of `communities:moderate` on the community also after it is deleted, and to them while the message is flagged.\",\n params: MessageAttachmentParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await container.chats.attachmentFile(\n req.params.chatId,\n req.params.messageId,\n req.params.assetId,\n actor,\n );\n return redirect(reply, url);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/uploads/:id/file',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberUploadFile',\n tags,\n summary: 'Redirect to an image you uploaded',\n description: 'Served only to the person who uploaded it, once it is finalized.',\n params: UploadFileParams,\n response: responses,\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await container.assets.ownUploadFile(req.params.id, actor);\n return redirect(reply, url);\n },\n });\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@ import { memberCommentsRoutes } from "./comments.js";
4
4
  import { memberCommunityRoutes } from "./community.js";
5
5
  import { memberCoursesRoutes } from "./courses.js";
6
6
  import { memberDownloadsRoutes } from "./downloads.js";
7
+ import { memberEventsRoutes } from "./events.js";
7
8
  import { memberFileRoutes } from "./files.js";
8
9
  import { memberNotificationRoutes } from "./notifications.js";
9
10
  import { memberNotificationSettingsRoutes } from "./notification-settings.js";
@@ -24,6 +25,7 @@ async function memberRoutes(app, container) {
24
25
  await memberCommentsRoutes(app, container);
25
26
  await memberAssetsRoutes(app, container);
26
27
  await memberDownloadsRoutes(app, container);
28
+ await memberEventsRoutes(app, container);
27
29
  await memberFileRoutes(app, container);
28
30
  await memberNotificationRoutes(app, container);
29
31
  await memberNotificationSettingsRoutes(app, container);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/http/routes/member/index.ts"],"sourcesContent":["// Student HTTP API under `/api/member`. Session-guarded like every\n// back-office route, but scoped by the session's `org_users` row —\n// the reads gate on the person's enrollments, not on staff role. One file per\n// resource; everything under `/api/member` lives in this directory and nowhere\n// else. The course, download, and progress reads call reporting/member.\nimport type { FastifyInstance } from 'fastify';\nimport type { Container } from '../../../app/container.js';\nimport { memberAssetsRoutes } from './assets.js';\nimport { memberChatRoutes } from './chats.js';\nimport { memberCommentsRoutes } from './comments.js';\nimport { memberCommunityRoutes } from './community.js';\nimport { memberCoursesRoutes } from './courses.js';\nimport { memberDownloadsRoutes } from './downloads.js';\nimport { memberFileRoutes } from './files.js';\nimport { memberNotificationRoutes } from './notifications.js';\nimport { memberNotificationSettingsRoutes } from './notification-settings.js';\nimport { memberDeviceRoutes } from './devices.js';\nimport { memberOrgRoutes } from './org.js';\nimport { memberPostRoutes } from './posts.js';\nimport { memberProfileRoutes } from './profile.js';\nimport { memberProgressRoutes } from './progress.js';\n\nexport async function memberRoutes(app: FastifyInstance, container: Container): Promise<void> {\n await memberOrgRoutes(app, container);\n await memberProfileRoutes(app, container);\n await memberCommunityRoutes(app, container);\n await memberPostRoutes(app, container);\n await memberChatRoutes(app, container);\n await memberCoursesRoutes(app, container);\n await memberProgressRoutes(app, container);\n await memberCommentsRoutes(app, container);\n await memberAssetsRoutes(app, container);\n await memberDownloadsRoutes(app, container);\n await memberFileRoutes(app, container);\n await memberNotificationRoutes(app, container);\n await memberNotificationSettingsRoutes(app, container);\n await memberDeviceRoutes(app, container);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsBA,eAAsB,aAAa,KAAsB,WAAqC;CAC5F,MAAM,gBAAgB,KAAK,SAAS;CACpC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,sBAAsB,KAAK,SAAS;CAC1C,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,qBAAqB,KAAK,SAAS;CACzC,MAAM,qBAAqB,KAAK,SAAS;CACzC,MAAM,mBAAmB,KAAK,SAAS;CACvC,MAAM,sBAAsB,KAAK,SAAS;CAC1C,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,yBAAyB,KAAK,SAAS;CAC7C,MAAM,iCAAiC,KAAK,SAAS;CACrD,MAAM,mBAAmB,KAAK,SAAS;AACzC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/http/routes/member/index.ts"],"sourcesContent":["// Member HTTP API under `/api/member`. Every route resolves the caller's actor,\n// so only an active org user reaches it; the reads gate on the person's grants\n// and permissions. One file per resource; everything under `/api/member` lives\n// in this directory and nowhere else. The course, download, and progress reads\n// call reporting/member.\nimport type { FastifyInstance } from 'fastify';\nimport type { Container } from '../../../app/container.js';\nimport { memberAssetsRoutes } from './assets.js';\nimport { memberChatRoutes } from './chats.js';\nimport { memberCommentsRoutes } from './comments.js';\nimport { memberCommunityRoutes } from './community.js';\nimport { memberCoursesRoutes } from './courses.js';\nimport { memberDownloadsRoutes } from './downloads.js';\nimport { memberEventsRoutes } from './events.js';\nimport { memberFileRoutes } from './files.js';\nimport { memberNotificationRoutes } from './notifications.js';\nimport { memberNotificationSettingsRoutes } from './notification-settings.js';\nimport { memberDeviceRoutes } from './devices.js';\nimport { memberOrgRoutes } from './org.js';\nimport { memberPostRoutes } from './posts.js';\nimport { memberProfileRoutes } from './profile.js';\nimport { memberProgressRoutes } from './progress.js';\n\nexport async function memberRoutes(app: FastifyInstance, container: Container): Promise<void> {\n await memberOrgRoutes(app, container);\n await memberProfileRoutes(app, container);\n await memberCommunityRoutes(app, container);\n await memberPostRoutes(app, container);\n await memberChatRoutes(app, container);\n await memberCoursesRoutes(app, container);\n await memberProgressRoutes(app, container);\n await memberCommentsRoutes(app, container);\n await memberAssetsRoutes(app, container);\n await memberDownloadsRoutes(app, container);\n await memberEventsRoutes(app, container);\n await memberFileRoutes(app, container);\n await memberNotificationRoutes(app, container);\n await memberNotificationSettingsRoutes(app, container);\n await memberDeviceRoutes(app, container);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAuBA,eAAsB,aAAa,KAAsB,WAAqC;CAC5F,MAAM,gBAAgB,KAAK,SAAS;CACpC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,sBAAsB,KAAK,SAAS;CAC1C,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,qBAAqB,KAAK,SAAS;CACzC,MAAM,qBAAqB,KAAK,SAAS;CACzC,MAAM,mBAAmB,KAAK,SAAS;CACvC,MAAM,sBAAsB,KAAK,SAAS;CAC1C,MAAM,mBAAmB,KAAK,SAAS;CACvC,MAAM,iBAAiB,KAAK,SAAS;CACrC,MAAM,yBAAyB,KAAK,SAAS;CAC7C,MAAM,iCAAiC,KAAK,SAAS;CACrD,MAAM,mBAAmB,KAAK,SAAS;AACzC"}
@@ -1,14 +1,20 @@
1
1
  import { refusals } from "../../schemas/shared.js";
2
+ import { resolveCaller } from "../../scope.js";
2
3
  import { CommunityParam } from "../../schemas/chats.js";
3
4
  import { CommunityNotificationSettings, OrgNotificationSettings, PatchNotificationSettings } from "../../schemas/notifications.js";
4
5
  import "../../schemas/index.js";
5
- import { actorOf } from "./actor.js";
6
6
  import { z } from "zod";
7
- import { NotFoundError } from "@headless-lms/core/shared/errors";
8
7
  //#region src/http/routes/member/notification-settings.ts
9
8
  async function memberNotificationSettingsRoutes(app, container) {
10
9
  const r = app.withTypeProvider();
11
10
  const tags = ["Notifications"];
11
+ async function memberOf(req) {
12
+ const caller = await resolveCaller(container, req);
13
+ return {
14
+ ...caller.actor,
15
+ userId: caller.userId
16
+ };
17
+ }
12
18
  r.route({
13
19
  method: "GET",
14
20
  url: "/api/member/communities/:communityId/settings/notifications",
@@ -17,7 +23,7 @@ async function memberNotificationSettingsRoutes(app, container) {
17
23
  operationId: "getMemberCommunityNotificationSettings",
18
24
  tags,
19
25
  summary: "Read how you are told about each notification category in a community",
20
- description: "For anyone who can open the community, staff included. One entry per category of the list in use, each with the channels it offers on this deployment, whether each is on for you and why (`locked`, `space`, `community`, `spaceDefault` or `default`), and `default`, what the channel would be without your own setting at that level. A per-space category also lists every space you can open, with its name. `digest` is the digest schedule of the community, null when it sends none. 404 when you cannot open the community.",
26
+ description: "For anyone who can open the community. One entry per category of the list in use that can apply to you, each with the channels it offers on this deployment, whether each is on for you and why (`locked`, `space`, `community`, `spaceDefault` or `default`), and `default`, what the channel would be without your own setting at that level. A per-space category also lists every space you can open, with its name. `digest` is the digest schedule of the community, null when it sends none. 404 when you cannot open the community.",
21
27
  params: CommunityParam,
22
28
  response: {
23
29
  200: CommunityNotificationSettings,
@@ -25,10 +31,7 @@ async function memberNotificationSettingsRoutes(app, container) {
25
31
  }
26
32
  },
27
33
  handler: async (req) => {
28
- const actor = await actorOf(container, req.orgId, req.userId);
29
- const view = await container.reporting.member.communityNotificationSettings(req.orgId, actor, req.userId, req.params.communityId);
30
- if (!view) throw new NotFoundError("Community", req.params.communityId);
31
- return view;
34
+ return container.reporting.member.communityNotificationSettings(req.params.communityId, await memberOf(req));
32
35
  }
33
36
  });
34
37
  r.route({
@@ -39,7 +42,7 @@ async function memberNotificationSettingsRoutes(app, container) {
39
42
  operationId: "setMemberCommunityNotificationSettings",
40
43
  tags,
41
44
  summary: "Switch notification categories on or off per channel in a community",
42
- description: "For anyone who can open the community, staff included. Each setting switches one category on one channel, for all spaces or, for a per-space category, for one space; `enabled: null` removes your choice so the default applies again. Settings left out keep their values. 404 when you cannot open the community; 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given for a category that is not per space.",
45
+ description: "For anyone who can open the community. Each setting switches one category on one channel, for all spaces or, for a per-space category, for one space; `enabled: null` removes your choice so the default applies again. Settings left out keep their values. 404 when you cannot open the community; 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given for a category that is not per space.",
43
46
  params: CommunityParam,
44
47
  body: PatchNotificationSettings,
45
48
  response: {
@@ -48,12 +51,7 @@ async function memberNotificationSettingsRoutes(app, container) {
48
51
  }
49
52
  },
50
53
  handler: async (req, reply) => {
51
- const actor = await actorOf(container, req.orgId, req.userId);
52
- await container.notificationSettings.set(req.orgId, {
53
- id: actor.id,
54
- role: actor.role,
55
- userId: req.userId
56
- }, { communityId: req.params.communityId }, req.body.settings);
54
+ await container.notificationSettings.set({ communityId: req.params.communityId }, req.body.settings, await memberOf(req));
57
55
  return reply.code(204).send();
58
56
  }
59
57
  });
@@ -65,15 +63,14 @@ async function memberNotificationSettingsRoutes(app, container) {
65
63
  operationId: "getMemberNotificationSettings",
66
64
  tags,
67
65
  summary: "Read how you are told about notifications outside any community",
68
- description: "For anyone signed in to the organization. The organization's categories, such as access and course content, each with the channels it offers, whether each is on for you, and `default`, what it would be without your own setting.",
66
+ description: "For anyone signed in to the organization. The organization's categories that can apply to you, such as access and course content, each with the channels it offers, whether each is on for you, and `default`, what it would be without your own setting.",
69
67
  response: {
70
68
  200: OrgNotificationSettings,
71
69
  ...refusals(403)
72
70
  }
73
71
  },
74
72
  handler: async (req) => {
75
- await actorOf(container, req.orgId, req.userId);
76
- return container.reporting.member.orgNotificationSettings(req.orgId, req.userId);
73
+ return container.reporting.member.orgNotificationSettings(await memberOf(req));
77
74
  }
78
75
  });
79
76
  r.route({
@@ -92,12 +89,7 @@ async function memberNotificationSettingsRoutes(app, container) {
92
89
  }
93
90
  },
94
91
  handler: async (req, reply) => {
95
- const actor = await actorOf(container, req.orgId, req.userId);
96
- await container.notificationSettings.set(req.orgId, {
97
- id: actor.id,
98
- role: actor.role,
99
- userId: req.userId
100
- }, "org", req.body.settings);
92
+ await container.notificationSettings.set("org", req.body.settings, await memberOf(req));
101
93
  return reply.code(204).send();
102
94
  }
103
95
  });
@@ -1 +1 @@
1
- {"version":3,"file":"notification-settings.js","names":[],"sources":["../../../../src/http/routes/member/notification-settings.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 { actorOf } from './actor.js';\nimport {\n CommunityParam,\n CommunityNotificationSettings,\n OrgNotificationSettings,\n PatchNotificationSettings,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberNotificationSettingsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Notifications'];\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberCommunityNotificationSettings',\n tags,\n summary: 'Read how you are told about each notification category in a community',\n description:\n 'For anyone who can open the community, staff included. One entry per category of the list in use, each with the channels it offers on this deployment, whether each is on for you and why (`locked`, `space`, `community`, `spaceDefault` or `default`), and `default`, what the channel would be without your own setting at that level. A per-space category also lists every space you can open, with its name. `digest` is the digest schedule of the community, null when it sends none. 404 when you cannot open the community.',\n params: CommunityParam,\n response: { 200: CommunityNotificationSettings, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const view = await container.reporting.member.communityNotificationSettings(\n req.orgId,\n actor,\n req.userId,\n req.params.communityId,\n );\n if (!view) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return view;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/communities/:communityId/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setMemberCommunityNotificationSettings',\n tags,\n summary: 'Switch notification categories on or off per channel in a community',\n description:\n 'For anyone who can open the community, staff included. Each setting switches one category on one channel, for all spaces or, for a per-space category, for one space; `enabled: null` removes your choice so the default applies again. Settings left out keep their values. 404 when you cannot open the community; 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given for a category that is not per space.',\n params: CommunityParam,\n body: PatchNotificationSettings,\n response: { 204: z.void(), ...refusals(400, 403, 404, 422) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await container.notificationSettings.set(\n req.orgId,\n { id: actor.id, role: actor.role, userId: req.userId },\n { communityId: req.params.communityId },\n req.body.settings,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotificationSettings',\n tags,\n summary: 'Read how you are told about notifications outside any community',\n description:\n \"For anyone signed in to the organization. The organization's categories, such as access and course content, each with the channels it offers, whether each is on for you, and `default`, what it would be without your own setting.\",\n response: { 200: OrgNotificationSettings, ...refusals(403) },\n },\n handler: async (req) => {\n await actorOf(container, req.orgId, req.userId);\n return container.reporting.member.orgNotificationSettings(req.orgId, req.userId);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setMemberNotificationSettings',\n tags,\n summary: 'Switch notification categories outside any community on or off per channel',\n description:\n \"For anyone signed in to the organization. Works like the community settings, against the organization's categories; `enabled: null` removes your choice. 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given.\",\n body: PatchNotificationSettings,\n response: { 204: z.void(), ...refusals(400, 403, 422) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await container.notificationSettings.set(\n req.orgId,\n { id: actor.id, role: actor.role, userId: req.userId },\n 'org',\n req.body.settings,\n );\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;;AAcA,eAAsB,iCACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,eAAe;CAE7B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAA+B,GAAG,SAAS,KAAK,GAAG;GAAE;EACxE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,OAAO,MAAM,UAAU,UAAU,OAAO,8BAC5C,IAAI,OACJ,OACA,IAAI,QACJ,IAAI,OAAO,WACb;GACA,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,aAAa,IAAI,OAAO,WAAW;GAE7D,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,UAAU,qBAAqB,IACnC,IAAI,OACJ;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM;IAAM,QAAQ,IAAI;GAAO,GACrD,EAAE,aAAa,IAAI,OAAO,YAAY,GACtC,IAAI,KAAK,QACX;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,UAAU;IAAE,KAAK;IAAyB,GAAG,SAAS,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9C,OAAO,UAAU,UAAU,OAAO,wBAAwB,IAAI,OAAO,IAAI,MAAM;EACjF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,UAAU,qBAAqB,IACnC,IAAI,OACJ;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM;IAAM,QAAQ,IAAI;GAAO,GACrD,OACA,IAAI,KAAK,QACX;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"notification-settings.js","names":[],"sources":["../../../../src/http/routes/member/notification-settings.ts"],"sourcesContent":["import type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../../app/container.js';\nimport { resolveCaller } from '../../scope.js';\nimport {\n CommunityParam,\n CommunityNotificationSettings,\n OrgNotificationSettings,\n PatchNotificationSettings,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberNotificationSettingsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Notifications'];\n\n async function memberOf(req: FastifyRequest) {\n const caller = await resolveCaller(container, req);\n return { ...caller.actor, userId: caller.userId };\n }\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberCommunityNotificationSettings',\n tags,\n summary: 'Read how you are told about each notification category in a community',\n description:\n 'For anyone who can open the community. One entry per category of the list in use that can apply to you, each with the channels it offers on this deployment, whether each is on for you and why (`locked`, `space`, `community`, `spaceDefault` or `default`), and `default`, what the channel would be without your own setting at that level. A per-space category also lists every space you can open, with its name. `digest` is the digest schedule of the community, null when it sends none. 404 when you cannot open the community.',\n params: CommunityParam,\n response: { 200: CommunityNotificationSettings, ...refusals(403, 404) },\n },\n handler: async (req) => {\n return container.reporting.member.communityNotificationSettings(\n req.params.communityId,\n await memberOf(req),\n );\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/communities/:communityId/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setMemberCommunityNotificationSettings',\n tags,\n summary: 'Switch notification categories on or off per channel in a community',\n description:\n 'For anyone who can open the community. Each setting switches one category on one channel, for all spaces or, for a per-space category, for one space; `enabled: null` removes your choice so the default applies again. Settings left out keep their values. 404 when you cannot open the community; 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given for a category that is not per space.',\n params: CommunityParam,\n body: PatchNotificationSettings,\n response: { 204: z.void(), ...refusals(400, 403, 404, 422) },\n },\n handler: async (req, reply) => {\n await container.notificationSettings.set(\n { communityId: req.params.communityId },\n req.body.settings,\n await memberOf(req),\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotificationSettings',\n tags,\n summary: 'Read how you are told about notifications outside any community',\n description:\n \"For anyone signed in to the organization. The organization's categories that can apply to you, such as access and course content, each with the channels it offers, whether each is on for you, and `default`, what it would be without your own setting.\",\n response: { 200: OrgNotificationSettings, ...refusals(403) },\n },\n handler: async (req) => {\n return container.reporting.member.orgNotificationSettings(await memberOf(req));\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/settings/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setMemberNotificationSettings',\n tags,\n summary: 'Switch notification categories outside any community on or off per channel',\n description:\n \"For anyone signed in to the organization. Works like the community settings, against the organization's categories; `enabled: null` removes your choice. 422 when a category does not exist, does not offer the channel, the channel is locked, or `spaceId` is given.\",\n body: PatchNotificationSettings,\n response: { 204: z.void(), ...refusals(400, 403, 422) },\n },\n handler: async (req, reply) => {\n await container.notificationSettings.set('org', req.body.settings, await memberOf(req));\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAaA,eAAsB,iCACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,eAAe;CAE7B,eAAe,SAAS,KAAqB;EAC3C,MAAM,SAAS,MAAM,cAAc,WAAW,GAAG;EACjD,OAAO;GAAE,GAAG,OAAO;GAAO,QAAQ,OAAO;EAAO;CAClD;CAEA,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAA+B,GAAG,SAAS,KAAK,GAAG;GAAE;EACxE;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,UAAU,OAAO,8BAChC,IAAI,OAAO,aACX,MAAM,SAAS,GAAG,CACpB;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,qBAAqB,IACnC,EAAE,aAAa,IAAI,OAAO,YAAY,GACtC,IAAI,KAAK,UACT,MAAM,SAAS,GAAG,CACpB;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,UAAU;IAAE,KAAK;IAAyB,GAAG,SAAS,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,UAAU,OAAO,wBAAwB,MAAM,SAAS,GAAG,CAAC;EAC/E;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,qBAAqB,IAAI,OAAO,IAAI,KAAK,UAAU,MAAM,SAAS,GAAG,CAAC;GACtF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
@@ -1,13 +1,20 @@
1
1
  import { refusals } from "../../schemas/shared.js";
2
+ import { resolveCaller } from "../../scope.js";
2
3
  import { NotificationFilterQuery, NotificationIdParam, NotificationListQuery, NotificationUnreadCounts, NotificationView, NotificationsPage } from "../../schemas/notifications.js";
3
4
  import "../../schemas/index.js";
4
- import { actorOf } from "./actor.js";
5
+ import { InvalidInputError } from "@headless-lms/core/shared/errors";
5
6
  import { z } from "zod";
6
- import { InvalidInputError, NotFoundError } from "@headless-lms/core/shared/errors";
7
7
  //#region src/http/routes/member/notifications.ts
8
8
  async function memberNotificationRoutes(app, container) {
9
9
  const r = app.withTypeProvider();
10
10
  const tags = ["Notifications"];
11
+ async function memberOf(req) {
12
+ const caller = await resolveCaller(container, req);
13
+ return {
14
+ ...caller.actor,
15
+ userId: caller.userId
16
+ };
17
+ }
11
18
  r.route({
12
19
  method: "GET",
13
20
  url: "/api/member/notifications",
@@ -24,12 +31,11 @@ async function memberNotificationRoutes(app, container) {
24
31
  }
25
32
  },
26
33
  handler: async (req) => {
27
- const actor = await actorOf(container, req.orgId, req.userId);
28
34
  const { page, pageSize, ...filter } = req.query;
29
- return container.reporting.member.notifications(req.orgId, actor, req.userId, filter, {
35
+ return container.reporting.member.notifications(filter, {
30
36
  page,
31
37
  pageSize
32
- });
38
+ }, await memberOf(req));
33
39
  }
34
40
  });
35
41
  r.route({
@@ -40,15 +46,14 @@ async function memberNotificationRoutes(app, container) {
40
46
  operationId: "getMemberNotificationUnreadCounts",
41
47
  tags,
42
48
  summary: "Count your unread notifications in the org list and in each community",
43
- description: "For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open, staff included.",
49
+ description: "For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open.",
44
50
  response: {
45
51
  200: NotificationUnreadCounts,
46
52
  ...refusals(403)
47
53
  }
48
54
  },
49
55
  handler: async (req) => {
50
- const actor = await actorOf(container, req.orgId, req.userId);
51
- return container.reporting.member.notificationUnreadCounts(req.orgId, actor, req.userId);
56
+ return container.reporting.member.notificationUnreadCounts(await memberOf(req));
52
57
  }
53
58
  });
54
59
  r.route({
@@ -67,10 +72,7 @@ async function memberNotificationRoutes(app, container) {
67
72
  }
68
73
  },
69
74
  handler: async (req) => {
70
- const actor = await actorOf(container, req.orgId, req.userId);
71
- const view = await container.reporting.member.notification(req.orgId, actor, req.userId, req.params.notificationId);
72
- if (!view) throw new NotFoundError("Notification", req.params.notificationId);
73
- return view;
75
+ return container.reporting.member.notification(req.params.notificationId, await memberOf(req));
74
76
  }
75
77
  });
76
78
  r.route({
@@ -89,8 +91,7 @@ async function memberNotificationRoutes(app, container) {
89
91
  }
90
92
  },
91
93
  handler: async (req, reply) => {
92
- await actorOf(container, req.orgId, req.userId);
93
- await container.notificationList.markRead(req.orgId, req.userId, req.params.notificationId);
94
+ await container.notificationList.markRead(req.params.notificationId, await memberOf(req));
94
95
  return reply.code(204).send();
95
96
  }
96
97
  });
@@ -113,8 +114,7 @@ async function memberNotificationRoutes(app, container) {
113
114
  if (req.body !== void 0) throw new InvalidInputError("Mark-read takes its filters in the query string, not in a body");
114
115
  },
115
116
  handler: async (req, reply) => {
116
- await actorOf(container, req.orgId, req.userId);
117
- await container.notificationList.markAllRead(req.orgId, req.userId, req.query);
117
+ await container.notificationList.markAllRead(req.query, await memberOf(req));
118
118
  return reply.code(204).send();
119
119
  }
120
120
  });