@headless-lms/server 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/app/community-mail.d.ts +10 -10
- package/dist/app/community-mail.js +72 -28
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +48 -10
- package/dist/app/container.js +268 -48
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.d.ts +47 -0
- package/dist/app/notification-channels.js +175 -0
- package/dist/app/notification-channels.js.map +1 -0
- package/dist/app/notification-reads.d.ts +23 -0
- package/dist/app/notification-reads.js +74 -0
- package/dist/app/notification-reads.js.map +1 -0
- package/dist/app/platform.d.ts +14 -0
- package/dist/app/platform.js +14 -0
- package/dist/app/platform.js.map +1 -0
- package/dist/http/config.d.ts +4 -0
- package/dist/http/errors.d.ts +1 -0
- package/dist/http/errors.js +24 -4
- package/dist/http/errors.js.map +1 -1
- package/dist/http/fastify.d.ts +8 -0
- package/dist/http/mcp/catalogue.d.ts +11 -9
- package/dist/http/mcp/catalogue.js +15 -21
- package/dist/http/mcp/catalogue.js.map +1 -1
- package/dist/http/mcp/challenge.js +3 -3
- package/dist/http/mcp/challenge.js.map +1 -1
- package/dist/http/mcp/route.js +7 -7
- package/dist/http/mcp/route.js.map +1 -1
- package/dist/http/mcp/test-support.d.ts +6 -5
- package/dist/http/mcp/test-support.js +32 -11
- package/dist/http/mcp/test-support.js.map +1 -1
- package/dist/http/mcp/tool.d.ts +3 -9
- package/dist/http/mcp/tool.js +14 -6
- package/dist/http/mcp/tool.js.map +1 -1
- package/dist/http/mcp/tools/automations.js +10 -20
- package/dist/http/mcp/tools/automations.js.map +1 -1
- package/dist/http/mcp/tools/catalog.js +25 -42
- package/dist/http/mcp/tools/catalog.js.map +1 -1
- package/dist/http/mcp/tools/comments.js +9 -19
- package/dist/http/mcp/tools/comments.js.map +1 -1
- package/dist/http/mcp/tools/communities.js +16 -31
- package/dist/http/mcp/tools/communities.js.map +1 -1
- package/dist/http/mcp/tools/community-members.js +9 -16
- package/dist/http/mcp/tools/community-members.js.map +1 -1
- package/dist/http/mcp/tools/community-moderation.js +13 -25
- package/dist/http/mcp/tools/community-moderation.js.map +1 -1
- package/dist/http/mcp/tools/courses.js +11 -21
- package/dist/http/mcp/tools/courses.js.map +1 -1
- package/dist/http/mcp/tools/events.d.ts +6 -0
- package/dist/http/mcp/tools/events.js +170 -0
- package/dist/http/mcp/tools/events.js.map +1 -0
- package/dist/http/mcp/tools/index.d.ts +5 -1
- package/dist/http/mcp/tools/index.js +45 -10
- package/dist/http/mcp/tools/index.js.map +1 -1
- package/dist/http/mcp/tools/organization.d.ts +6 -0
- package/dist/http/mcp/tools/organization.js +214 -0
- package/dist/http/mcp/tools/organization.js.map +1 -0
- package/dist/http/mcp/tools/people.js +30 -49
- package/dist/http/mcp/tools/people.js.map +1 -1
- package/dist/http/plugins/auth.js +37 -8
- package/dist/http/plugins/auth.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +5 -1
- package/dist/http/plugins/error-handler.js +13 -3
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +18 -2
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/plugins/platform.d.ts +7 -0
- package/dist/http/plugins/platform.js +13 -0
- package/dist/http/plugins/platform.js.map +1 -0
- package/dist/http/routes/access.js +54 -36
- package/dist/http/routes/access.js.map +1 -1
- package/dist/http/routes/activities.js +45 -45
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +30 -65
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +37 -42
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.js +33 -36
- package/dist/http/routes/bundles.js.map +1 -1
- package/dist/http/routes/communities.js +144 -136
- package/dist/http/routes/communities.js.map +1 -1
- package/dist/http/routes/community-moderation.js +83 -137
- package/dist/http/routes/community-moderation.js.map +1 -1
- package/dist/http/routes/course-analytics.js +32 -15
- package/dist/http/routes/course-analytics.js.map +1 -1
- package/dist/http/routes/courses.js +25 -32
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +9 -9
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +21 -39
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +41 -47
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/entitlements.js +19 -23
- package/dist/http/routes/entitlements.js.map +1 -1
- package/dist/http/routes/events.d.ts +7 -0
- package/dist/http/routes/events.js +333 -0
- package/dist/http/routes/events.js.map +1 -0
- package/dist/http/routes/integrations.js +40 -73
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/member/assets.js +10 -13
- package/dist/http/routes/member/assets.js.map +1 -1
- package/dist/http/routes/member/chats.js +177 -75
- package/dist/http/routes/member/chats.js.map +1 -1
- package/dist/http/routes/member/comments.js +65 -26
- package/dist/http/routes/member/comments.js.map +1 -1
- package/dist/http/routes/member/community.js +68 -51
- package/dist/http/routes/member/community.js.map +1 -1
- package/dist/http/routes/member/courses.js +12 -19
- package/dist/http/routes/member/courses.js.map +1 -1
- package/dist/http/routes/member/devices.d.ts +7 -0
- package/dist/http/routes/member/devices.js +97 -0
- package/dist/http/routes/member/devices.js.map +1 -0
- package/dist/http/routes/member/downloads.js +6 -9
- package/dist/http/routes/member/downloads.js.map +1 -1
- package/dist/http/routes/member/events.d.ts +7 -0
- package/dist/http/routes/member/events.js +386 -0
- package/dist/http/routes/member/events.js.map +1 -0
- package/dist/http/routes/member/files.js +71 -46
- package/dist/http/routes/member/files.js.map +1 -1
- package/dist/http/routes/member/index.js +6 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.d.ts +7 -0
- package/dist/http/routes/member/notification-settings.js +100 -0
- package/dist/http/routes/member/notification-settings.js.map +1 -0
- package/dist/http/routes/member/notifications.js +69 -45
- package/dist/http/routes/member/notifications.js.map +1 -1
- package/dist/http/routes/member/org.js +5 -15
- package/dist/http/routes/member/org.js.map +1 -1
- package/dist/http/routes/member/posts.js +94 -95
- package/dist/http/routes/member/posts.js.map +1 -1
- package/dist/http/routes/member/profile.js +15 -6
- package/dist/http/routes/member/profile.js.map +1 -1
- package/dist/http/routes/member/progress.js +7 -11
- package/dist/http/routes/member/progress.js.map +1 -1
- package/dist/http/routes/organizations.js +415 -110
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/platform.d.ts +7 -0
- package/dist/http/routes/platform.js +61 -0
- package/dist/http/routes/platform.js.map +1 -0
- package/dist/http/routes/students.js +12 -75
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.js +30 -39
- package/dist/http/routes/webhooks.js.map +1 -1
- package/dist/http/routes.js +9 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/access.d.ts +346 -64
- package/dist/http/schemas/access.js +39 -12
- package/dist/http/schemas/access.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +13 -8
- package/dist/http/schemas/assets.js +6 -5
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +4 -8
- package/dist/http/schemas/chats.d.ts +238 -36
- package/dist/http/schemas/chats.js +14 -7
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +140 -33
- package/dist/http/schemas/community.js +25 -9
- package/dist/http/schemas/community.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +51 -1
- package/dist/http/schemas/course-analytics.js +19 -1
- package/dist/http/schemas/course-analytics.js.map +1 -1
- package/dist/http/schemas/dashboard.d.ts +1 -0
- package/dist/http/schemas/dashboard.js +2 -1
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +45 -73
- package/dist/http/schemas/discussion.js +16 -4
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +9 -12
- package/dist/http/schemas/entitlements.js +11 -5
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/events.d.ts +527 -0
- package/dist/http/schemas/events.js +64 -0
- package/dist/http/schemas/events.js.map +1 -0
- package/dist/http/schemas/files.d.ts +15 -5
- package/dist/http/schemas/files.js +10 -2
- package/dist/http/schemas/files.js.map +1 -1
- package/dist/http/schemas/index.d.ts +14 -12
- package/dist/http/schemas/index.js +14 -12
- package/dist/http/schemas/invites.d.ts +3 -47
- package/dist/http/schemas/invites.js +4 -25
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/member.d.ts +94 -7
- package/dist/http/schemas/member.js +7 -3
- package/dist/http/schemas/member.js.map +1 -1
- package/dist/http/schemas/members.d.ts +329 -65
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +502 -175
- package/dist/http/schemas/notifications.js +148 -28
- package/dist/http/schemas/notifications.js.map +1 -1
- package/dist/http/schemas/platform.d.ts +37 -0
- package/dist/http/schemas/platform.js +19 -0
- package/dist/http/schemas/platform.js.map +1 -0
- package/dist/http/schemas/posts.d.ts +92 -38
- package/dist/http/schemas/posts.js +4 -4
- package/dist/http/schemas/posts.js.map +1 -1
- package/dist/http/schemas/profile.d.ts +1 -1
- package/dist/http/schemas/shared.d.ts +10 -13
- package/dist/http/schemas/shared.js +3 -11
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +22 -0
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -12
- package/dist/http/scope.js +30 -35
- package/dist/http/scope.js.map +1 -1
- package/dist/http/server.js +9 -0
- package/dist/http/server.js.map +1 -1
- package/dist/http/shutdown.d.ts +11 -0
- package/dist/http/shutdown.js +40 -0
- package/dist/http/shutdown.js.map +1 -0
- package/dist/http/sse.d.ts +19 -0
- package/dist/http/sse.js +104 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/http/test-caller.d.ts +13 -0
- package/dist/http/test-caller.js +14 -0
- package/dist/http/test-caller.js.map +1 -0
- package/dist/http/well-known.js +2 -2
- package/dist/http/well-known.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packages/server/package.js +1 -1
- package/package.json +7 -6
- package/dist/http/routes/member/actor.d.ts +0 -14
- package/dist/http/routes/member/actor.js +0 -11
- package/dist/http/routes/member/actor.js.map +0 -1
|
@@ -1 +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,8 +4,11 @@ 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";
|
|
10
|
+
import { memberNotificationSettingsRoutes } from "./notification-settings.js";
|
|
11
|
+
import { memberDeviceRoutes } from "./devices.js";
|
|
9
12
|
import { memberOrgRoutes } from "./org.js";
|
|
10
13
|
import { memberPostRoutes } from "./posts.js";
|
|
11
14
|
import { memberProfileRoutes } from "./profile.js";
|
|
@@ -22,8 +25,11 @@ async function memberRoutes(app, container) {
|
|
|
22
25
|
await memberCommentsRoutes(app, container);
|
|
23
26
|
await memberAssetsRoutes(app, container);
|
|
24
27
|
await memberDownloadsRoutes(app, container);
|
|
28
|
+
await memberEventsRoutes(app, container);
|
|
25
29
|
await memberFileRoutes(app, container);
|
|
26
30
|
await memberNotificationRoutes(app, container);
|
|
31
|
+
await memberNotificationSettingsRoutes(app, container);
|
|
32
|
+
await memberDeviceRoutes(app, container);
|
|
27
33
|
}
|
|
28
34
|
//#endregion
|
|
29
35
|
export { memberRoutes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/http/routes/member/index.ts"],"sourcesContent":["//
|
|
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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Container } from "../../../app/container.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/routes/member/notification-settings.d.ts
|
|
4
|
+
declare function memberNotificationSettingsRoutes(app: FastifyInstance, container: Container): Promise<void>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { memberNotificationSettingsRoutes };
|
|
7
|
+
//# sourceMappingURL=notification-settings.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { refusals } from "../../schemas/shared.js";
|
|
2
|
+
import { resolveCaller } from "../../scope.js";
|
|
3
|
+
import { CommunityParam } from "../../schemas/chats.js";
|
|
4
|
+
import { CommunityNotificationSettings, OrgNotificationSettings, PatchNotificationSettings } from "../../schemas/notifications.js";
|
|
5
|
+
import "../../schemas/index.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/http/routes/member/notification-settings.ts
|
|
8
|
+
async function memberNotificationSettingsRoutes(app, container) {
|
|
9
|
+
const r = app.withTypeProvider();
|
|
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
|
+
}
|
|
18
|
+
r.route({
|
|
19
|
+
method: "GET",
|
|
20
|
+
url: "/api/member/communities/:communityId/settings/notifications",
|
|
21
|
+
preHandler: app.requireOrgSession,
|
|
22
|
+
schema: {
|
|
23
|
+
operationId: "getMemberCommunityNotificationSettings",
|
|
24
|
+
tags,
|
|
25
|
+
summary: "Read how you are told about each notification category in a 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.",
|
|
27
|
+
params: CommunityParam,
|
|
28
|
+
response: {
|
|
29
|
+
200: CommunityNotificationSettings,
|
|
30
|
+
...refusals(403, 404)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
handler: async (req) => {
|
|
34
|
+
return container.reporting.member.communityNotificationSettings(req.params.communityId, await memberOf(req));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
r.route({
|
|
38
|
+
method: "PATCH",
|
|
39
|
+
url: "/api/member/communities/:communityId/settings/notifications",
|
|
40
|
+
preHandler: app.requireOrgSession,
|
|
41
|
+
schema: {
|
|
42
|
+
operationId: "setMemberCommunityNotificationSettings",
|
|
43
|
+
tags,
|
|
44
|
+
summary: "Switch notification categories on or off per channel in a community",
|
|
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.",
|
|
46
|
+
params: CommunityParam,
|
|
47
|
+
body: PatchNotificationSettings,
|
|
48
|
+
response: {
|
|
49
|
+
204: z.void(),
|
|
50
|
+
...refusals(400, 403, 404, 422)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
handler: async (req, reply) => {
|
|
54
|
+
await container.notificationSettings.set({ communityId: req.params.communityId }, req.body.settings, await memberOf(req));
|
|
55
|
+
return reply.code(204).send();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
r.route({
|
|
59
|
+
method: "GET",
|
|
60
|
+
url: "/api/member/settings/notifications",
|
|
61
|
+
preHandler: app.requireOrgSession,
|
|
62
|
+
schema: {
|
|
63
|
+
operationId: "getMemberNotificationSettings",
|
|
64
|
+
tags,
|
|
65
|
+
summary: "Read how you are told about notifications outside any community",
|
|
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.",
|
|
67
|
+
response: {
|
|
68
|
+
200: OrgNotificationSettings,
|
|
69
|
+
...refusals(403)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
handler: async (req) => {
|
|
73
|
+
return container.reporting.member.orgNotificationSettings(await memberOf(req));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
r.route({
|
|
77
|
+
method: "PATCH",
|
|
78
|
+
url: "/api/member/settings/notifications",
|
|
79
|
+
preHandler: app.requireOrgSession,
|
|
80
|
+
schema: {
|
|
81
|
+
operationId: "setMemberNotificationSettings",
|
|
82
|
+
tags,
|
|
83
|
+
summary: "Switch notification categories outside any community on or off per channel",
|
|
84
|
+
description: "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.",
|
|
85
|
+
body: PatchNotificationSettings,
|
|
86
|
+
response: {
|
|
87
|
+
204: z.void(),
|
|
88
|
+
...refusals(400, 403, 422)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
handler: async (req, reply) => {
|
|
92
|
+
await container.notificationSettings.set("org", req.body.settings, await memberOf(req));
|
|
93
|
+
return reply.code(204).send();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { memberNotificationSettingsRoutes };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=notification-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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,12 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { refusals } from "../../schemas/shared.js";
|
|
2
|
+
import { resolveCaller } from "../../scope.js";
|
|
3
|
+
import { NotificationFilterQuery, NotificationIdParam, NotificationListQuery, NotificationUnreadCounts, NotificationView, NotificationsPage } from "../../schemas/notifications.js";
|
|
3
4
|
import "../../schemas/index.js";
|
|
4
|
-
import {
|
|
5
|
+
import { InvalidInputError } from "@headless-lms/core/shared/errors";
|
|
6
|
+
import { z } from "zod";
|
|
5
7
|
//#region src/http/routes/member/notifications.ts
|
|
6
8
|
async function memberNotificationRoutes(app, container) {
|
|
7
9
|
const r = app.withTypeProvider();
|
|
8
|
-
const notifications = container.notifications;
|
|
9
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
|
+
}
|
|
10
18
|
r.route({
|
|
11
19
|
method: "GET",
|
|
12
20
|
url: "/api/member/notifications",
|
|
@@ -14,84 +22,100 @@ async function memberNotificationRoutes(app, container) {
|
|
|
14
22
|
schema: {
|
|
15
23
|
operationId: "listMemberNotifications",
|
|
16
24
|
tags,
|
|
17
|
-
summary: "List your notifications, newest first, with
|
|
18
|
-
description: "For any member of the organization. Each notification names what it refers to; a
|
|
19
|
-
querystring:
|
|
25
|
+
summary: "List your notifications, newest first, with the unread count of the same filters",
|
|
26
|
+
description: "For any member of the organization. Filters combine with AND; the values of one filter combine with OR, and `includeOrg=true` with `communityId` adds the org list, so one query reads a community with the course and account notifications. 400 for `includeOrg` without `communityId` or with `scope`. Each notification carries the current titles, names and excerpts of what it refers to; a subject or parent you can no longer open, or that was deleted, is null. `display` is the text stored when it was sent.",
|
|
27
|
+
querystring: NotificationListQuery,
|
|
20
28
|
response: {
|
|
21
29
|
200: NotificationsPage,
|
|
22
|
-
...refusals(403)
|
|
30
|
+
...refusals(400, 403)
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
handler: async (req) => {
|
|
26
|
-
const
|
|
27
|
-
return container.reporting.member.notifications(
|
|
34
|
+
const { page, pageSize, ...filter } = req.query;
|
|
35
|
+
return container.reporting.member.notifications(filter, {
|
|
36
|
+
page,
|
|
37
|
+
pageSize
|
|
38
|
+
}, await memberOf(req));
|
|
28
39
|
}
|
|
29
40
|
});
|
|
30
41
|
r.route({
|
|
31
|
-
method: "
|
|
32
|
-
url: "/api/member/notifications
|
|
42
|
+
method: "GET",
|
|
43
|
+
url: "/api/member/notifications/unread-counts",
|
|
33
44
|
preHandler: app.requireOrgSession,
|
|
34
45
|
schema: {
|
|
35
|
-
operationId: "
|
|
46
|
+
operationId: "getMemberNotificationUnreadCounts",
|
|
36
47
|
tags,
|
|
37
|
-
summary: "
|
|
38
|
-
description: "For any member of the organization.
|
|
39
|
-
params: NotificationIdParam,
|
|
48
|
+
summary: "Count your unread notifications in the org list and in each community",
|
|
49
|
+
description: "For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open.",
|
|
40
50
|
response: {
|
|
41
|
-
200:
|
|
42
|
-
...refusals(403
|
|
51
|
+
200: NotificationUnreadCounts,
|
|
52
|
+
...refusals(403)
|
|
43
53
|
}
|
|
44
54
|
},
|
|
45
55
|
handler: async (req) => {
|
|
46
|
-
|
|
47
|
-
const notification = await notifications.markNotificationRead(req.orgId, req.userId, req.params.notificationId);
|
|
48
|
-
return container.reporting.member.notificationView(req.orgId, actor, notification);
|
|
56
|
+
return container.reporting.member.notificationUnreadCounts(await memberOf(req));
|
|
49
57
|
}
|
|
50
58
|
});
|
|
51
59
|
r.route({
|
|
52
|
-
method: "
|
|
53
|
-
url: "/api/member/notifications
|
|
60
|
+
method: "GET",
|
|
61
|
+
url: "/api/member/notifications/:notificationId",
|
|
54
62
|
preHandler: app.requireOrgSession,
|
|
55
63
|
schema: {
|
|
56
|
-
operationId: "
|
|
64
|
+
operationId: "getMemberNotification",
|
|
57
65
|
tags,
|
|
58
|
-
summary: "
|
|
59
|
-
description: "For any
|
|
60
|
-
|
|
66
|
+
summary: "Read one of your notifications",
|
|
67
|
+
description: "For any member of the organization. The notification as the list shows it, with current titles, names and excerpts; reading it does not mark it read. 404 when it is not yours or was not shown in the app.",
|
|
68
|
+
params: NotificationIdParam,
|
|
69
|
+
response: {
|
|
70
|
+
200: NotificationView,
|
|
71
|
+
...refusals(403, 404)
|
|
72
|
+
}
|
|
61
73
|
},
|
|
62
74
|
handler: async (req) => {
|
|
63
|
-
return
|
|
75
|
+
return container.reporting.member.notification(req.params.notificationId, await memberOf(req));
|
|
64
76
|
}
|
|
65
77
|
});
|
|
66
78
|
r.route({
|
|
67
|
-
method: "
|
|
68
|
-
url: "/api/member/
|
|
79
|
+
method: "POST",
|
|
80
|
+
url: "/api/member/notifications/:notificationId/read",
|
|
69
81
|
preHandler: app.requireOrgSession,
|
|
70
82
|
schema: {
|
|
71
|
-
operationId: "
|
|
83
|
+
operationId: "markMemberNotificationRead",
|
|
72
84
|
tags,
|
|
73
|
-
summary: "
|
|
74
|
-
description: "For any
|
|
75
|
-
|
|
85
|
+
summary: "Mark one of your notifications read",
|
|
86
|
+
description: "For any member of the organization. Marking a read notification keeps its first read time. 404 when it is not yours.",
|
|
87
|
+
params: NotificationIdParam,
|
|
88
|
+
response: {
|
|
89
|
+
204: z.void(),
|
|
90
|
+
...refusals(403, 404)
|
|
91
|
+
}
|
|
76
92
|
},
|
|
77
|
-
handler: async (req) => {
|
|
78
|
-
|
|
93
|
+
handler: async (req, reply) => {
|
|
94
|
+
await container.notificationList.markRead(req.params.notificationId, await memberOf(req));
|
|
95
|
+
return reply.code(204).send();
|
|
79
96
|
}
|
|
80
97
|
});
|
|
81
98
|
r.route({
|
|
82
|
-
method: "
|
|
83
|
-
url: "/api/member/
|
|
99
|
+
method: "POST",
|
|
100
|
+
url: "/api/member/notifications/read",
|
|
84
101
|
preHandler: app.requireOrgSession,
|
|
85
102
|
schema: {
|
|
86
|
-
operationId: "
|
|
103
|
+
operationId: "markAllMemberNotificationsRead",
|
|
87
104
|
tags,
|
|
88
|
-
summary: "
|
|
89
|
-
description: "For any
|
|
90
|
-
|
|
91
|
-
response: {
|
|
105
|
+
summary: "Mark your notifications matching the filters read",
|
|
106
|
+
description: "For any member of the organization. Takes the filters of the list in the query string, `includeOrg` included; with none, marks every notification read. 400 when the request has a body.",
|
|
107
|
+
querystring: NotificationFilterQuery,
|
|
108
|
+
response: {
|
|
109
|
+
204: z.void(),
|
|
110
|
+
...refusals(400, 403)
|
|
111
|
+
}
|
|
92
112
|
},
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
preValidation: async (req) => {
|
|
114
|
+
if (req.body !== void 0) throw new InvalidInputError("Mark-read takes its filters in the query string, not in a body");
|
|
115
|
+
},
|
|
116
|
+
handler: async (req, reply) => {
|
|
117
|
+
await container.notificationList.markAllRead(req.query, await memberOf(req));
|
|
118
|
+
return reply.code(204).send();
|
|
95
119
|
}
|
|
96
120
|
});
|
|
97
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.js","names":[],"sources":["../../../../src/http/routes/member/notifications.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport type { Container } from '../../../app/container.js';\nimport {
|
|
1
|
+
{"version":3,"file":"notifications.js","names":[],"sources":["../../../../src/http/routes/member/notifications.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 { InvalidInputError } from '@headless-lms/core/shared/errors';\nimport { resolveCaller } from '../../scope.js';\nimport {\n NotificationFilterQuery,\n NotificationIdParam,\n NotificationListQuery,\n NotificationUnreadCounts,\n NotificationView,\n NotificationsPage,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberNotificationRoutes(\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/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberNotifications',\n tags,\n summary: 'List your notifications, newest first, with the unread count of the same filters',\n description:\n 'For any member of the organization. Filters combine with AND; the values of one filter combine with OR, and `includeOrg=true` with `communityId` adds the org list, so one query reads a community with the course and account notifications. 400 for `includeOrg` without `communityId` or with `scope`. Each notification carries the current titles, names and excerpts of what it refers to; a subject or parent you can no longer open, or that was deleted, is null. `display` is the text stored when it was sent.',\n querystring: NotificationListQuery,\n response: { 200: NotificationsPage, ...refusals(400, 403) },\n },\n handler: async (req) => {\n const { page, pageSize, ...filter } = req.query;\n return container.reporting.member.notifications(\n filter,\n { page, pageSize },\n await memberOf(req),\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications/unread-counts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotificationUnreadCounts',\n tags,\n summary: 'Count your unread notifications in the org list and in each community',\n description:\n 'For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open.',\n response: { 200: NotificationUnreadCounts, ...refusals(403) },\n },\n handler: async (req) => {\n return container.reporting.member.notificationUnreadCounts(await memberOf(req));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications/:notificationId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotification',\n tags,\n summary: 'Read one of your notifications',\n description:\n 'For any member of the organization. The notification as the list shows it, with current titles, names and excerpts; reading it does not mark it read. 404 when it is not yours or was not shown in the app.',\n params: NotificationIdParam,\n response: { 200: NotificationView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n return container.reporting.member.notification(\n req.params.notificationId,\n await memberOf(req),\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/:notificationId/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markMemberNotificationRead',\n tags,\n summary: 'Mark one of your notifications read',\n description:\n 'For any member of the organization. Marking a read notification keeps its first read time. 404 when it is not yours.',\n params: NotificationIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n await container.notificationList.markRead(req.params.notificationId, await memberOf(req));\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markAllMemberNotificationsRead',\n tags,\n summary: 'Mark your notifications matching the filters read',\n description:\n 'For any member of the organization. Takes the filters of the list in the query string, `includeOrg` included; with none, marks every notification read. 400 when the request has a body.',\n querystring: NotificationFilterQuery,\n response: { 204: z.void(), ...refusals(400, 403) },\n },\n preValidation: async (req) => {\n if (req.body !== undefined) {\n throw new InvalidInputError(\n 'Mark-read takes its filters in the query string, not in a body',\n );\n }\n },\n handler: async (req, reply) => {\n await container.notificationList.markAllRead(req.query, await memberOf(req));\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAgBA,eAAsB,yBACpB,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,aAAa;GACb,UAAU;IAAE,KAAK;IAAmB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC5D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,EAAE,MAAM,UAAU,GAAG,WAAW,IAAI;GAC1C,OAAO,UAAU,UAAU,OAAO,cAChC,QACA;IAAE;IAAM;GAAS,GACjB,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,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,UAAU,OAAO,yBAAyB,MAAM,SAAS,GAAG,CAAC;EAChF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,UAAU,OAAO,aAChC,IAAI,OAAO,gBACX,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,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,iBAAiB,SAAS,IAAI,OAAO,gBAAgB,MAAM,SAAS,GAAG,CAAC;GACxF,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,aAAa;GACb,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,eAAe,OAAO,QAAQ;GAC5B,IAAI,IAAI,SAAS,KAAA,GACf,MAAM,IAAI,kBACR,gEACF;EAEJ;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,iBAAiB,YAAY,IAAI,OAAO,MAAM,SAAS,GAAG,CAAC;GAC3E,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { refusals } from "../../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../../scope.js";
|
|
3
3
|
import { MemberOrg, MemberViewer } from "../../schemas/member.js";
|
|
4
4
|
import "../../schemas/index.js";
|
|
5
|
-
import { actorOf } from "./actor.js";
|
|
6
5
|
//#region src/http/routes/member/org.ts
|
|
7
6
|
async function memberOrgRoutes(app, container) {
|
|
8
7
|
const r = app.withTypeProvider();
|
|
@@ -18,13 +17,7 @@ async function memberOrgRoutes(app, container) {
|
|
|
18
17
|
response: { 200: MemberOrg }
|
|
19
18
|
},
|
|
20
19
|
handler: async (req) => {
|
|
21
|
-
|
|
22
|
-
if (!org) throw new UnauthorizedError("Your session points to an organization that no longer exists");
|
|
23
|
-
return {
|
|
24
|
-
id: org.id,
|
|
25
|
-
name: org.name,
|
|
26
|
-
slug: org.slug
|
|
27
|
-
};
|
|
20
|
+
return container.organizations.portalIdentity(req.orgId);
|
|
28
21
|
}
|
|
29
22
|
});
|
|
30
23
|
r.route({
|
|
@@ -35,18 +28,15 @@ async function memberOrgRoutes(app, container) {
|
|
|
35
28
|
operationId: "getMemberViewer",
|
|
36
29
|
tags: ["Organizations"],
|
|
37
30
|
summary: "Get the caller's identity within the session's org",
|
|
38
|
-
description: "For a member of the organization: your org user id, which the member API keys on,
|
|
31
|
+
description: "For a member of the organization: your org user id, which the member API keys on, your org role, the permissions you hold anywhere, and those you hold on the org.",
|
|
39
32
|
response: {
|
|
40
33
|
200: MemberViewer,
|
|
41
34
|
...refusals(403)
|
|
42
35
|
}
|
|
43
36
|
},
|
|
44
37
|
handler: async (req) => {
|
|
45
|
-
const
|
|
46
|
-
return
|
|
47
|
-
orgUserId: orgUser.id,
|
|
48
|
-
role: orgUser.role
|
|
49
|
-
};
|
|
38
|
+
const actor = await resolveActor(container, req);
|
|
39
|
+
return container.reporting.member.viewer(actor);
|
|
50
40
|
}
|
|
51
41
|
});
|
|
52
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org.js","names":[],"sources":["../../../../src/http/routes/member/org.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { MemberOrg, MemberViewer, refusals } from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport {
|
|
1
|
+
{"version":3,"file":"org.js","names":[],"sources":["../../../../src/http/routes/member/org.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { MemberOrg, MemberViewer, refusals } from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { resolveActor } from '../../scope.js';\n\nexport async function memberOrgRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/member/org',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberOrg',\n tags: ['Organizations'],\n summary: \"Get the portal org's public identity (branding)\",\n description:\n 'For any signed-in person whose session has this organization active. Name and slug only.',\n response: { 200: MemberOrg },\n },\n handler: async (req) => {\n return container.organizations.portalIdentity(req.orgId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/viewer',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberViewer',\n tags: ['Organizations'],\n summary: \"Get the caller's identity within the session's org\",\n description:\n 'For a member of the organization: your org user id, which the member API keys on, your org role, the permissions you hold anywhere, and those you hold on the org.',\n response: { 200: MemberViewer, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.member.viewer(actor);\n },\n });\n}\n"],"mappings":";;;;;AAMA,eAAsB,gBAAgB,KAAsB,WAAqC;CAC/F,MAAM,IAAI,IAAI,iBAAkC;CAEhD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aACE;GACF,UAAU,EAAE,KAAK,UAAU;EAC7B;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,cAAc,eAAe,IAAI,KAAK;EACzD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aACE;GACF,UAAU;IAAE,KAAK;IAAc,GAAG,SAAS,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;EAChD;CACF,CAAC;AACH"}
|