@headless-lms/server 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/app/community-mail.d.ts +5 -3
- package/dist/app/community-mail.js +60 -12
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +24 -3
- package/dist/app/container.js +102 -38
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.js +1 -0
- package/dist/app/notification-channels.js.map +1 -1
- package/dist/app/notification-reads.d.ts +3 -3
- package/dist/app/notification-reads.js +7 -28
- package/dist/app/notification-reads.js.map +1 -1
- package/dist/app/platform.d.ts +14 -0
- package/dist/app/platform.js +14 -0
- package/dist/app/platform.js.map +1 -0
- package/dist/http/config.d.ts +4 -0
- package/dist/http/errors.js +9 -4
- package/dist/http/errors.js.map +1 -1
- package/dist/http/fastify.d.ts +8 -0
- package/dist/http/mcp/catalogue.d.ts +11 -9
- package/dist/http/mcp/catalogue.js +15 -21
- package/dist/http/mcp/catalogue.js.map +1 -1
- package/dist/http/mcp/challenge.js +3 -3
- package/dist/http/mcp/challenge.js.map +1 -1
- package/dist/http/mcp/route.js +7 -7
- package/dist/http/mcp/route.js.map +1 -1
- package/dist/http/mcp/test-support.d.ts +6 -5
- package/dist/http/mcp/test-support.js +32 -11
- package/dist/http/mcp/test-support.js.map +1 -1
- package/dist/http/mcp/tool.d.ts +3 -9
- package/dist/http/mcp/tool.js +14 -6
- package/dist/http/mcp/tool.js.map +1 -1
- package/dist/http/mcp/tools/automations.js +10 -20
- package/dist/http/mcp/tools/automations.js.map +1 -1
- package/dist/http/mcp/tools/catalog.js +25 -42
- package/dist/http/mcp/tools/catalog.js.map +1 -1
- package/dist/http/mcp/tools/comments.js +9 -19
- package/dist/http/mcp/tools/comments.js.map +1 -1
- package/dist/http/mcp/tools/communities.js +16 -31
- package/dist/http/mcp/tools/communities.js.map +1 -1
- package/dist/http/mcp/tools/community-members.js +9 -16
- package/dist/http/mcp/tools/community-members.js.map +1 -1
- package/dist/http/mcp/tools/community-moderation.js +13 -25
- package/dist/http/mcp/tools/community-moderation.js.map +1 -1
- package/dist/http/mcp/tools/courses.js +11 -21
- package/dist/http/mcp/tools/courses.js.map +1 -1
- package/dist/http/mcp/tools/events.d.ts +6 -0
- package/dist/http/mcp/tools/events.js +170 -0
- package/dist/http/mcp/tools/events.js.map +1 -0
- package/dist/http/mcp/tools/index.d.ts +5 -1
- package/dist/http/mcp/tools/index.js +45 -10
- package/dist/http/mcp/tools/index.js.map +1 -1
- package/dist/http/mcp/tools/organization.d.ts +6 -0
- package/dist/http/mcp/tools/organization.js +214 -0
- package/dist/http/mcp/tools/organization.js.map +1 -0
- package/dist/http/mcp/tools/people.js +30 -49
- package/dist/http/mcp/tools/people.js.map +1 -1
- package/dist/http/plugins/auth.js +37 -8
- package/dist/http/plugins/auth.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +5 -1
- package/dist/http/plugins/error-handler.js +10 -2
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +18 -2
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/plugins/platform.d.ts +7 -0
- package/dist/http/plugins/platform.js +13 -0
- package/dist/http/plugins/platform.js.map +1 -0
- package/dist/http/routes/access.js +54 -36
- package/dist/http/routes/access.js.map +1 -1
- package/dist/http/routes/activities.js +45 -45
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +30 -65
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +37 -42
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.js +33 -36
- package/dist/http/routes/bundles.js.map +1 -1
- package/dist/http/routes/communities.js +93 -145
- package/dist/http/routes/communities.js.map +1 -1
- package/dist/http/routes/community-moderation.js +83 -137
- package/dist/http/routes/community-moderation.js.map +1 -1
- package/dist/http/routes/course-analytics.js +32 -15
- package/dist/http/routes/course-analytics.js.map +1 -1
- package/dist/http/routes/courses.js +25 -32
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +9 -9
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +21 -39
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +41 -47
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/entitlements.js +19 -23
- package/dist/http/routes/entitlements.js.map +1 -1
- package/dist/http/routes/events.d.ts +7 -0
- package/dist/http/routes/events.js +333 -0
- package/dist/http/routes/events.js.map +1 -0
- package/dist/http/routes/integrations.js +40 -73
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/member/assets.js +10 -13
- package/dist/http/routes/member/assets.js.map +1 -1
- package/dist/http/routes/member/chats.js +170 -47
- package/dist/http/routes/member/chats.js.map +1 -1
- package/dist/http/routes/member/comments.js +65 -26
- package/dist/http/routes/member/comments.js.map +1 -1
- package/dist/http/routes/member/community.js +45 -52
- package/dist/http/routes/member/community.js.map +1 -1
- package/dist/http/routes/member/courses.js +12 -19
- package/dist/http/routes/member/courses.js.map +1 -1
- package/dist/http/routes/member/devices.js +9 -10
- package/dist/http/routes/member/devices.js.map +1 -1
- package/dist/http/routes/member/downloads.js +6 -9
- package/dist/http/routes/member/downloads.js.map +1 -1
- package/dist/http/routes/member/events.d.ts +7 -0
- package/dist/http/routes/member/events.js +386 -0
- package/dist/http/routes/member/events.js.map +1 -0
- package/dist/http/routes/member/files.js +71 -46
- package/dist/http/routes/member/files.js.map +1 -1
- package/dist/http/routes/member/index.js +2 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.js +15 -23
- package/dist/http/routes/member/notification-settings.js.map +1 -1
- package/dist/http/routes/member/notifications.js +16 -16
- package/dist/http/routes/member/notifications.js.map +1 -1
- package/dist/http/routes/member/org.js +5 -15
- package/dist/http/routes/member/org.js.map +1 -1
- package/dist/http/routes/member/posts.js +67 -68
- package/dist/http/routes/member/posts.js.map +1 -1
- package/dist/http/routes/member/profile.js +15 -6
- package/dist/http/routes/member/profile.js.map +1 -1
- package/dist/http/routes/member/progress.js +7 -11
- package/dist/http/routes/member/progress.js.map +1 -1
- package/dist/http/routes/organizations.js +319 -135
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/platform.d.ts +7 -0
- package/dist/http/routes/platform.js +61 -0
- package/dist/http/routes/platform.js.map +1 -0
- package/dist/http/routes/students.js +12 -75
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.js +30 -39
- package/dist/http/routes/webhooks.js.map +1 -1
- package/dist/http/routes.js +9 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/access.d.ts +346 -64
- package/dist/http/schemas/access.js +39 -12
- package/dist/http/schemas/access.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +19 -14
- package/dist/http/schemas/assets.js +6 -5
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +4 -4
- package/dist/http/schemas/chats.d.ts +211 -32
- package/dist/http/schemas/chats.js +13 -4
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +91 -44
- package/dist/http/schemas/community.js +21 -8
- package/dist/http/schemas/community.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +51 -1
- package/dist/http/schemas/course-analytics.js +19 -1
- package/dist/http/schemas/course-analytics.js.map +1 -1
- package/dist/http/schemas/dashboard.d.ts +1 -0
- package/dist/http/schemas/dashboard.js +2 -1
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +22 -72
- package/dist/http/schemas/discussion.js +2 -2
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +9 -12
- package/dist/http/schemas/entitlements.js +11 -5
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/events.d.ts +527 -0
- package/dist/http/schemas/events.js +64 -0
- package/dist/http/schemas/events.js.map +1 -0
- package/dist/http/schemas/files.d.ts +15 -5
- package/dist/http/schemas/files.js +10 -2
- package/dist/http/schemas/files.js.map +1 -1
- package/dist/http/schemas/index.d.ts +11 -9
- package/dist/http/schemas/index.js +11 -9
- package/dist/http/schemas/integrations.d.ts +4 -4
- package/dist/http/schemas/invites.d.ts +3 -47
- package/dist/http/schemas/invites.js +4 -25
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/member.d.ts +94 -7
- package/dist/http/schemas/member.js +7 -3
- package/dist/http/schemas/member.js.map +1 -1
- package/dist/http/schemas/members.d.ts +333 -69
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +57 -53
- package/dist/http/schemas/platform.d.ts +37 -0
- package/dist/http/schemas/platform.js +19 -0
- package/dist/http/schemas/platform.js.map +1 -0
- package/dist/http/schemas/posts.d.ts +55 -35
- package/dist/http/schemas/posts.js +4 -4
- package/dist/http/schemas/posts.js.map +1 -1
- package/dist/http/schemas/profile.d.ts +1 -1
- package/dist/http/schemas/shared.d.ts +1 -11
- package/dist/http/schemas/shared.js +1 -10
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +25 -3
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -13
- package/dist/http/scope.js +29 -40
- package/dist/http/scope.js.map +1 -1
- package/dist/http/server.js +9 -0
- package/dist/http/server.js.map +1 -1
- package/dist/http/shutdown.d.ts +11 -0
- package/dist/http/shutdown.js +40 -0
- package/dist/http/shutdown.js.map +1 -0
- package/dist/http/sse.d.ts +19 -0
- package/dist/http/sse.js +104 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/http/test-caller.d.ts +13 -0
- package/dist/http/test-caller.js +14 -0
- package/dist/http/test-caller.js.map +1 -0
- package/dist/http/well-known.js +2 -2
- package/dist/http/well-known.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packages/server/package.js +1 -1
- package/package.json +7 -6
- package/dist/http/routes/member/actor.d.ts +0 -14
- package/dist/http/routes/member/actor.js +0 -11
- package/dist/http/routes/member/actor.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"community-moderation.js","names":[],"sources":["../../../src/http/routes/community-moderation.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 { resolveScope } from '../scope.js';\nimport {\n Chat,\n CommentView,\n CommunityChatParam,\n CommunityCommentParam,\n CommunityIdParam,\n CommunityMembersPage,\n CommunityMembersQuery,\n CommunityMessageParam,\n CommunityPostParam,\n CommunitySpaceParam,\n OrgUserBody,\n PageQuery,\n PostView,\n ResolveCommentFlags,\n ResolveMessageFlags,\n ResolvePostFlags,\n SpaceMember,\n SpaceMemberParam,\n SpaceMembersPage,\n SpaceQueue,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communityModerationRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/members',\n schema: {\n operationId: 'listCommunityMembers',\n tags,\n summary: 'List the people who hold a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Everyone with active access to the community, granted directly or through a bundle, paged and searched by name or email.',\n params: CommunityIdParam,\n querystring: CommunityMembersQuery,\n response: { 200: CommunityMembersPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const page = await container.reporting.admin.communityMembers(\n scope.orgId,\n req.params.communityId,\n req.query,\n );\n if (!page) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return page;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId/members',\n schema: {\n operationId: 'listCommunitySpaceMembers',\n tags,\n summary: 'List the members, requests and invitations of a space (staff)',\n description:\n \"Staff only (owner, admin, instructor). The space's members, its open requests to join and its open invitations.\",\n params: CommunitySpaceParam,\n querystring: PageQuery,\n response: { 200: SpaceMembersPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.entitlements.listSpaceMembers(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.query,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members',\n schema: {\n operationId: 'addCommunitySpaceMember',\n tags,\n summary: 'Add a person to a space (staff)',\n description:\n 'Staff only (owner, admin, instructor). Places a member of the community straight into the space, with no request or invitation. 409 when the space is open to all members, the person is already placed, or the space is full.',\n params: CommunitySpaceParam,\n body: OrgUserBody,\n response: { 201: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const member = await container.entitlements.addSpaceMember(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.body.orgUserId,\n );\n return reply.code(201).send(member);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/invites',\n schema: {\n operationId: 'inviteCommunitySpaceMember',\n tags,\n summary: 'Invite a person into a space (staff)',\n description:\n 'Staff only (owner, admin, instructor). Invites a member of the community into a `by_invite` space; they join when they accept. 403 when the person is staff or does not hold the community; 409 when the space is not invite-only or the person is already placed.',\n params: CommunitySpaceParam,\n body: OrgUserBody,\n response: { 201: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const member = await container.entitlements.invite(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.body.orgUserId,\n );\n return reply.code(201).send(member);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/approval',\n schema: {\n operationId: 'approveCommunitySpaceRequest',\n tags,\n summary: \"Approve a person's request to join a space (staff)\",\n description:\n 'Staff only (owner, admin, instructor). Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.',\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.entitlements.approve(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/decline',\n schema: {\n operationId: 'declineCommunitySpaceRequest',\n tags,\n summary: \"Decline a person's request to join a space (staff)\",\n description:\n 'Staff only (owner, admin, instructor). Closes the open request. 404 when the person has no open request; 409 when they are already a member.',\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.entitlements.decline(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/removal',\n schema: {\n operationId: 'removeCommunitySpaceMember',\n tags,\n summary: 'Remove a person from a space (staff)',\n description:\n 'Staff only (owner, admin, instructor). Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.',\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.entitlements.remove(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId/queue',\n schema: {\n operationId: 'getCommunitySpaceQueue',\n tags,\n summary: 'The moderation queue of a space (staff)',\n description:\n 'Staff only (owner, admin, instructor). Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.',\n params: CommunitySpaceParam,\n response: { 200: SpaceQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.posts.moderationQueue(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n req.params.spaceId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/approval',\n schema: {\n operationId: 'approveCommunityPost',\n tags,\n summary: 'Publish a post that is awaiting review (staff)',\n description:\n 'Staff only (owner, admin, instructor). 409 when the post is not awaiting review.',\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.posts.approve(\n scope.orgId,\n req.params.postId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/removal',\n schema: {\n operationId: 'removeCommunityPost',\n tags,\n summary: 'Remove a post (staff)',\n description:\n 'Staff only (owner, admin, instructor). Takes a published or pending post down and resolves its open flags as `removed`. 409 when it is already removed or was never submitted.',\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.posts.remove(\n scope.orgId,\n req.params.postId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/restoration',\n schema: {\n operationId: 'restoreCommunityPost',\n tags,\n summary: 'Restore a removed post (staff)',\n description: 'Staff only (owner, admin, instructor). 409 when the post is not removed.',\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.posts.restore(\n scope.orgId,\n req.params.postId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/flags/resolution',\n schema: {\n operationId: 'resolveCommunityPostFlags',\n tags,\n summary: 'Dismiss the open flags on a post (staff)',\n description:\n 'Staff only (owner, admin, instructor). Closes every open flag on the post and leaves it up.',\n params: CommunityPostParam,\n body: ResolvePostFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.posts.resolveFlags(\n scope.orgId,\n req.params.postId,\n { id: scope.orgUserId, role: scope.role },\n req.body.resolution,\n req.params.communityId,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/removal',\n schema: {\n operationId: 'removeCommunityComment',\n tags,\n summary: 'Remove a comment on a post (staff)',\n description:\n 'Staff only (owner, admin, instructor). Hides the comment and resolves its open flags as `removed`. Removing a removed comment returns it unchanged.',\n params: CommunityCommentParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.discussion.remove(\n scope.orgId,\n req.params.commentId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/restoration',\n schema: {\n operationId: 'restoreCommunityComment',\n tags,\n summary: 'Restore a removed comment on a post (staff)',\n description:\n 'Staff only (owner, admin, instructor). Publishes the comment again. Restoring a comment that is not removed returns it unchanged.',\n params: CommunityCommentParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.discussion.restore(\n scope.orgId,\n req.params.commentId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/flags/resolution',\n schema: {\n operationId: 'resolveCommunityCommentFlags',\n tags,\n summary: 'Dismiss the open flags on a comment (staff)',\n description:\n 'Staff only (owner, admin, instructor). Closes every open flag on the comment and leaves it up.',\n params: CommunityCommentParam,\n body: ResolveCommentFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.discussion.resolveReports(\n scope.orgId,\n req.params.commentId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/messages/:messageId/flags/resolution',\n schema: {\n operationId: 'resolveCommunityMessageFlags',\n tags,\n summary: 'Dismiss the open flags on a message (staff)',\n description:\n 'Staff only (owner, admin, instructor). Closes every open flag on the message and leaves it up.',\n params: CommunityMessageParam,\n body: ResolveMessageFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.chats.resolveMessageFlags(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.messageId,\n req.body.resolution,\n req.params.communityId,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/chats/:chatId/lock',\n schema: {\n operationId: 'lockCommunityChat',\n tags,\n summary: 'Lock a chat (staff)',\n description:\n 'Staff only (owner, admin, instructor). Nobody can send or edit messages until it is unlocked; its open message flags are resolved as `locked`. Locking a locked chat returns it unchanged.',\n params: CommunityChatParam,\n response: { 200: Chat, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.chats.lock(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.chatId,\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/chats/:chatId/unlock',\n schema: {\n operationId: 'unlockCommunityChat',\n tags,\n summary: 'Unlock a chat (staff)',\n description:\n 'Staff only (owner, admin, instructor). Lets participants send messages again. Unlocking an unlocked chat returns it unchanged.',\n params: CommunityChatParam,\n response: { 200: Chat, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.chats.unlock(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.chatId,\n req.params.communityId,\n );\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"community-moderation.js","names":[],"sources":["../../../src/http/routes/community-moderation.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\nimport {\n Chat,\n CommentView,\n CommunityChatParam,\n CommunityCommentParam,\n CommunityIdParam,\n CommunityMembersPage,\n CommunityMembersQuery,\n CommunityMessageParam,\n CommunityPostParam,\n CommunitySpaceParam,\n OrgUserBody,\n PageQuery,\n PostView,\n ResolveCommentFlags,\n ResolveMessageFlags,\n ResolvePostFlags,\n SpaceMember,\n SpaceMemberParam,\n SpaceMembersPage,\n SpaceQueue,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communityModerationRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/members',\n schema: {\n operationId: 'listCommunityMembers',\n tags,\n summary: 'List the people who hold a community',\n description: `Requires \\`access:read\\`. Everyone with active access to the community, granted directly or through a bundle, paged and searched by name or email.`,\n params: CommunityIdParam,\n querystring: CommunityMembersQuery,\n response: { 200: CommunityMembersPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.communityMembers(req.params.communityId, req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId/members',\n schema: {\n operationId: 'listCommunitySpaceMembers',\n tags,\n summary: 'List the members, requests and invitations of a space',\n description: `For a person who can open the space. Its members; a holder of \\`communities:moderate\\` on the community also sees its open requests to join and open invitations. People who open every space of the community through their role are not listed. 404 when you cannot open the space.`,\n params: CommunitySpaceParam,\n querystring: PageQuery,\n response: { 200: SpaceMembersPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.entitlements.listSpaceMembers(\n req.params.communityId,\n req.params.spaceId,\n req.query,\n actor,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members',\n schema: {\n operationId: 'addCommunitySpaceMember',\n tags,\n summary: 'Add a person to a space',\n description: `Requires \\`communities:moderate\\`. Places a member of the community straight into the space, with no request or invitation. 409 when the space is open to all members, the person is already placed, or the space is full.`,\n params: CommunitySpaceParam,\n body: OrgUserBody,\n response: { 201: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const member = await container.entitlements.addSpaceMember(\n req.params.communityId,\n req.params.spaceId,\n req.body.orgUserId,\n actor,\n );\n return reply.code(201).send(member);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/invites',\n schema: {\n operationId: 'inviteCommunitySpaceMember',\n tags,\n summary: 'Invite a person into a space',\n description: `Requires \\`communities:moderate\\`. Invites a member of the community into a \\`by_invite\\` space; they join when they accept. 403 when the person opens every space or is not a member of the community; 409 when the space is not invite-only or the person is already placed.`,\n params: CommunitySpaceParam,\n body: OrgUserBody,\n response: { 201: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const member = await container.entitlements.invite(\n req.params.communityId,\n req.params.spaceId,\n req.body.orgUserId,\n actor,\n );\n return reply.code(201).send(member);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/approve',\n schema: {\n operationId: 'approveCommunitySpaceRequest',\n tags,\n summary: \"Approve a person's request to join a space\",\n description: `Requires \\`communities:moderate\\`. Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.`,\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.entitlements.approve(\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n actor,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/decline',\n schema: {\n operationId: 'declineCommunitySpaceRequest',\n tags,\n summary: \"Decline a person's request to join a space\",\n description: `Requires \\`communities:moderate\\`. Closes the open request. 404 when the person has no open request; 409 when they are already a member.`,\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.entitlements.decline(\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n actor,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/members/:orgUserId/remove',\n schema: {\n operationId: 'removeCommunitySpaceMember',\n tags,\n summary: 'Remove a person from a space',\n description: `Requires \\`communities:moderate\\`. Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.`,\n params: SpaceMemberParam,\n response: { 200: SpaceMember, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.entitlements.remove(\n req.params.communityId,\n req.params.spaceId,\n req.params.orgUserId,\n actor,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId/queue',\n schema: {\n operationId: 'getCommunitySpaceQueue',\n tags,\n summary: 'The moderation queue of a space',\n description: `Requires \\`communities:moderate\\`. Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.`,\n params: CommunitySpaceParam,\n response: { 200: SpaceQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.posts.moderationQueue(req.params.communityId, req.params.spaceId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/approve',\n schema: {\n operationId: 'approveCommunityPost',\n tags,\n summary: 'Publish a post that is awaiting review',\n description: `Requires \\`communities:moderate\\`. 409 when the post is not awaiting review.`,\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.posts.approve(req.params.postId, req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/remove',\n schema: {\n operationId: 'removeCommunityPost',\n tags,\n summary: 'Remove a post',\n description: `The author can remove their own post; anyone else needs \\`communities:moderate\\` on the community. Takes a published or pending post down; removing someone else's post resolves its open flags as \\`removed\\`. 409 when it is already removed or was never submitted.`,\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.posts.remove(req.params.postId, req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/restore',\n schema: {\n operationId: 'restoreCommunityPost',\n tags,\n summary: 'Restore a removed post',\n description: `Requires \\`communities:moderate\\`. 409 when the post is not removed.`,\n params: CommunityPostParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.posts.restore(req.params.postId, req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/posts/:postId/flags/resolve',\n schema: {\n operationId: 'resolveCommunityPostFlags',\n tags,\n summary: 'Dismiss the open flags on a post',\n description: `Requires \\`communities:moderate\\`. Closes every open flag on the post and leaves it up.`,\n params: CommunityPostParam,\n body: ResolvePostFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.posts.resolveFlags(\n req.params.postId,\n req.body.resolution,\n req.params.communityId,\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/remove',\n schema: {\n operationId: 'removeCommunityComment',\n tags,\n summary: 'Remove a comment on a post',\n description: `The author can remove their own comment; anyone else needs \\`communities:moderate\\` on the community. Hides the comment; removing someone else's comment resolves its open flags as \\`removed\\`. Removing a removed comment returns it unchanged.`,\n params: CommunityCommentParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.discussion.remove(\n req.params.commentId,\n { communityId: req.params.communityId },\n actor,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/restore',\n schema: {\n operationId: 'restoreCommunityComment',\n tags,\n summary: 'Restore a removed comment on a post',\n description: `Requires one of \\`courses:moderate\\`, \\`communities:moderate\\`. Publishes the comment again. Restoring a comment that is not removed returns it unchanged.`,\n params: CommunityCommentParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.discussion.restore(\n req.params.commentId,\n { communityId: req.params.communityId },\n actor,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/comments/:commentId/flags/resolve',\n schema: {\n operationId: 'resolveCommunityCommentFlags',\n tags,\n summary: 'Dismiss the open flags on a comment',\n description: `Requires one of \\`courses:moderate\\`, \\`communities:moderate\\`. Closes every open flag on the comment and leaves it up.`,\n params: CommunityCommentParam,\n body: ResolveCommentFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.discussion.resolveReports(\n req.params.commentId,\n { communityId: req.params.communityId },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/messages/:messageId/flags/resolve',\n schema: {\n operationId: 'resolveCommunityMessageFlags',\n tags,\n summary: 'Dismiss the open flags on a message',\n description: `Requires \\`communities:moderate\\`. Closes every open flag on the message and leaves it up.`,\n params: CommunityMessageParam,\n body: ResolveMessageFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.chats.resolveMessageFlags(\n req.params.messageId,\n req.body.resolution,\n req.params.communityId,\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/chats/:chatId/lock',\n schema: {\n operationId: 'lockCommunityChat',\n tags,\n summary: 'Lock a chat',\n description: `Requires \\`communities:moderate\\`. Nobody can send or edit messages until it is unlocked; its open message flags are resolved as \\`locked\\`. Locking a locked chat returns it unchanged.`,\n params: CommunityChatParam,\n response: { 200: Chat, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.chats.lock(req.params.chatId, req.params.communityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/chats/:chatId/unlock',\n schema: {\n operationId: 'unlockCommunityChat',\n tags,\n summary: 'Unlock a chat',\n description: `Requires \\`communities:moderate\\`. Lets participants send messages again. Unlocking an unlocked chat returns it unchanged.`,\n params: CommunityChatParam,\n response: { 200: Chat, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.chats.unlock(req.params.chatId, req.params.communityId, actor);\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { CourseIdParam } from "../schemas/content.js";
|
|
4
|
+
import { CourseIdPathParam } from "../schemas/activities.js";
|
|
4
5
|
import { EnrollmentSeries, EnrollmentSeriesQuery } from "../schemas/dashboard.js";
|
|
5
|
-
import { CourseAnalytics } from "../schemas/course-analytics.js";
|
|
6
|
+
import { CourseAnalytics, CourseLearnersPage, CourseLearnersQuery } from "../schemas/course-analytics.js";
|
|
6
7
|
import "../schemas/index.js";
|
|
7
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
8
8
|
//#region src/http/routes/course-analytics.ts
|
|
9
9
|
async function courseAnalyticsRoutes(app, container) {
|
|
10
10
|
const r = app.withTypeProvider();
|
|
@@ -15,8 +15,8 @@ async function courseAnalyticsRoutes(app, container) {
|
|
|
15
15
|
schema: {
|
|
16
16
|
operationId: "getCourseAnalytics",
|
|
17
17
|
tags: ["Reporting"],
|
|
18
|
-
summary: "Completion and engagement stats for a course
|
|
19
|
-
description: "
|
|
18
|
+
summary: "Completion and engagement stats for a course",
|
|
19
|
+
description: "Requires `progress:read`.",
|
|
20
20
|
params: CourseIdParam,
|
|
21
21
|
response: {
|
|
22
22
|
200: CourseAnalytics,
|
|
@@ -24,10 +24,8 @@ async function courseAnalyticsRoutes(app, container) {
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
handler: async (req) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (!analytics) throw new NotFoundError("Course", req.params.id);
|
|
30
|
-
return analytics;
|
|
27
|
+
const actor = await resolveActor(container, req);
|
|
28
|
+
return container.reporting.admin.courseAnalytics(req.params.id, actor);
|
|
31
29
|
}
|
|
32
30
|
});
|
|
33
31
|
r.route({
|
|
@@ -37,8 +35,8 @@ async function courseAnalyticsRoutes(app, container) {
|
|
|
37
35
|
schema: {
|
|
38
36
|
operationId: "getCourseEnrollmentSeries",
|
|
39
37
|
tags: ["Reporting"],
|
|
40
|
-
summary: "Enrollments granted per day for a course over a trailing window
|
|
41
|
-
description: "
|
|
38
|
+
summary: "Enrollments granted per day for a course over a trailing window",
|
|
39
|
+
description: "Requires `progress:read`.",
|
|
42
40
|
params: CourseIdParam,
|
|
43
41
|
querystring: EnrollmentSeriesQuery,
|
|
44
42
|
response: {
|
|
@@ -47,10 +45,29 @@ async function courseAnalyticsRoutes(app, container) {
|
|
|
47
45
|
}
|
|
48
46
|
},
|
|
49
47
|
handler: async (req) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const actor = await resolveActor(container, req);
|
|
49
|
+
return container.reporting.admin.courseEnrollments(req.params.id, req.query.days, actor);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
r.route({
|
|
53
|
+
method: "GET",
|
|
54
|
+
url: "/api/courses/:courseId/learners",
|
|
55
|
+
preHandler: app.requireOrgSession,
|
|
56
|
+
schema: {
|
|
57
|
+
operationId: "listCourseLearners",
|
|
58
|
+
tags: ["Reporting"],
|
|
59
|
+
summary: "List a course's learners with their access and progress",
|
|
60
|
+
description: `Requires \`progress:read\`. Everyone with a grant that opens the course, directly or through a bundle, active or not, each with that grant and their progress. Paged and searched by name or email.`,
|
|
61
|
+
params: CourseIdPathParam,
|
|
62
|
+
querystring: CourseLearnersQuery,
|
|
63
|
+
response: {
|
|
64
|
+
200: CourseLearnersPage,
|
|
65
|
+
...refusals(403, 404)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
handler: async (req) => {
|
|
69
|
+
const actor = await resolveActor(container, req);
|
|
70
|
+
return container.reporting.admin.courseLearners(req.params.courseId, req.query, actor);
|
|
54
71
|
}
|
|
55
72
|
});
|
|
56
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"course-analytics.js","names":[],"sources":["../../../src/http/routes/course-analytics.ts"],"sourcesContent":["// HTTP routes for per-course analytics (reporting).\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n CourseAnalytics,\n CourseIdParam,\n EnrollmentSeries,\n EnrollmentSeriesQuery,\n refusals,\n} from '../schemas/index.js';\nimport
|
|
1
|
+
{"version":3,"file":"course-analytics.js","names":[],"sources":["../../../src/http/routes/course-analytics.ts"],"sourcesContent":["// HTTP routes for per-course analytics (reporting).\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n CourseAnalytics,\n CourseIdParam,\n CourseIdPathParam,\n CourseLearnersPage,\n CourseLearnersQuery,\n EnrollmentSeries,\n EnrollmentSeriesQuery,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function courseAnalyticsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/courses/:id/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourseAnalytics',\n tags: ['Reporting'],\n summary: 'Completion and engagement stats for a course',\n description: 'Requires `progress:read`.',\n params: CourseIdParam,\n response: { 200: CourseAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.courseAnalytics(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/courses/:id/analytics/enrollments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourseEnrollmentSeries',\n tags: ['Reporting'],\n summary: 'Enrollments granted per day for a course over a trailing window',\n description: 'Requires `progress:read`.',\n params: CourseIdParam,\n querystring: EnrollmentSeriesQuery,\n response: { 200: EnrollmentSeries, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.courseEnrollments(req.params.id, req.query.days, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/courses/:courseId/learners',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listCourseLearners',\n tags: ['Reporting'],\n summary: \"List a course's learners with their access and progress\",\n description: `Requires \\`progress:read\\`. Everyone with a grant that opens the course, directly or through a bundle, active or not, each with that grant and their progress. Paged and searched by name or email.`,\n params: CourseIdPathParam,\n querystring: CourseLearnersQuery,\n response: { 200: CourseLearnersPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.courseLearners(req.params.courseId, req.query, actor);\n },\n });\n}\n"],"mappings":";;;;;;;;AAgBA,eAAsB,sBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAEhD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAiB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC1D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,gBAAgB,IAAI,OAAO,IAAI,KAAK;EACvE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,aAAa;GACb,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,kBAAkB,IAAI,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK;EACzF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,aAAa;GACb,UAAU;IAAE,KAAK;IAAoB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,eAAe,IAAI,OAAO,UAAU,IAAI,OAAO,KAAK;EACvF;CACF,CAAC;AACH"}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { Course, CourseIdParam, CourseSettings, CoursesPage, CoursesQuery, CreateCourse, PatchCourseSettings, UpdateCourse } from "../schemas/content.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
6
|
//#region src/http/routes/courses.ts
|
|
8
7
|
async function coursesRoutes(app, container) {
|
|
9
8
|
const r = app.withTypeProvider();
|
|
@@ -15,8 +14,8 @@ async function coursesRoutes(app, container) {
|
|
|
15
14
|
schema: {
|
|
16
15
|
operationId: "listCourses",
|
|
17
16
|
tags: ["Content"],
|
|
18
|
-
summary: "List courses
|
|
19
|
-
description:
|
|
17
|
+
summary: "List courses",
|
|
18
|
+
description: `Requires \`courses:read\`. Every course in the organization, drafts included, filtered by status and category and searched by title.`,
|
|
20
19
|
querystring: CoursesQuery,
|
|
21
20
|
response: {
|
|
22
21
|
200: CoursesPage,
|
|
@@ -24,8 +23,8 @@ async function coursesRoutes(app, container) {
|
|
|
24
23
|
}
|
|
25
24
|
},
|
|
26
25
|
handler: async (req) => {
|
|
27
|
-
const
|
|
28
|
-
return courses.listCourses(
|
|
26
|
+
const actor = await resolveActor(container, req);
|
|
27
|
+
return courses.listCourses(req.query, actor);
|
|
29
28
|
}
|
|
30
29
|
});
|
|
31
30
|
r.route({
|
|
@@ -35,8 +34,8 @@ async function coursesRoutes(app, container) {
|
|
|
35
34
|
schema: {
|
|
36
35
|
operationId: "getCourse",
|
|
37
36
|
tags: ["Content"],
|
|
38
|
-
summary: "Get a course
|
|
39
|
-
description: "
|
|
37
|
+
summary: "Get a course",
|
|
38
|
+
description: "Requires `courses:read`.",
|
|
40
39
|
params: CourseIdParam,
|
|
41
40
|
response: {
|
|
42
41
|
200: Course,
|
|
@@ -44,10 +43,8 @@ async function coursesRoutes(app, container) {
|
|
|
44
43
|
}
|
|
45
44
|
},
|
|
46
45
|
handler: async (req) => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (!course) throw new NotFoundError("Course", req.params.id);
|
|
50
|
-
return course;
|
|
46
|
+
const actor = await resolveActor(container, req);
|
|
47
|
+
return courses.getCourse(req.params.id, actor);
|
|
51
48
|
}
|
|
52
49
|
});
|
|
53
50
|
r.route({
|
|
@@ -57,8 +54,8 @@ async function coursesRoutes(app, container) {
|
|
|
57
54
|
schema: {
|
|
58
55
|
operationId: "createCourse",
|
|
59
56
|
tags: ["Content"],
|
|
60
|
-
summary: "Create a course
|
|
61
|
-
description:
|
|
57
|
+
summary: "Create a course",
|
|
58
|
+
description: `Requires \`courses:write\`. The course starts as a draft. 409 when its slug is taken.`,
|
|
62
59
|
body: CreateCourse,
|
|
63
60
|
response: {
|
|
64
61
|
201: Course,
|
|
@@ -66,8 +63,8 @@ async function coursesRoutes(app, container) {
|
|
|
66
63
|
}
|
|
67
64
|
},
|
|
68
65
|
handler: async (req, reply) => {
|
|
69
|
-
const
|
|
70
|
-
const course = await courses.createCourse(
|
|
66
|
+
const actor = await resolveActor(container, req);
|
|
67
|
+
const course = await courses.createCourse(req.body, actor);
|
|
71
68
|
return reply.code(201).send(course);
|
|
72
69
|
}
|
|
73
70
|
});
|
|
@@ -78,8 +75,8 @@ async function coursesRoutes(app, container) {
|
|
|
78
75
|
schema: {
|
|
79
76
|
operationId: "updateCourse",
|
|
80
77
|
tags: ["Content"],
|
|
81
|
-
summary: "Update a course
|
|
82
|
-
description:
|
|
78
|
+
summary: "Update a course",
|
|
79
|
+
description: `Requires \`courses:write\`. Changes only the fields sent. \`thumbnailAssetId\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \`media:read\` (403 otherwise).`,
|
|
83
80
|
params: CourseIdParam,
|
|
84
81
|
body: UpdateCourse,
|
|
85
82
|
response: {
|
|
@@ -88,8 +85,8 @@ async function coursesRoutes(app, container) {
|
|
|
88
85
|
}
|
|
89
86
|
},
|
|
90
87
|
handler: async (req) => {
|
|
91
|
-
const
|
|
92
|
-
return courses.updateCourse(
|
|
88
|
+
const actor = await resolveActor(container, req);
|
|
89
|
+
return courses.updateCourse(req.params.id, req.body, actor);
|
|
93
90
|
}
|
|
94
91
|
});
|
|
95
92
|
r.route({
|
|
@@ -99,8 +96,8 @@ async function coursesRoutes(app, container) {
|
|
|
99
96
|
schema: {
|
|
100
97
|
operationId: "updateCourseSettings",
|
|
101
98
|
tags: ["Content"],
|
|
102
|
-
summary: "Update a course's settings
|
|
103
|
-
description:
|
|
99
|
+
summary: "Update a course's settings",
|
|
100
|
+
description: `Requires \`courses:write\`. Merges the settings sent into the current ones.`,
|
|
104
101
|
params: CourseIdParam,
|
|
105
102
|
body: PatchCourseSettings,
|
|
106
103
|
response: {
|
|
@@ -109,9 +106,8 @@ async function coursesRoutes(app, container) {
|
|
|
109
106
|
}
|
|
110
107
|
},
|
|
111
108
|
handler: async (req) => {
|
|
112
|
-
const
|
|
113
|
-
req.
|
|
114
|
-
return courses.patchCourseSettings(scope.orgId, req.params.id, req.body);
|
|
109
|
+
const actor = await resolveActor(container, req);
|
|
110
|
+
return courses.patchCourseSettings(req.params.id, req.body, actor);
|
|
115
111
|
}
|
|
116
112
|
});
|
|
117
113
|
r.route({
|
|
@@ -121,8 +117,8 @@ async function coursesRoutes(app, container) {
|
|
|
121
117
|
schema: {
|
|
122
118
|
operationId: "deleteCourse",
|
|
123
119
|
tags: ["Content"],
|
|
124
|
-
summary: "Delete a course
|
|
125
|
-
description: "
|
|
120
|
+
summary: "Delete a course",
|
|
121
|
+
description: "Requires `courses:delete`.",
|
|
126
122
|
params: CourseIdParam,
|
|
127
123
|
response: {
|
|
128
124
|
204: z.void(),
|
|
@@ -130,11 +126,8 @@ async function coursesRoutes(app, container) {
|
|
|
130
126
|
}
|
|
131
127
|
},
|
|
132
128
|
handler: async (req, reply) => {
|
|
133
|
-
const
|
|
134
|
-
await courses.deleteCourse(
|
|
135
|
-
id: scope.orgUserId,
|
|
136
|
-
role: scope.role
|
|
137
|
-
});
|
|
129
|
+
const actor = await resolveActor(container, req);
|
|
130
|
+
await courses.deleteCourse(req.params.id, actor);
|
|
138
131
|
return reply.code(204).send();
|
|
139
132
|
}
|
|
140
133
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"courses.js","names":[],"sources":["../../../src/http/routes/courses.ts"],"sourcesContent":["// HTTP routes for the courses context. Request + response are validated against\n// the shared contract schemas by the Zod type provider, and @fastify/swagger\n// reads the same schemas to build the OpenAPI spec the SDK is generated from.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Course,\n CourseIdParam,\n CourseSettings,\n CoursesPage,\n CoursesQuery,\n CreateCourse,\n PatchCourseSettings,\n UpdateCourse,\n refusals,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport {
|
|
1
|
+
{"version":3,"file":"courses.js","names":[],"sources":["../../../src/http/routes/courses.ts"],"sourcesContent":["// HTTP routes for the courses context. Request + response are validated against\n// the shared contract schemas by the Zod type provider, and @fastify/swagger\n// reads the same schemas to build the OpenAPI spec the SDK is generated from.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Course,\n CourseIdParam,\n CourseSettings,\n CoursesPage,\n CoursesQuery,\n CreateCourse,\n PatchCourseSettings,\n UpdateCourse,\n refusals,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function coursesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const courses = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listCourses',\n tags: ['Content'],\n summary: 'List courses',\n description: `Requires \\`courses:read\\`. Every course in the organization, drafts included, filtered by status and category and searched by title.`,\n querystring: CoursesQuery,\n response: { 200: CoursesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return courses.listCourses(req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourse',\n tags: ['Content'],\n summary: 'Get a course',\n description: 'Requires `courses:read`.',\n params: CourseIdParam,\n response: { 200: Course, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return courses.getCourse(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createCourse',\n tags: ['Content'],\n summary: 'Create a course',\n description: `Requires \\`courses:write\\`. The course starts as a draft. 409 when its slug is taken.`,\n body: CreateCourse,\n response: { 201: Course, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const course = await courses.createCourse(req.body, actor);\n return reply.code(201).send(course);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourse',\n tags: ['Content'],\n summary: 'Update a course',\n description: `Requires \\`courses:write\\`. Changes only the fields sent. \\`thumbnailAssetId\\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise).`,\n params: CourseIdParam,\n body: UpdateCourse,\n response: { 200: Course, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return courses.updateCourse(req.params.id, req.body, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id/settings',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourseSettings',\n tags: ['Content'],\n summary: \"Update a course's settings\",\n description: `Requires \\`courses:write\\`. Merges the settings sent into the current ones.`,\n params: CourseIdParam,\n body: PatchCourseSettings,\n response: { 200: CourseSettings, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return courses.patchCourseSettings(req.params.id, req.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteCourse',\n tags: ['Content'],\n summary: 'Delete a course',\n description: 'Requires `courses:delete`.',\n params: CourseIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await courses.deleteCourse(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;AAoBA,eAAsB,cAAc,KAAsB,WAAqC;CAC7F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,UAAU,UAAU;CAE1B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,GAAG;GAAE;EACjD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,IAAI,OAAO,KAAK;EAC7C;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAQ,GAAG,SAAS,KAAK,GAAG;GAAE;EACjD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,UAAU,IAAI,OAAO,IAAI,KAAK;EAC/C;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAQ,GAAG,SAAS,KAAK,GAAG;GAAE;EACjD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,SAAS,MAAM,QAAQ,aAAa,IAAI,MAAM,KAAK;GACzD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,MAAM;EACpC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAQ,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,aAAa,IAAI,OAAO,IAAI,IAAI,MAAM,KAAK;EAC5D;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,oBAAoB,IAAI,OAAO,IAAI,IAAI,MAAM,KAAK;EACnE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,aAAa,IAAI,OAAO,IAAI,KAAK;GAC/C,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { EnrollmentSeries, EnrollmentSeriesQuery, OverviewStats } from "../schemas/dashboard.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
//#region src/http/routes/dashboard.ts
|
|
@@ -12,16 +12,16 @@ async function dashboardRoutes(app, container) {
|
|
|
12
12
|
schema: {
|
|
13
13
|
operationId: "getOverview",
|
|
14
14
|
tags: ["Reporting"],
|
|
15
|
-
summary: "Back-office overview stats
|
|
16
|
-
description: "
|
|
15
|
+
summary: "Back-office overview stats",
|
|
16
|
+
description: "Requires `analytics:read`.",
|
|
17
17
|
response: {
|
|
18
18
|
200: OverviewStats,
|
|
19
19
|
...refusals(403)
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
handler: async (req) => {
|
|
23
|
-
const
|
|
24
|
-
return container.reporting.admin.overview(
|
|
23
|
+
const actor = await resolveActor(container, req);
|
|
24
|
+
return container.reporting.admin.overview(actor);
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
r.route({
|
|
@@ -31,8 +31,8 @@ async function dashboardRoutes(app, container) {
|
|
|
31
31
|
schema: {
|
|
32
32
|
operationId: "getEnrollmentSeries",
|
|
33
33
|
tags: ["Reporting"],
|
|
34
|
-
summary: "Enrollments granted per day over a trailing window
|
|
35
|
-
description: "
|
|
34
|
+
summary: "Enrollments granted per day over a trailing window",
|
|
35
|
+
description: "Requires `analytics:read`.",
|
|
36
36
|
querystring: EnrollmentSeriesQuery,
|
|
37
37
|
response: {
|
|
38
38
|
200: EnrollmentSeries,
|
|
@@ -40,8 +40,8 @@ async function dashboardRoutes(app, container) {
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
handler: async (req) => {
|
|
43
|
-
const
|
|
44
|
-
return container.reporting.admin.enrollments(
|
|
43
|
+
const actor = await resolveActor(container, req);
|
|
44
|
+
return container.reporting.admin.enrollments(req.query.days, actor);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.js","names":[],"sources":["../../../src/http/routes/dashboard.ts"],"sourcesContent":["// HTTP routes for the dashboard (overview) context.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n EnrollmentSeries,\n EnrollmentSeriesQuery,\n OverviewStats,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport {
|
|
1
|
+
{"version":3,"file":"dashboard.js","names":[],"sources":["../../../src/http/routes/dashboard.ts"],"sourcesContent":["// HTTP routes for the dashboard (overview) context.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n EnrollmentSeries,\n EnrollmentSeriesQuery,\n OverviewStats,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function dashboardRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/overview',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getOverview',\n tags: ['Reporting'],\n summary: 'Back-office overview stats',\n description: 'Requires `analytics:read`.',\n response: { 200: OverviewStats, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.overview(actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/overview/enrollments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEnrollmentSeries',\n tags: ['Reporting'],\n summary: 'Enrollments granted per day over a trailing window',\n description: 'Requires `analytics:read`.',\n querystring: EnrollmentSeriesQuery,\n response: { 200: EnrollmentSeries, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.enrollments(req.query.days, actor);\n },\n });\n}\n"],"mappings":";;;;;AAYA,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,WAAW;GAClB,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAe,GAAG,SAAS,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,SAAS,KAAK;EACjD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,YAAY,IAAI,MAAM,MAAM,KAAK;EACpE;CACF,CAAC;AACH"}
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { Comment, CommentIdParam, CommentView, CommentsPage, CommentsQuery, PatchComment, ReplyToComment } from "../schemas/discussion.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
6
|
//#region src/http/routes/discussion.ts
|
|
8
7
|
async function discussionRoutes(app, container) {
|
|
9
8
|
const r = app.withTypeProvider();
|
|
10
9
|
const discussion = container.discussion;
|
|
11
|
-
const staff = async (req) => {
|
|
12
|
-
await resolveScope(container, req);
|
|
13
|
-
};
|
|
14
10
|
r.route({
|
|
15
11
|
method: "GET",
|
|
16
12
|
url: "/api/comments",
|
|
17
|
-
preHandler: staff,
|
|
18
13
|
schema: {
|
|
19
14
|
operationId: "listComments",
|
|
20
15
|
tags: ["Discussion"],
|
|
21
|
-
summary: "List course comments for moderation
|
|
22
|
-
description:
|
|
16
|
+
summary: "List course comments for moderation",
|
|
17
|
+
description: `Requires \`courses:moderate\`. Comments on the activities of the courses you moderate, filtered by course, activity, author, status or open reports; each row carries the author's email and the open reports.`,
|
|
23
18
|
querystring: CommentsQuery,
|
|
24
19
|
response: {
|
|
25
20
|
200: CommentsPage,
|
|
@@ -27,18 +22,18 @@ async function discussionRoutes(app, container) {
|
|
|
27
22
|
}
|
|
28
23
|
},
|
|
29
24
|
handler: async (req) => {
|
|
30
|
-
|
|
25
|
+
const actor = await resolveActor(container, req);
|
|
26
|
+
return discussion.listComments(req.query, actor);
|
|
31
27
|
}
|
|
32
28
|
});
|
|
33
29
|
r.route({
|
|
34
30
|
method: "DELETE",
|
|
35
31
|
url: "/api/comments/:commentId",
|
|
36
|
-
preHandler: staff,
|
|
37
32
|
schema: {
|
|
38
33
|
operationId: "moderateRemoveComment",
|
|
39
34
|
tags: ["Discussion"],
|
|
40
|
-
summary: "Remove a course comment
|
|
41
|
-
description: "
|
|
35
|
+
summary: "Remove a course comment",
|
|
36
|
+
description: "Needs `courses:moderate` on the course, unless it is your own comment. Hides the comment and resolves its open reports as `removed`. Removing a removed comment returns it unchanged.",
|
|
42
37
|
params: CommentIdParam,
|
|
43
38
|
response: {
|
|
44
39
|
200: CommentView,
|
|
@@ -46,19 +41,18 @@ async function discussionRoutes(app, container) {
|
|
|
46
41
|
}
|
|
47
42
|
},
|
|
48
43
|
handler: async (req) => {
|
|
49
|
-
const
|
|
50
|
-
return discussion.remove(req.
|
|
44
|
+
const actor = await resolveActor(container, req);
|
|
45
|
+
return discussion.remove(req.params.commentId, "lesson", actor);
|
|
51
46
|
}
|
|
52
47
|
});
|
|
53
48
|
r.route({
|
|
54
49
|
method: "PATCH",
|
|
55
50
|
url: "/api/comments/:commentId",
|
|
56
|
-
preHandler: staff,
|
|
57
51
|
schema: {
|
|
58
52
|
operationId: "publishComment",
|
|
59
53
|
tags: ["Discussion"],
|
|
60
|
-
summary: "Publish a course comment
|
|
61
|
-
description: "
|
|
54
|
+
summary: "Publish a course comment",
|
|
55
|
+
description: "Needs `courses:moderate` on the course. Approves a pending comment or restores a removed one; a published comment is returned unchanged.",
|
|
62
56
|
params: CommentIdParam,
|
|
63
57
|
body: PatchComment,
|
|
64
58
|
response: {
|
|
@@ -67,20 +61,18 @@ async function discussionRoutes(app, container) {
|
|
|
67
61
|
}
|
|
68
62
|
},
|
|
69
63
|
handler: async (req) => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
return discussion.publish(req.orgId, req.params.commentId, orgUser, null);
|
|
64
|
+
const actor = await resolveActor(container, req);
|
|
65
|
+
return discussion.publish(req.params.commentId, "lesson", actor);
|
|
73
66
|
}
|
|
74
67
|
});
|
|
75
68
|
r.route({
|
|
76
69
|
method: "DELETE",
|
|
77
70
|
url: "/api/comments/:commentId/reports",
|
|
78
|
-
preHandler: staff,
|
|
79
71
|
schema: {
|
|
80
72
|
operationId: "dismissCommentReports",
|
|
81
73
|
tags: ["Discussion"],
|
|
82
|
-
summary: "Dismiss every open report on a course comment
|
|
83
|
-
description: "
|
|
74
|
+
summary: "Dismiss every open report on a course comment",
|
|
75
|
+
description: "Needs `courses:moderate` on the course. The comment stays up.",
|
|
84
76
|
params: CommentIdParam,
|
|
85
77
|
response: {
|
|
86
78
|
204: z.void(),
|
|
@@ -88,21 +80,19 @@ async function discussionRoutes(app, container) {
|
|
|
88
80
|
}
|
|
89
81
|
},
|
|
90
82
|
handler: async (req, reply) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
await discussion.resolveReports(req.orgId, req.params.commentId, orgUser, null);
|
|
83
|
+
const actor = await resolveActor(container, req);
|
|
84
|
+
await discussion.resolveReports(req.params.commentId, "lesson", actor);
|
|
94
85
|
return reply.code(204).send();
|
|
95
86
|
}
|
|
96
87
|
});
|
|
97
88
|
r.route({
|
|
98
89
|
method: "POST",
|
|
99
90
|
url: "/api/comments/:commentId/replies",
|
|
100
|
-
preHandler: staff,
|
|
101
91
|
schema: {
|
|
102
92
|
operationId: "replyToComment",
|
|
103
93
|
tags: ["Discussion"],
|
|
104
|
-
summary: "Reply to a course comment from the moderation queue
|
|
105
|
-
description:
|
|
94
|
+
summary: "Reply to a course comment from the moderation queue",
|
|
95
|
+
description: `Requires \`courses:moderate\`. Needed on the comment's course. Replies nest one level: answering a reply attaches to its root comment.`,
|
|
106
96
|
params: CommentIdParam,
|
|
107
97
|
body: ReplyToComment,
|
|
108
98
|
response: {
|
|
@@ -111,16 +101,8 @@ async function discussionRoutes(app, container) {
|
|
|
111
101
|
}
|
|
112
102
|
},
|
|
113
103
|
handler: async (req) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const parent = await discussion.getComment(req.orgId, req.params.commentId);
|
|
117
|
-
if (!parent || parent.activityId === null) throw new NotFoundError("Comment", req.params.commentId);
|
|
118
|
-
return discussion.postComment(req.orgId, {
|
|
119
|
-
actor: orgUser,
|
|
120
|
-
activityId: parent.activityId,
|
|
121
|
-
parentId: parent.parentId ?? parent.id,
|
|
122
|
-
body: req.body.body
|
|
123
|
-
});
|
|
104
|
+
const actor = await resolveActor(container, req);
|
|
105
|
+
return discussion.replyToComment(req.params.commentId, req.body.body, actor);
|
|
124
106
|
}
|
|
125
107
|
});
|
|
126
108
|
}
|