@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":"organizations.js","names":[],"sources":["../../../src/http/routes/organizations.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AcceptInvite,\n AcceptInviteResult,\n CreateInvite,\n CreateOrganization,\n Invite,\n MemberIdParam,\n MemberRoleChange,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n Organization,\n UpdateMemberRole,\n UpdateOrganization,\n refusals,\n} from '../schemas/index.js';\nimport type { MemberWriteContext } from '@headless-lms/core/organizations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function organizationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const organizations = container.organizations;\n const tags = ['Organizations'];\n\n // Create a new organization on the caller's behalf and make it their active\n // org. This is the API's own front door for org creation — it drives Better\n // Auth internally, so callers use the typed SDK, not the auth namespace. No\n // resolveScope here: the caller has no active org yet, only a session.\n r.route({\n method: 'POST',\n url: '/api/organizations',\n // Session only: a first-time signup has no active org yet, so requiring one\n // here would 401 the very call that produces it.\n preHandler: app.requireSession,\n schema: {\n operationId: 'createOrganization',\n tags,\n summary: 'Create an organization and make it active',\n description:\n 'For any signed-in person, including one with no organization yet. The caller becomes its owner.',\n body: CreateOrganization,\n response: { 201: Organization },\n },\n handler: async (req, reply) => {\n const org = await container.orgProvider.createOrganization(req.headers, req.body);\n return reply.code(201).send({\n name: org.name,\n slug: org.slug,\n id: org.id,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n // Update the caller's active organization (name/slug). Writes go through Better\n // Auth, which enforces the caller's org-update permission (owner/admin).\n r.route({\n method: 'PATCH',\n url: '/api/organizations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateOrganization',\n tags,\n summary: 'Update the active organization (staff)',\n description: 'Owner or admin. Changes its name or slug; 409 when the slug is taken.',\n body: UpdateOrganization,\n response: { 200: Organization, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const org = await container.orgProvider.updateOrganization(\n req.headers,\n scope.authOrgId,\n req.body,\n );\n return reply.send({\n id: org.id,\n name: org.name,\n slug: org.slug,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/members',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMembers',\n tags,\n summary: \"List the organization's team members (staff)\",\n description:\n 'Staff only (owner, admin, instructor). The team: everyone in the organization who is not a student.',\n querystring: MembersQuery,\n response: { 200: MembersPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return organizations.listMembers(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/organizations/members/:id/role',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateMemberRole',\n tags,\n summary: \"Change a member's role (staff)\",\n description:\n 'Owner or admin only. The owner role can be neither granted nor changed (409), and only a moderator can become a student.',\n params: MemberIdParam,\n body: UpdateMemberRole,\n response: { 200: MemberRoleChange, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const member = await organizations.updateMemberRole(\n ctx,\n { id: scope.orgUserId, role: scope.role },\n req.params.id,\n req.body.role,\n );\n if (!member) {\n throw new NotFoundError('Member', req.params.id);\n }\n return member;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/members/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeMember',\n tags,\n summary: 'Remove an organization member (staff)',\n description:\n 'Owner or admin only. Removing a pending member cancels their invite. The owner cannot be removed (409).',\n params: MemberIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const removed = await organizations.removeMember(\n ctx,\n { id: scope.orgUserId, role: scope.role },\n req.params.id,\n );\n if (!removed) {\n throw new NotFoundError('Member', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createInvite',\n tags,\n summary: 'Invite a member or student into the active organization (staff)',\n description:\n 'Owner or admin only. A student is created by this call, so their access can be granted before they accept. 409 when a student with that email is already in the organization.',\n body: CreateInvite,\n response: { 201: Invite, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n // Domain ids, not the session's auth ids: invites FK to organizations.id\n // and users.id.\n const scope = await resolveScope(container, req);\n const invite = await organizations.createInvite(\n { id: scope.orgUserId, role: scope.role },\n {\n orgId: scope.orgId,\n inviterUserId: scope.userId,\n email: req.body.email,\n role: req.body.role,\n ...(req.body.firstName !== undefined && { firstName: req.body.firstName }),\n ...(req.body.lastName !== undefined && { lastName: req.body.lastName }),\n sendEmail: req.body.sendEmail,\n },\n );\n\n req.log.debug({ inviteId: invite.id }, 'created invite');\n return reply.code(201).send({\n id: invite.id,\n email: invite.email,\n role: invite.role,\n status: invite.status,\n expiresAt: invite.expiresAt,\n createdAt: invite.createdAt,\n });\n },\n });\n\n // The invite landing page's first call, made before the invitee has any\n // session — so no guard. It turns the emailed token into the two facts the\n // page needs: who the invitation is for, and whether that address already has\n // an account. The email comes from the invitation row rather than the URL, so\n // it cannot be steered by editing the link.\n r.route({\n method: 'GET',\n url: '/api/organizations/invites/:token',\n schema: {\n operationId: 'getInvite',\n tags,\n summary: 'Read an invite by its token',\n description:\n 'Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.',\n params: InviteTokenParam,\n response: { 200: GetInviteResult, ...refusals(404) },\n },\n handler: async (req) => {\n const invite = await organizations.peekInvite(req.params.token);\n if (!invite) {\n // Same answer for forged, expired, and already-accepted tokens: a\n // probe learns nothing about which invitations exist.\n throw new NotFoundError('Invite', 'token');\n }\n // Whether the address already has a login is deliberately not answered\n // here. The mirror cannot say — an admin's student is a person long\n // before they are an account — and asking the auth engine would mean\n // reading its tables for one boolean. The sign-up form discovers it\n // instead: a duplicate signup comes back 422 and the page flips to\n // sign-in.\n const person = await container.identity.getUserByEmail(invite.email);\n return {\n email: invite.email,\n firstName: person?.firstName ?? null,\n lastName: person?.lastName ?? null,\n };\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites/accept',\n preHandler: app.requireSession,\n schema: {\n operationId: 'acceptInvite',\n tags,\n summary: 'Accept an invite with the signed-in account',\n description:\n \"For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired, or the email does not match.\",\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, ...refusals(400, 404) },\n },\n handler: async (req) => {\n // No active org yet, so no resolveScope — but acceptInvite links a domain\n // users.id, never the auth account id.\n const user = await container.identity.getUserByExternalId(req.authUser.id);\n if (!user) {\n throw new NotFoundError('User', req.authUser.id);\n }\n const accepted = await container.organizations.acceptInvite({\n token: req.body.token,\n userId: user.id,\n email: req.authUser.email,\n });\n\n // The session predates this grant, so it carries no active org and every\n // org-scoped read would 401. Students are never org members, so the org\n // plugin's set-active cannot do it — stamp the session row directly.\n const org = await container.organizations.getById(accepted.orgId);\n if (!org) {\n throw new NotFoundError('Organization', accepted.orgId);\n }\n const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);\n\n req.log.info({ accepted, stamped }, 'accepted invite');\n return {};\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"organizations.js","names":[],"sources":["../../../src/http/routes/organizations.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AcceptInvite,\n AcceptInviteResult,\n AssignmentList,\n ContentIdParam,\n CreateInvite,\n CreateOrganization,\n CreateRole,\n Invite,\n NotificationCategoryDefaults,\n NotificationCategoryList,\n NotificationChannelInfos,\n NotificationListScopeQuery,\n NotificationTypeInfos,\n ReplaceNotificationCategories,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n OrgRoleList,\n OrgRoleView,\n OrgUserContentParam,\n OrgUserIdParam,\n Organization,\n RoleKeyParam,\n SetRole,\n TransferOwnership,\n UpdateOrganization,\n UpdateRole,\n refusals,\n} from '../schemas/index.js';\nimport type { Invite as InviteRecord } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor, resolveCaller } from '../scope.js';\n\nfunction inviteView(invite: InviteRecord) {\n return {\n id: invite.id,\n email: invite.email,\n roleKey: invite.roleKey,\n status: invite.status,\n expiresAt: invite.expiresAt,\n createdAt: invite.createdAt,\n };\n}\n\nexport async function organizationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const organizations = container.organizations;\n const tags = ['Organizations'];\n const peopleTags = ['People'];\n const roleTags = ['Roles'];\n\n r.route({\n method: 'POST',\n url: '/api/organizations',\n preHandler: app.requireSession,\n schema: {\n operationId: 'createOrganization',\n tags,\n summary: 'Create an organization and make it active',\n description:\n 'For any signed-in person, including one with no organization yet. The caller becomes its owner.',\n body: CreateOrganization,\n response: { 201: Organization },\n },\n handler: async (req, reply) => {\n const org = await organizations.createOrganization(req.body, req.headers);\n return reply.code(201).send({\n name: org.name,\n slug: org.slug,\n id: org.id,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/organizations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateOrganization',\n tags,\n summary: 'Update the active organization',\n description: `Requires \\`organizations:write\\`. Changes its name or slug; 409 when the slug is taken.`,\n body: UpdateOrganization,\n response: { 200: Organization, ...refusals(403, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n const org = await organizations.updateOrganization(req.body, actor);\n return {\n id: org.id,\n name: org.name,\n slug: org.slug,\n createdAt: org.createdAt.toISOString(),\n };\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/transfer-ownership',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'transferOwnership',\n tags,\n summary: 'Transfer ownership of the organization',\n description:\n \"Owner only. The new owner must be an active member. The previous owner takes the named org role, Member by default. 403 for anyone else, including the owner's API keys.\",\n body: TransferOwnership,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await organizations.transferOwnership(req.body, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/roles',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listRoles',\n tags: roleTags,\n summary: \"The organization's roles\",\n description: `Requires \\`roles:read\\`. Each role with its levels, permissions and the number of people who hold it.`,\n response: { 200: OrgRoleList, ...refusals(403) },\n },\n handler: async (req) => organizations.listRoles(await resolveActor(container, req)),\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/roles/:key',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getRole',\n tags: roleTags,\n summary: 'Read a role',\n description: 'Requires `roles:read`.',\n params: RoleKeyParam,\n response: { 200: OrgRoleView, ...refusals(403, 404) },\n },\n handler: async (req) =>\n organizations.getRole(req.params.key, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/organizations/roles/:key',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateRole',\n tags: roleTags,\n summary: 'Rename or change a role',\n description: `Requires \\`roles:write\\`. A rename needs nothing else. Changing permissions, levels or the team flag needs every permission the role holds before and after the change (403 names the missing ones). Owner and Member can only be renamed (409). Names are unique in the organization (409).`,\n params: RoleKeyParam,\n body: UpdateRole,\n response: { 200: OrgRoleView, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) =>\n organizations.updateRole(req.params.key, req.body, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/roles',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createRole',\n tags: roleTags,\n summary: 'Add a role',\n description: `Requires \\`roles:write\\`. The role starts from a deployment role (\\`base\\`) and needs every permission it holds.`,\n body: CreateRole,\n response: { 201: OrgRoleView, ...refusals(400, 403, 409) },\n },\n handler: async (req, reply) =>\n reply\n .code(201)\n .send(await organizations.createRole(req.body, await resolveActor(container, req))),\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/roles/:key/customization',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resetRole',\n tags: roleTags,\n summary: \"Reset a role to the deployment's version\",\n description: `Requires \\`roles:write\\`. Needs every permission the role holds before and after.`,\n params: RoleKeyParam,\n response: { 200: OrgRoleView, ...refusals(403, 404, 409) },\n },\n handler: async (req) =>\n organizations.resetRole(req.params.key, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/roles/:key',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteRole',\n tags: roleTags,\n summary: 'Delete a role the organization added',\n description: `Requires \\`roles:delete\\`. Refused while anyone holds it (409, with the count).`,\n params: RoleKeyParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n await organizations.deleteRole(req.params.key, await resolveActor(container, req));\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/people',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listTeam',\n tags: peopleTags,\n summary: \"The organization's team\",\n description: `Requires \\`people:read\\`. Everyone with an org role other than Member or a role on a content item, invited people included.`,\n querystring: MembersQuery,\n response: { 200: MembersPage, ...refusals(403) },\n },\n handler: async (req) => organizations.listTeam(req.query, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/people/:orgUserId/roles',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listPersonRoles',\n tags: peopleTags,\n summary: \"A person's roles\",\n description: `Requires \\`people:read\\`. Their org role and every role they hold on a content item.`,\n params: OrgUserIdParam,\n response: { 200: AssignmentList, ...refusals(403) },\n },\n handler: async (req) =>\n organizations.listAssignments(req.params.orgUserId, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'PUT',\n url: '/api/organizations/people/:orgUserId/roles/org',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setOrgRole',\n tags: peopleTags,\n summary: \"Set a person's org role\",\n description: `Requires \\`people:write\\`. Needs every permission the old and the new role hold. Nobody changes their own roles, and the owner's role changes only by transfer.`,\n params: OrgUserIdParam,\n body: SetRole,\n response: { 200: AssignmentList, ...refusals(403, 404, 409) },\n },\n handler: async (req) =>\n organizations.setOrgRole(\n req.params.orgUserId,\n req.body.roleKey,\n await resolveActor(container, req),\n ),\n });\n\n r.route({\n method: 'PUT',\n url: '/api/organizations/people/:orgUserId/roles/content/:contentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'assignRole',\n tags: peopleTags,\n summary: 'Give a person a role on a content item',\n description: `Requires \\`people:write\\`. The role must list the item's type as a level. Needs every permission the old and the new role give there.`,\n params: OrgUserContentParam,\n body: SetRole,\n response: { 200: AssignmentList, ...refusals(403, 404, 409) },\n },\n handler: async (req) =>\n organizations.assignRole(\n req.params.orgUserId,\n req.params.contentId,\n req.body.roleKey,\n await resolveActor(container, req),\n ),\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/people/:orgUserId/roles/content/:contentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unassignRole',\n tags: peopleTags,\n summary: \"Remove a person's role on a content item\",\n description: 'Requires `people:write`.',\n params: OrgUserContentParam,\n response: { 200: AssignmentList, ...refusals(403, 404) },\n },\n handler: async (req) =>\n organizations.unassignRole(\n req.params.orgUserId,\n req.params.contentId,\n await resolveActor(container, req),\n ),\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/content/:contentId/roles',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listRolesOn',\n tags: peopleTags,\n summary: 'The people with a role on a content item',\n description: 'Requires `people:read`.',\n params: ContentIdParam,\n response: { 200: AssignmentList, ...refusals(403) },\n },\n handler: async (req) =>\n organizations.listAssignmentsOn(req.params.contentId, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/people/:orgUserId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removePerson',\n tags: peopleTags,\n summary: 'Remove a person from the organization',\n description: `Requires \\`people:delete\\`. Needs every permission the person holds, where they hold it. Revokes their API keys and connected apps here. The owner can't be removed (409); neither can someone whose posts must be kept (409).`,\n params: OrgUserIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n await organizations.removePerson(req.params.orgUserId, await resolveActor(container, req));\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createInvite',\n tags: peopleTags,\n summary: 'Invite a person with an org role',\n description: `Requires \\`people:write\\`. The person is created by this call, so access and item roles can be given before they accept. Needs every permission the role holds; never \\`owner\\` (409).`,\n body: CreateInvite,\n response: { 201: Invite, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const caller = await resolveCaller(container, req);\n const invite = await organizations.createInvite(req.body, caller.actor, caller.userId);\n return reply.code(201).send(inviteView(invite));\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/people/:orgUserId/invite/resend',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resendInvite',\n tags: peopleTags,\n summary: \"Resend a person's pending invite\",\n description: `Requires \\`people:write\\`. Issues a new link; the resender becomes the inviter.`,\n params: OrgUserIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const caller = await resolveCaller(container, req);\n await organizations.resendInvite(req.params.orgUserId, caller.actor, caller.userId);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/people/:orgUserId/invite',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'cancelInvite',\n tags: peopleTags,\n summary: \"Cancel a person's pending invite\",\n description: `Requires \\`people:write\\`. Someone still invited who holds no access and no item role is removed with it.`,\n params: OrgUserIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n await organizations.cancelInvite(req.params.orgUserId, await resolveActor(container, req));\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/invites/:token',\n schema: {\n operationId: 'getInvite',\n tags: peopleTags,\n summary: 'Read an invite by its token',\n description:\n 'Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.',\n params: InviteTokenParam,\n response: { 200: GetInviteResult, ...refusals(404) },\n },\n handler: async (req) => organizations.describeInvite(req.params.token),\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites/accept',\n preHandler: app.requireSession,\n schema: {\n operationId: 'acceptInvite',\n tags: peopleTags,\n summary: 'Accept an invite with the signed-in account',\n description:\n \"For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired, the email does not match, or the inviter no longer holds the role.\",\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, ...refusals(400, 404) },\n },\n handler: async (req) => {\n await organizations.acceptInvite(\n { token: req.body.token, authUserId: req.authUser.id, email: req.authUser.email },\n req.headers,\n );\n return {};\n },\n });\n\n const notificationTags = ['Notifications'];\n\n r.route({\n method: 'GET',\n url: '/api/notification-types',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listNotificationTypes',\n tags: notificationTags,\n summary: 'List the notification types a category list can cover',\n description: `Requires one of \\`notification_categories:read\\`, \\`communities:read\\`. \\`scope=org\\` lists the types the org list can cover and needs \\`notification_categories:read\\`; \\`scope=community\\` lists those a community list can cover and also takes \\`communities:read\\` on any community. Each type carries all its scopes, so an editor can tell which types can be per space.`,\n querystring: NotificationListScopeQuery,\n response: { 200: NotificationTypeInfos, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCatalog.types(actor, req.query.scope);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/notification-channels',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listNotificationChannels',\n tags: notificationTags,\n summary: 'List the notification channels this deployment can deliver on',\n description: `Requires one of \\`notification_categories:read\\`, \\`communities:read\\`. \\`communities:read\\` on any community is enough. \\`inApp\\` is always first.`,\n response: { 200: NotificationChannelInfos, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCatalog.channels(actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/notification-categories',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getOrgNotificationCategories',\n tags: notificationTags,\n summary: \"Get the organization's notification categories\",\n description: `Requires \\`notification_categories:read\\`. The list in use for notifications outside any community, with \\`source\\` saying whether it is the default or a stored list.`,\n response: { 200: NotificationCategoryList, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCategories.org.get(actor.orgId, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/notification-categories',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'replaceOrgNotificationCategories',\n tags: notificationTags,\n summary: \"Replace the organization's notification categories\",\n description: `Requires \\`notification_categories:write\\`. Stores the whole list. 422 when a key repeats, a type cannot be in the org list, a reason is not allowed for its type, a channel is not configured, or earlier categories already cover everything a category covers.`,\n body: ReplaceNotificationCategories,\n response: { 200: NotificationCategoryList, ...refusals(400, 403, 422) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.notificationCategories.org.replace(actor.orgId, req.body.categories, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/notification-categories',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resetOrgNotificationCategories',\n tags: notificationTags,\n summary: 'Go back to the default notification categories for the organization',\n description: `Requires \\`notification_categories:write\\`. Removes the stored list; the default applies again.`,\n response: { 204: z.void(), ...refusals(403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.notificationCategories.org.reset(actor.orgId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/notification-categories/defaults',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDefaultNotificationCategories',\n tags: notificationTags,\n summary: 'Get the default notification categories',\n description: `Requires one of \\`notification_categories:read\\`, \\`communities:read\\`. The default org list (\\`scope=org\\`, which needs \\`notification_categories:read\\`) or community list (\\`scope=community\\`, which also takes \\`communities:read\\` on any community), offering only the configured channels.`,\n querystring: NotificationListScopeQuery,\n response: { 200: NotificationCategoryDefaults, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return { categories: container.notificationCatalog.defaults(actor, req.query.scope) };\n },\n });\n}\n"],"mappings":";;;;;;;;;AAsCA,SAAS,WAAW,QAAsB;CACxC,OAAO;EACL,IAAI,OAAO;EACX,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,WAAW,OAAO;CACpB;AACF;AAEA,eAAsB,oBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,gBAAgB,UAAU;CAChC,MAAM,OAAO,CAAC,eAAe;CAC7B,MAAM,aAAa,CAAC,QAAQ;CAC5B,MAAM,WAAW,CAAC,OAAO;CAEzB,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU,EAAE,KAAK,aAAa;EAChC;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,MAAM,MAAM,cAAc,mBAAmB,IAAI,MAAM,IAAI,OAAO;GACxE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;IAC1B,MAAM,IAAI;IACV,MAAM,IAAI;IACV,IAAI,IAAI;IACR,WAAW,IAAI,UAAU,YAAY;GACvC,CAAC;EACH;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAc,GAAG,SAAS,KAAK,GAAG;GAAE;EACvD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,MAAM,MAAM,cAAc,mBAAmB,IAAI,MAAM,KAAK;GAClE,OAAO;IACL,IAAI,IAAI;IACR,MAAM,IAAI;IACV,MAAM,IAAI;IACV,WAAW,IAAI,UAAU,YAAY;GACvC;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,kBAAkB,IAAI,MAAM,KAAK;GACrD,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,MAAM;GACN,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,GAAG;GAAE;EACjD;EACA,SAAS,OAAO,QAAQ,cAAc,UAAU,MAAM,aAAa,WAAW,GAAG,CAAC;CACpF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QACd,cAAc,QAAQ,IAAI,OAAO,KAAK,MAAM,aAAa,WAAW,GAAG,CAAC;CAC5E,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAChE;EACA,SAAS,OAAO,QACd,cAAc,WAAW,IAAI,OAAO,KAAK,IAAI,MAAM,MAAM,aAAa,WAAW,GAAG,CAAC;CACzF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,KAAK,UACnB,MACG,KAAK,GAAG,CAAC,CACT,KAAK,MAAM,cAAc,WAAW,IAAI,MAAM,MAAM,aAAa,WAAW,GAAG,CAAC,CAAC;CACxF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QACd,cAAc,UAAU,IAAI,OAAO,KAAK,MAAM,aAAa,WAAW,GAAG,CAAC;CAC9E,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,cAAc,WAAW,IAAI,OAAO,KAAK,MAAM,aAAa,WAAW,GAAG,CAAC;GACjF,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,MAAM;GACN,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,GAAG;GAAE;EACjD;EACA,SAAS,OAAO,QAAQ,cAAc,SAAS,IAAI,OAAO,MAAM,aAAa,WAAW,GAAG,CAAC;CAC9F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,QACd,cAAc,gBAAgB,IAAI,OAAO,WAAW,MAAM,aAAa,WAAW,GAAG,CAAC;CAC1F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QACd,cAAc,WACZ,IAAI,OAAO,WACX,IAAI,KAAK,SACT,MAAM,aAAa,WAAW,GAAG,CACnC;CACJ,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QACd,cAAc,WACZ,IAAI,OAAO,WACX,IAAI,OAAO,WACX,IAAI,KAAK,SACT,MAAM,aAAa,WAAW,GAAG,CACnC;CACJ,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QACd,cAAc,aACZ,IAAI,OAAO,WACX,IAAI,OAAO,WACX,MAAM,aAAa,WAAW,GAAG,CACnC;CACJ,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,QACd,cAAc,kBAAkB,IAAI,OAAO,WAAW,MAAM,aAAa,WAAW,GAAG,CAAC;CAC5F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,cAAc,aAAa,IAAI,OAAO,WAAW,MAAM,aAAa,WAAW,GAAG,CAAC;GACzF,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,MAAM;GACN,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAQ,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,SAAS,MAAM,cAAc,WAAW,GAAG;GACjD,MAAM,SAAS,MAAM,cAAc,aAAa,IAAI,MAAM,OAAO,OAAO,OAAO,MAAM;GACrF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,WAAW,MAAM,CAAC;EAChD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,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,SAAS,MAAM,cAAc,WAAW,GAAG;GACjD,MAAM,cAAc,aAAa,IAAI,OAAO,WAAW,OAAO,OAAO,OAAO,MAAM;GAClF,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,MAAM;GACN,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,cAAc,aAAa,IAAI,OAAO,WAAW,MAAM,aAAa,WAAW,GAAG,CAAC;GACzF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAiB,GAAG,SAAS,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,QAAQ,cAAc,eAAe,IAAI,OAAO,KAAK;CACvE,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK;IAAoB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,cAAc,aAClB;IAAE,OAAO,IAAI,KAAK;IAAO,YAAY,IAAI,SAAS;IAAI,OAAO,IAAI,SAAS;GAAM,GAChF,IAAI,OACN;GACA,OAAO,CAAC;EACV;CACF,CAAC;CAED,MAAM,mBAAmB,CAAC,eAAe;CAEzC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAuB,GAAG,SAAS,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,oBAAoB,MAAM,OAAO,IAAI,MAAM,KAAK;EACnE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,oBAAoB,SAAS,KAAK;EACrD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,uBAAuB,IAAI,IAAI,MAAM,OAAO,KAAK;EACpE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,uBAAuB,IAAI,QAAQ,MAAM,OAAO,IAAI,KAAK,YAAY,KAAK;EAC7F;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM;GACN,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,GAAG;GAAE;EAC9C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,uBAAuB,IAAI,MAAM,MAAM,OAAO,KAAK;GACnE,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,MAAM;GACN,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAA8B,GAAG,SAAS,GAAG;GAAE;EAClE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,EAAE,YAAY,UAAU,oBAAoB,SAAS,OAAO,IAAI,MAAM,KAAK,EAAE;EACtF;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Container } from "../../app/container.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/routes/platform.d.ts
|
|
4
|
+
declare function platformRoutes(app: FastifyInstance, container: Container): Promise<void>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { platformRoutes };
|
|
7
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { refusals } from "../schemas/shared.js";
|
|
2
|
+
import { CreatedApiKey } from "../schemas/access.js";
|
|
3
|
+
import { CreatePlatformApiKey, CreatePlatformOrganization, PlatformOrgIdParam, PlatformOrganization } from "../schemas/platform.js";
|
|
4
|
+
import "../schemas/index.js";
|
|
5
|
+
//#region src/http/routes/platform.ts
|
|
6
|
+
async function platformRoutes(app, container) {
|
|
7
|
+
const r = app.withTypeProvider();
|
|
8
|
+
const tags = ["Platform"];
|
|
9
|
+
const organizations = container.organizations;
|
|
10
|
+
r.route({
|
|
11
|
+
method: "POST",
|
|
12
|
+
url: "/api/platform/organizations",
|
|
13
|
+
preHandler: app.requirePlatform,
|
|
14
|
+
schema: {
|
|
15
|
+
operationId: "createPlatformOrganization",
|
|
16
|
+
tags,
|
|
17
|
+
summary: "Create an organization and its first API key",
|
|
18
|
+
description: `Requires the platform key as \`Authorization: Bearer <platform key>\`. Creates the organization, owned by the platform account, and an API key for the platform account with the Owner role and no expiry. The key is returned once. 409 when the slug is in use.`,
|
|
19
|
+
body: CreatePlatformOrganization,
|
|
20
|
+
response: {
|
|
21
|
+
201: PlatformOrganization,
|
|
22
|
+
...refusals(400, 409)
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handler: async (req, reply) => {
|
|
26
|
+
const created = await organizations.createPlatformOrganization(req.body);
|
|
27
|
+
return reply.code(201).send({
|
|
28
|
+
organization: {
|
|
29
|
+
...created.organization,
|
|
30
|
+
createdAt: created.organization.createdAt.toISOString()
|
|
31
|
+
},
|
|
32
|
+
apiKey: created.apiKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
r.route({
|
|
37
|
+
method: "POST",
|
|
38
|
+
url: "/api/platform/organizations/:orgId/api-keys",
|
|
39
|
+
preHandler: app.requirePlatform,
|
|
40
|
+
schema: {
|
|
41
|
+
operationId: "createPlatformApiKey",
|
|
42
|
+
tags,
|
|
43
|
+
summary: "Create another API key for an organization",
|
|
44
|
+
description: `Requires the platform key as \`Authorization: Bearer <platform key>\`. Creates an API key for the platform account in the organization, with the Owner role and no expiry, to replace a lost or leaked one; revoke the old key with the new one. The key is returned once. 404 when the platform account is not an active member of the organization.`,
|
|
45
|
+
params: PlatformOrgIdParam,
|
|
46
|
+
body: CreatePlatformApiKey,
|
|
47
|
+
response: {
|
|
48
|
+
201: CreatedApiKey,
|
|
49
|
+
...refusals(400, 404)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
handler: async (req, reply) => {
|
|
53
|
+
const created = await organizations.createPlatformApiKey(req.params.orgId, req.body.name);
|
|
54
|
+
return reply.code(201).send(created);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { platformRoutes };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","names":[],"sources":["../../../src/http/routes/platform.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport type { Container } from '../../app/container.js';\nimport {\n CreatedApiKey,\n CreatePlatformApiKey,\n CreatePlatformOrganization,\n PlatformOrganization,\n PlatformOrgIdParam,\n refusals,\n} from '../schemas/index.js';\n\nexport async function platformRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Platform'];\n const organizations = container.organizations;\n\n r.route({\n method: 'POST',\n url: '/api/platform/organizations',\n preHandler: app.requirePlatform,\n schema: {\n operationId: 'createPlatformOrganization',\n tags,\n summary: 'Create an organization and its first API key',\n description: `Requires the platform key as \\`Authorization: Bearer <platform key>\\`. Creates the organization, owned by the platform account, and an API key for the platform account with the Owner role and no expiry. The key is returned once. 409 when the slug is in use.`,\n body: CreatePlatformOrganization,\n response: { 201: PlatformOrganization, ...refusals(400, 409) },\n },\n handler: async (req, reply) => {\n const created = await organizations.createPlatformOrganization(req.body);\n return reply.code(201).send({\n organization: {\n ...created.organization,\n createdAt: created.organization.createdAt.toISOString(),\n },\n apiKey: created.apiKey,\n });\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/platform/organizations/:orgId/api-keys',\n preHandler: app.requirePlatform,\n schema: {\n operationId: 'createPlatformApiKey',\n tags,\n summary: 'Create another API key for an organization',\n description: `Requires the platform key as \\`Authorization: Bearer <platform key>\\`. Creates an API key for the platform account in the organization, with the Owner role and no expiry, to replace a lost or leaked one; revoke the old key with the new one. The key is returned once. 404 when the platform account is not an active member of the organization.`,\n params: PlatformOrgIdParam,\n body: CreatePlatformApiKey,\n response: { 201: CreatedApiKey, ...refusals(400, 404) },\n },\n handler: async (req, reply) => {\n const created = await organizations.createPlatformApiKey(req.params.orgId, req.body.name);\n return reply.code(201).send(created);\n },\n });\n}\n"],"mappings":";;;;;AAYA,eAAsB,eAAe,KAAsB,WAAqC;CAC9F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,UAAU;CACxB,MAAM,gBAAgB,UAAU;CAEhC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAsB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC/D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,MAAM,cAAc,2BAA2B,IAAI,IAAI;GACvE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;IAC1B,cAAc;KACZ,GAAG,QAAQ;KACX,WAAW,QAAQ,aAAa,UAAU,YAAY;IACxD;IACA,QAAQ,QAAQ;GAClB,CAAC;EACH;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAe,GAAG,SAAS,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,MAAM,cAAc,qBAAqB,IAAI,OAAO,OAAO,IAAI,KAAK,IAAI;GACxF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,OAAO;EACrC;CACF,CAAC;AACH"}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { Student, StudentAnalytics, StudentIdParam, StudentsPage, StudentsQuery } from "../schemas/students.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
5
|
//#region src/http/routes/students.ts
|
|
8
6
|
async function studentsRoutes(app, container) {
|
|
9
7
|
const r = app.withTypeProvider();
|
|
@@ -14,19 +12,16 @@ async function studentsRoutes(app, container) {
|
|
|
14
12
|
preHandler: app.requireOrgSession,
|
|
15
13
|
schema: {
|
|
16
14
|
operationId: "listStudents",
|
|
17
|
-
tags: ["
|
|
18
|
-
summary: "List students
|
|
19
|
-
description:
|
|
15
|
+
tags: ["People"],
|
|
16
|
+
summary: "List students",
|
|
17
|
+
description: `Requires \`people:read\`. People whose org role is Member, invited ones included, with the roles they hold on content items.`,
|
|
20
18
|
querystring: StudentsQuery,
|
|
21
19
|
response: {
|
|
22
20
|
200: StudentsPage,
|
|
23
21
|
...refusals(403)
|
|
24
22
|
}
|
|
25
23
|
},
|
|
26
|
-
handler: async (req) =>
|
|
27
|
-
const scope = await resolveScope(container, req);
|
|
28
|
-
return students.listStudents(scope.orgId, req.query);
|
|
29
|
-
}
|
|
24
|
+
handler: async (req) => students.listStudents(req.query, await resolveActor(container, req))
|
|
30
25
|
});
|
|
31
26
|
r.route({
|
|
32
27
|
method: "GET",
|
|
@@ -34,9 +29,9 @@ async function studentsRoutes(app, container) {
|
|
|
34
29
|
preHandler: app.requireOrgSession,
|
|
35
30
|
schema: {
|
|
36
31
|
operationId: "getStudent",
|
|
37
|
-
tags: ["
|
|
38
|
-
summary: "
|
|
39
|
-
description:
|
|
32
|
+
tags: ["People"],
|
|
33
|
+
summary: "A person's learner record",
|
|
34
|
+
description: `Requires \`people:read\`. Opens for any person in the organization, whatever their roles.`,
|
|
40
35
|
params: StudentIdParam,
|
|
41
36
|
response: {
|
|
42
37
|
200: Student,
|
|
@@ -44,10 +39,7 @@ async function studentsRoutes(app, container) {
|
|
|
44
39
|
}
|
|
45
40
|
},
|
|
46
41
|
handler: async (req) => {
|
|
47
|
-
|
|
48
|
-
const student = await students.getStudent(scope.orgId, req.params.id);
|
|
49
|
-
if (!student) throw new NotFoundError("Student", req.params.id);
|
|
50
|
-
return student;
|
|
42
|
+
return students.getStudent(req.params.id, await resolveActor(container, req));
|
|
51
43
|
}
|
|
52
44
|
});
|
|
53
45
|
r.route({
|
|
@@ -57,8 +49,8 @@ async function studentsRoutes(app, container) {
|
|
|
57
49
|
schema: {
|
|
58
50
|
operationId: "getStudentAnalytics",
|
|
59
51
|
tags: ["Reporting"],
|
|
60
|
-
summary: "A
|
|
61
|
-
description:
|
|
52
|
+
summary: "A person's progress across courses",
|
|
53
|
+
description: `Requires \`people:read\`. Per-course progress and completion.`,
|
|
62
54
|
params: StudentIdParam,
|
|
63
55
|
response: {
|
|
64
56
|
200: StudentAnalytics,
|
|
@@ -66,62 +58,7 @@ async function studentsRoutes(app, container) {
|
|
|
66
58
|
}
|
|
67
59
|
},
|
|
68
60
|
handler: async (req) => {
|
|
69
|
-
|
|
70
|
-
const analytics = await students.studentAnalytics(scope.orgId, req.params.id);
|
|
71
|
-
if (!analytics) throw new NotFoundError("Student", req.params.id);
|
|
72
|
-
return analytics;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
r.route({
|
|
76
|
-
method: "POST",
|
|
77
|
-
url: "/api/students/:id/invite/resend",
|
|
78
|
-
preHandler: app.requireOrgSession,
|
|
79
|
-
schema: {
|
|
80
|
-
operationId: "resendStudentInvite",
|
|
81
|
-
tags: ["Organizations"],
|
|
82
|
-
summary: "Re-send a pending student's invite email (staff)",
|
|
83
|
-
description: "Owner or admin only. Issues a new token, so the previous link stops working. 404 when the student has no pending invite; 409 when it was already accepted.",
|
|
84
|
-
params: StudentIdParam,
|
|
85
|
-
response: {
|
|
86
|
-
204: z.void(),
|
|
87
|
-
...refusals(403, 404, 409)
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
handler: async (req, reply) => {
|
|
91
|
-
const scope = await resolveScope(container, req);
|
|
92
|
-
await container.organizations.resendStudentInvite({
|
|
93
|
-
id: scope.orgUserId,
|
|
94
|
-
role: scope.role
|
|
95
|
-
}, {
|
|
96
|
-
orgId: scope.orgId,
|
|
97
|
-
orgUserId: req.params.id,
|
|
98
|
-
inviterUserId: scope.userId
|
|
99
|
-
});
|
|
100
|
-
return reply.code(204).send();
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
r.route({
|
|
104
|
-
method: "DELETE",
|
|
105
|
-
url: "/api/students/:id",
|
|
106
|
-
preHandler: app.requireOrgSession,
|
|
107
|
-
schema: {
|
|
108
|
-
operationId: "deleteStudent",
|
|
109
|
-
tags: ["Organizations"],
|
|
110
|
-
summary: "Delete a student (staff)",
|
|
111
|
-
description: "Owner or admin only. 409 when the person has written posts, comments or messages.",
|
|
112
|
-
params: StudentIdParam,
|
|
113
|
-
response: {
|
|
114
|
-
204: z.void(),
|
|
115
|
-
...refusals(403, 404, 409)
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
handler: async (req, reply) => {
|
|
119
|
-
const scope = await resolveScope(container, req);
|
|
120
|
-
await container.organizations.deleteOrgUser(scope.orgId, {
|
|
121
|
-
id: scope.orgUserId,
|
|
122
|
-
role: scope.role
|
|
123
|
-
}, req.params.id);
|
|
124
|
-
return reply.code(204).send();
|
|
61
|
+
return students.studentAnalytics(req.params.id, await resolveActor(container, req));
|
|
125
62
|
}
|
|
126
63
|
});
|
|
127
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Student,\n StudentAnalytics,\n StudentIdParam,\n StudentsPage,\n StudentsQuery,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function studentsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const students = container.reporting.admin;\n\n r.route({\n method: 'GET',\n url: '/api/students',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listStudents',\n tags: ['People'],\n summary: 'List students',\n description: `Requires \\`people:read\\`. People whose org role is Member, invited ones included, with the roles they hold on content items.`,\n querystring: StudentsQuery,\n response: { 200: StudentsPage, ...refusals(403) },\n },\n handler: async (req) => students.listStudents(req.query, await resolveActor(container, req)),\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudent',\n tags: ['People'],\n summary: \"A person's learner record\",\n description: `Requires \\`people:read\\`. Opens for any person in the organization, whatever their roles.`,\n params: StudentIdParam,\n response: { 200: Student, ...refusals(403, 404) },\n },\n handler: async (req) => {\n return students.getStudent(req.params.id, await resolveActor(container, req));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudentAnalytics',\n tags: ['Reporting'],\n summary: \"A person's progress across courses\",\n description: `Requires \\`people:read\\`. Per-course progress and completion.`,\n params: StudentIdParam,\n response: { 200: StudentAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n return students.studentAnalytics(req.params.id, await resolveActor(container, req));\n },\n });\n}\n"],"mappings":";;;;;AAaA,eAAsB,eAAe,KAAsB,WAAqC;CAC9F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,WAAW,UAAU,UAAU;CAErC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAc,GAAG,SAAS,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ,SAAS,aAAa,IAAI,OAAO,MAAM,aAAa,WAAW,GAAG,CAAC;CAC7F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,QAAQ;GACf,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAS,GAAG,SAAS,KAAK,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,MAAM,aAAa,WAAW,GAAG,CAAC;EAC9E;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,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,SAAS,iBAAiB,IAAI,OAAO,IAAI,MAAM,aAAa,WAAW,GAAG,CAAC;EACpF;CACF,CAAC;AACH"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { AutomationIdParam, CreateWebhookBody, CreatedWebhook, UpdateWebhookBody, Webhook, WebhookSecret } from "../schemas/automations.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/webhooks.ts
|
|
8
|
-
const DELIVERY_DESCRIPTION = "
|
|
7
|
+
const DELIVERY_DESCRIPTION = "`url` must be a public address: localhost and private network addresses are refused when saved and when delivering. Each subscribed event is POSTed to `url` as the JSON event envelope (`id`, `type`, `version`, `orgId`, `occurredAt`, `data`), signed per the Standard Webhooks spec: `webhook-id` (the event id, stable across retries), `webhook-timestamp` (unix seconds) and `webhook-signature` (`v1,` + base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the base64-decoded part of the `whsec_` secret). Any non-2xx response or timeout is a failed attempt and is retried. Deliveries are the webhook's automation runs: `GET /api/automations/{id}/runs`.";
|
|
9
8
|
async function webhooksRoutes(app, container) {
|
|
10
9
|
const r = app.withTypeProvider();
|
|
11
10
|
const automations = container.automations;
|
|
@@ -17,16 +16,16 @@ async function webhooksRoutes(app, container) {
|
|
|
17
16
|
schema: {
|
|
18
17
|
operationId: "listWebhooks",
|
|
19
18
|
tags,
|
|
20
|
-
summary: "List the organization's webhooks
|
|
21
|
-
description:
|
|
19
|
+
summary: "List the organization's webhooks",
|
|
20
|
+
description: `Requires \`automations:read\`. Signing secrets are not included.`,
|
|
22
21
|
response: {
|
|
23
22
|
200: z.array(Webhook),
|
|
24
23
|
...refusals(403)
|
|
25
24
|
}
|
|
26
25
|
},
|
|
27
26
|
handler: async (req) => {
|
|
28
|
-
const
|
|
29
|
-
return automations.listWebhooks(
|
|
27
|
+
const actor = await resolveActor(container, req);
|
|
28
|
+
return automations.listWebhooks(actor);
|
|
30
29
|
}
|
|
31
30
|
});
|
|
32
31
|
r.route({
|
|
@@ -36,8 +35,8 @@ async function webhooksRoutes(app, container) {
|
|
|
36
35
|
schema: {
|
|
37
36
|
operationId: "createWebhook",
|
|
38
37
|
tags,
|
|
39
|
-
summary: "Create a webhook
|
|
40
|
-
description: DELIVERY_DESCRIPTION
|
|
38
|
+
summary: "Create a webhook",
|
|
39
|
+
description: `Requires \`automations:write\`. ${DELIVERY_DESCRIPTION}`,
|
|
41
40
|
body: CreateWebhookBody,
|
|
42
41
|
response: {
|
|
43
42
|
201: CreatedWebhook,
|
|
@@ -45,8 +44,8 @@ async function webhooksRoutes(app, container) {
|
|
|
45
44
|
}
|
|
46
45
|
},
|
|
47
46
|
handler: async (req, reply) => {
|
|
48
|
-
const
|
|
49
|
-
const webhook = await automations.createWebhook(
|
|
47
|
+
const actor = await resolveActor(container, req);
|
|
48
|
+
const webhook = await automations.createWebhook(req.body, actor);
|
|
50
49
|
return reply.code(201).send(webhook);
|
|
51
50
|
}
|
|
52
51
|
});
|
|
@@ -57,8 +56,8 @@ async function webhooksRoutes(app, container) {
|
|
|
57
56
|
schema: {
|
|
58
57
|
operationId: "getWebhook",
|
|
59
58
|
tags,
|
|
60
|
-
summary: "Get a webhook
|
|
61
|
-
description:
|
|
59
|
+
summary: "Get a webhook",
|
|
60
|
+
description: `Requires \`automations:read\`. The signing secret is not included.`,
|
|
62
61
|
params: AutomationIdParam,
|
|
63
62
|
response: {
|
|
64
63
|
200: Webhook,
|
|
@@ -66,10 +65,8 @@ async function webhooksRoutes(app, container) {
|
|
|
66
65
|
}
|
|
67
66
|
},
|
|
68
67
|
handler: async (req) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (!webhook) throw new NotFoundError("Webhook", req.params.id);
|
|
72
|
-
return webhook;
|
|
68
|
+
const actor = await resolveActor(container, req);
|
|
69
|
+
return automations.getWebhook(req.params.id, actor);
|
|
73
70
|
}
|
|
74
71
|
});
|
|
75
72
|
r.route({
|
|
@@ -79,8 +76,8 @@ async function webhooksRoutes(app, container) {
|
|
|
79
76
|
schema: {
|
|
80
77
|
operationId: "updateWebhook",
|
|
81
78
|
tags,
|
|
82
|
-
summary: "Change a webhook's URL, events, description or enabled flag
|
|
83
|
-
description:
|
|
79
|
+
summary: "Change a webhook's URL, events, description or enabled flag",
|
|
80
|
+
description: `Requires \`automations:write\`. Changes only the fields sent. 400 when an event is reserved or unknown.`,
|
|
84
81
|
params: AutomationIdParam,
|
|
85
82
|
body: UpdateWebhookBody,
|
|
86
83
|
response: {
|
|
@@ -89,10 +86,8 @@ async function webhooksRoutes(app, container) {
|
|
|
89
86
|
}
|
|
90
87
|
},
|
|
91
88
|
handler: async (req) => {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (!webhook) throw new NotFoundError("Webhook", req.params.id);
|
|
95
|
-
return webhook;
|
|
89
|
+
const actor = await resolveActor(container, req);
|
|
90
|
+
return automations.updateWebhook(req.params.id, req.body, actor);
|
|
96
91
|
}
|
|
97
92
|
});
|
|
98
93
|
r.route({
|
|
@@ -102,8 +97,8 @@ async function webhooksRoutes(app, container) {
|
|
|
102
97
|
schema: {
|
|
103
98
|
operationId: "deleteWebhook",
|
|
104
99
|
tags,
|
|
105
|
-
summary: "Delete a webhook and its signing secret
|
|
106
|
-
description:
|
|
100
|
+
summary: "Delete a webhook and its signing secret",
|
|
101
|
+
description: `Requires \`automations:delete\`. Its delivery history is kept.`,
|
|
107
102
|
params: AutomationIdParam,
|
|
108
103
|
response: {
|
|
109
104
|
204: z.void(),
|
|
@@ -111,8 +106,8 @@ async function webhooksRoutes(app, container) {
|
|
|
111
106
|
}
|
|
112
107
|
},
|
|
113
108
|
handler: async (req, reply) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
109
|
+
const actor = await resolveActor(container, req);
|
|
110
|
+
await automations.deleteWebhook(req.params.id, actor);
|
|
116
111
|
return reply.code(204).send();
|
|
117
112
|
}
|
|
118
113
|
});
|
|
@@ -123,8 +118,8 @@ async function webhooksRoutes(app, container) {
|
|
|
123
118
|
schema: {
|
|
124
119
|
operationId: "getWebhookSecret",
|
|
125
120
|
tags,
|
|
126
|
-
summary: "Reveal a webhook's signing secret
|
|
127
|
-
description: "
|
|
121
|
+
summary: "Reveal a webhook's signing secret",
|
|
122
|
+
description: "Requires `automations:write`.",
|
|
128
123
|
params: AutomationIdParam,
|
|
129
124
|
response: {
|
|
130
125
|
200: WebhookSecret,
|
|
@@ -132,10 +127,8 @@ async function webhooksRoutes(app, container) {
|
|
|
132
127
|
}
|
|
133
128
|
},
|
|
134
129
|
handler: async (req) => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (!secret) throw new NotFoundError("Webhook", req.params.id);
|
|
138
|
-
return secret;
|
|
130
|
+
const actor = await resolveActor(container, req);
|
|
131
|
+
return automations.revealWebhookSecret(req.params.id, actor);
|
|
139
132
|
}
|
|
140
133
|
});
|
|
141
134
|
r.route({
|
|
@@ -145,8 +138,8 @@ async function webhooksRoutes(app, container) {
|
|
|
145
138
|
schema: {
|
|
146
139
|
operationId: "rotateWebhookSecret",
|
|
147
140
|
tags,
|
|
148
|
-
summary: "Replace a webhook's signing secret
|
|
149
|
-
description:
|
|
141
|
+
summary: "Replace a webhook's signing secret",
|
|
142
|
+
description: `Requires \`automations:write\`. The old secret stops working immediately.`,
|
|
150
143
|
params: AutomationIdParam,
|
|
151
144
|
response: {
|
|
152
145
|
200: WebhookSecret,
|
|
@@ -154,10 +147,8 @@ async function webhooksRoutes(app, container) {
|
|
|
154
147
|
}
|
|
155
148
|
},
|
|
156
149
|
handler: async (req) => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (!secret) throw new NotFoundError("Webhook", req.params.id);
|
|
160
|
-
return secret;
|
|
150
|
+
const actor = await resolveActor(container, req);
|
|
151
|
+
return automations.rotateWebhookSecret(req.params.id, actor);
|
|
161
152
|
}
|
|
162
153
|
});
|
|
163
154
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.js","names":[],"sources":["../../../src/http/routes/webhooks.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n AutomationIdParam,\n CreatedWebhook,\n CreateWebhookBody,\n UpdateWebhookBody,\n Webhook,\n WebhookSecret,\n refusals,\n} from '../schemas/index.js';\nimport
|
|
1
|
+
{"version":3,"file":"webhooks.js","names":[],"sources":["../../../src/http/routes/webhooks.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n AutomationIdParam,\n CreatedWebhook,\n CreateWebhookBody,\n UpdateWebhookBody,\n Webhook,\n WebhookSecret,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nconst DELIVERY_DESCRIPTION =\n \"`url` must be a public address: localhost and private network addresses are refused when saved and when delivering. Each subscribed event is POSTed to `url` as the JSON event envelope (`id`, `type`, `version`, `orgId`, `occurredAt`, `data`), signed per the Standard Webhooks spec: `webhook-id` (the event id, stable across retries), `webhook-timestamp` (unix seconds) and `webhook-signature` (`v1,` + base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the base64-decoded part of the `whsec_` secret). Any non-2xx response or timeout is a failed attempt and is retried. Deliveries are the webhook's automation runs: `GET /api/automations/{id}/runs`.\";\n\nexport async function webhooksRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const automations = container.automations;\n const tags = ['Automations'];\n\n r.route({\n method: 'GET',\n url: '/api/automations/webhooks',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listWebhooks',\n tags,\n summary: \"List the organization's webhooks\",\n description: `Requires \\`automations:read\\`. Signing secrets are not included.`,\n response: { 200: z.array(Webhook), ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.listWebhooks(actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/webhooks',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createWebhook',\n tags,\n summary: 'Create a webhook',\n description: `Requires \\`automations:write\\`. ${DELIVERY_DESCRIPTION}`,\n body: CreateWebhookBody,\n response: { 201: CreatedWebhook, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const webhook = await automations.createWebhook(req.body, actor);\n return reply.code(201).send(webhook);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/webhooks/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getWebhook',\n tags,\n summary: 'Get a webhook',\n description: `Requires \\`automations:read\\`. The signing secret is not included.`,\n params: AutomationIdParam,\n response: { 200: Webhook, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.getWebhook(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/automations/webhooks/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateWebhook',\n tags,\n summary: \"Change a webhook's URL, events, description or enabled flag\",\n description: `Requires \\`automations:write\\`. Changes only the fields sent. 400 when an event is reserved or unknown.`,\n params: AutomationIdParam,\n body: UpdateWebhookBody,\n response: { 200: Webhook, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.updateWebhook(req.params.id, req.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/automations/webhooks/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteWebhook',\n tags,\n summary: 'Delete a webhook and its signing secret',\n description: `Requires \\`automations:delete\\`. Its delivery history is kept.`,\n params: AutomationIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await automations.deleteWebhook(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/webhooks/:id/secret',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getWebhookSecret',\n tags,\n summary: \"Reveal a webhook's signing secret\",\n description: 'Requires `automations:write`.',\n params: AutomationIdParam,\n response: { 200: WebhookSecret, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.revealWebhookSecret(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/webhooks/:id/secret/rotate',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'rotateWebhookSecret',\n tags,\n summary: \"Replace a webhook's signing secret\",\n description: `Requires \\`automations:write\\`. The old secret stops working immediately.`,\n params: AutomationIdParam,\n response: { 200: WebhookSecret, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.rotateWebhookSecret(req.params.id, actor);\n },\n });\n}\n"],"mappings":"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"}
|
package/dist/http/routes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { communitiesRoutes } from "./routes/communities.js";
|
|
|
2
2
|
import { communityModerationRoutes } from "./routes/community-moderation.js";
|
|
3
3
|
import { coursesRoutes } from "./routes/courses.js";
|
|
4
4
|
import { downloadsRoutes } from "./routes/downloads.js";
|
|
5
|
+
import { eventsRoutes } from "./routes/events.js";
|
|
5
6
|
import { bundlesRoutes } from "./routes/bundles.js";
|
|
6
7
|
import { memberRoutes } from "./routes/member/index.js";
|
|
7
8
|
import { discussionRoutes } from "./routes/discussion.js";
|
|
@@ -16,6 +17,8 @@ import { courseAnalyticsRoutes } from "./routes/course-analytics.js";
|
|
|
16
17
|
import { assetsRoutes } from "./routes/assets.js";
|
|
17
18
|
import { integrationsRoutes } from "./routes/integrations.js";
|
|
18
19
|
import { accessRoutes } from "./routes/access.js";
|
|
20
|
+
import { platformRoutes } from "./routes/platform.js";
|
|
21
|
+
import { registerPlatformAuth } from "./plugins/platform.js";
|
|
19
22
|
//#region src/http/routes.ts
|
|
20
23
|
function registerRoutes(app, container) {
|
|
21
24
|
app.get("/health", async () => ({ status: "ok" }));
|
|
@@ -28,6 +31,7 @@ function registerRoutes(app, container) {
|
|
|
28
31
|
await communitiesRoutes(instance, container);
|
|
29
32
|
await communityModerationRoutes(instance, container);
|
|
30
33
|
await downloadsRoutes(instance, container);
|
|
34
|
+
await eventsRoutes(instance, container);
|
|
31
35
|
await bundlesRoutes(instance, container);
|
|
32
36
|
await memberRoutes(instance, container);
|
|
33
37
|
await discussionRoutes(instance, container);
|
|
@@ -42,6 +46,11 @@ function registerRoutes(app, container) {
|
|
|
42
46
|
await integrationsRoutes(instance, container);
|
|
43
47
|
await accessRoutes(instance, container);
|
|
44
48
|
});
|
|
49
|
+
const platform = container.platform;
|
|
50
|
+
if (platform) app.register(async (instance) => {
|
|
51
|
+
registerPlatformAuth(instance, platform);
|
|
52
|
+
await platformRoutes(instance, container);
|
|
53
|
+
});
|
|
45
54
|
}
|
|
46
55
|
//#endregion
|
|
47
56
|
export { registerRoutes };
|