@headless-lms/server 0.1.0 → 0.2.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 +1 -1
- package/dist/app/container.d.ts +7 -3
- package/dist/app/container.js +57 -26
- package/dist/app/container.js.map +1 -1
- package/dist/app/notifications.js +4 -1
- package/dist/app/notifications.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +1 -1
- package/dist/http/plugins/error-handler.js +15 -4
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +25 -22
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/routes/activities.js +34 -2
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +4 -0
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +32 -8
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.d.ts +7 -0
- package/dist/http/routes/bundles.js +170 -0
- package/dist/http/routes/bundles.js.map +1 -0
- package/dist/http/routes/course-analytics.d.ts +7 -0
- package/dist/http/routes/course-analytics.js +58 -0
- package/dist/http/routes/course-analytics.js.map +1 -0
- package/dist/http/routes/courses.js +8 -8
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +2 -2
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +1 -1
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +10 -10
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/learn/assets.js +1 -1
- package/dist/http/routes/learn/assets.js.map +1 -1
- package/dist/http/routes/learn/comments.js +7 -7
- package/dist/http/routes/learn/comments.js.map +1 -1
- package/dist/http/routes/learn/courses.js +26 -3
- package/dist/http/routes/learn/courses.js.map +1 -1
- package/dist/http/routes/learn/downloads.js +3 -3
- package/dist/http/routes/learn/downloads.js.map +1 -1
- package/dist/http/routes/learn/org.js +2 -2
- package/dist/http/routes/learn/org.js.map +1 -1
- package/dist/http/routes/learn/progress.js +2 -2
- package/dist/http/routes/learn/progress.js.map +1 -1
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/students.js +27 -6
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.d.ts +7 -0
- package/dist/http/routes/webhooks.js +159 -0
- package/dist/http/routes/webhooks.js.map +1 -0
- package/dist/http/routes.js +6 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/activities.d.ts +34 -1
- package/dist/http/schemas/activities.js +7 -2
- package/dist/http/schemas/activities.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +11 -7
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +75 -8
- package/dist/http/schemas/automations.js +13 -2
- package/dist/http/schemas/automations.js.map +1 -1
- package/dist/http/schemas/bundles.d.ts +63 -0
- package/dist/http/schemas/bundles.js +23 -0
- package/dist/http/schemas/bundles.js.map +1 -0
- package/dist/http/schemas/content.d.ts +18 -11
- package/dist/http/schemas/content.js +1 -2
- package/dist/http/schemas/content.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +29 -0
- package/dist/http/schemas/course-analytics.js +21 -0
- package/dist/http/schemas/course-analytics.js.map +1 -0
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +9 -9
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/downloads.d.ts +2 -0
- package/dist/http/schemas/downloads.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +14 -6
- package/dist/http/schemas/entitlements.js +4 -2
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/index.d.ts +6 -4
- package/dist/http/schemas/index.js +6 -4
- package/dist/http/schemas/integrations.d.ts +4 -4
- package/dist/http/schemas/integrations.js.map +1 -1
- package/dist/http/schemas/invites.js +2 -1
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/learn.d.ts +11 -1
- package/dist/http/schemas/learn.js.map +1 -1
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/organizations.js.map +1 -1
- package/dist/http/schemas/settings.js.map +1 -1
- package/dist/http/schemas/shared.d.ts +1 -0
- package/dist/http/schemas/shared.js +3 -1
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +30 -2
- package/dist/http/schemas/students.js +21 -2
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/server.js +4 -2
- package/dist/http/server.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org.js","names":[],"sources":["../../../../src/http/routes/learn/org.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { LearnOrg, LearnViewer } from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { UnauthorizedError } from '../../plugins/auth.js';\n\nexport async function learnOrgRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/learn/org',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnOrg',\n tags: ['
|
|
1
|
+
{"version":3,"file":"org.js","names":[],"sources":["../../../../src/http/routes/learn/org.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { LearnOrg, LearnViewer } from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { UnauthorizedError } from '../../plugins/auth.js';\n\nexport async function learnOrgRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/learn/org',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnOrg',\n tags: ['Organizations'],\n summary: \"Get the portal org's public identity (branding)\",\n response: { 200: LearnOrg },\n },\n handler: async (req) => {\n // The session's org (from `activeOrganizationId`) — surface its display\n // identity for the portal brand.\n const org = await container.organizations.getById(req.orgId);\n if (!org) {\n throw new UnauthorizedError('session organization not found');\n }\n return { id: org.id, name: org.name, slug: org.slug };\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/learn/viewer',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnViewer',\n tags: ['Organizations'],\n summary: \"Get the caller's identity within the session's org\",\n response: { 200: LearnViewer },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return { orgUserId: orgUser.id, role: orgUser.role };\n },\n });\n}\n"],"mappings":";;;;AAMA,eAAsB,eAAe,KAAsB,WAAqC;CAC9F,MAAM,IAAI,IAAI,iBAAkC;CAEhD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,UAAU,EAAE,KAAK,SAAS;EAC5B;EACA,SAAS,OAAO,QAAQ;GAGtB,MAAM,MAAM,MAAM,UAAU,cAAc,QAAQ,IAAI,KAAK;GAC3D,IAAI,CAAC,KACH,MAAM,IAAI,kBAAkB,gCAAgC;GAE9D,OAAO;IAAE,IAAI,IAAI;IAAI,MAAM,IAAI;IAAM,MAAM,IAAI;GAAK;EACtD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,UAAU,EAAE,KAAK,YAAY;EAC/B;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,OAAO;IAAE,WAAW,QAAQ;IAAI,MAAM,QAAQ;GAAK;EACrD;CACF,CAAC;AACH"}
|
|
@@ -13,7 +13,7 @@ async function learnProgressRoutes(app, container) {
|
|
|
13
13
|
preHandler: app.requireOrgSession,
|
|
14
14
|
schema: {
|
|
15
15
|
operationId: "reportProgress",
|
|
16
|
-
tags: ["
|
|
16
|
+
tags: ["Progress"],
|
|
17
17
|
summary: "Report usage on a target; the progress service decides completion",
|
|
18
18
|
body: ReportProgress,
|
|
19
19
|
response: {
|
|
@@ -40,7 +40,7 @@ async function learnProgressRoutes(app, container) {
|
|
|
40
40
|
preHandler: app.requireOrgSession,
|
|
41
41
|
schema: {
|
|
42
42
|
operationId: "getLearnCourseProgress",
|
|
43
|
-
tags: ["
|
|
43
|
+
tags: ["Progress"],
|
|
44
44
|
summary: "The student's progress in one course",
|
|
45
45
|
params: LearnCourseIdParam,
|
|
46
46
|
response: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress.js","names":[],"sources":["../../../../src/http/routes/learn/progress.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n CourseProgress,\n ErrorBody,\n LearnCourseIdParam,\n ProgressStatus,\n ReportProgress,\n} from '../../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../../app/container.js';\nimport { UnauthorizedError } from '../../plugins/auth.js';\n\nexport async function learnProgressRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const learn = container.reporting.learn;\n\n r.route({\n method: 'POST',\n url: '/api/learn/progress',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportProgress',\n tags: ['
|
|
1
|
+
{"version":3,"file":"progress.js","names":[],"sources":["../../../../src/http/routes/learn/progress.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n CourseProgress,\n ErrorBody,\n LearnCourseIdParam,\n ProgressStatus,\n ReportProgress,\n} from '../../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../../app/container.js';\nimport { UnauthorizedError } from '../../plugins/auth.js';\n\nexport async function learnProgressRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const learn = container.reporting.learn;\n\n r.route({\n method: 'POST',\n url: '/api/learn/progress',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportProgress',\n tags: ['Progress'],\n summary: 'Report usage on a target; the progress service decides completion',\n body: ReportProgress,\n response: { 200: ProgressStatus, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n // The activity carries its course, then the same enrollment gate as every\n // Learn read.\n const activity = await container.content.getActivity(req.orgId, req.body.activity);\n if (!activity) {\n throw new NotFoundError('Activity', req.body.activity);\n }\n const course = await learn.getCourse(req.orgId, orgUser.id, activity.courseId);\n if (!course) {\n throw new NotFoundError('Activity', req.body.activity);\n }\n const record = await container.progress.report(req.orgId, {\n orgUserId: orgUser.id,\n activityId: req.body.activity,\n reports: req.body.reports,\n });\n return { status: record.completedAt ? ('completed' as const) : ('in-progress' as const) };\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/learn/courses/:courseId/progress',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnCourseProgress',\n tags: ['Progress'],\n summary: \"The student's progress in one course\",\n params: LearnCourseIdParam,\n response: { 200: CourseProgress, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n const view = await learn.courseProgress(req.orgId, orgUser.id, req.params.courseId);\n if (!view) {\n throw new NotFoundError('Course', req.params.courseId);\n }\n return view;\n },\n });\n}\n"],"mappings":";;;;;;AAaA,eAAsB,oBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,QAAQ,UAAU,UAAU;CAElC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,UAAU;GACjB,SAAS;GACT,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,KAAK;GAAU;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAI9B,MAAM,WAAW,MAAM,UAAU,QAAQ,YAAY,IAAI,OAAO,IAAI,KAAK,QAAQ;GACjF,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,YAAY,IAAI,KAAK,QAAQ;GAGvD,IAAI,CAAC,MADgB,MAAM,UAAU,IAAI,OAAO,QAAQ,IAAI,SAAS,QAAQ,GAE3E,MAAM,IAAI,cAAc,YAAY,IAAI,KAAK,QAAQ;GAOvD,OAAO,EAAE,SAAQ,MALI,UAAU,SAAS,OAAO,IAAI,OAAO;IACxD,WAAW,QAAQ;IACnB,YAAY,IAAI,KAAK;IACrB,SAAS,IAAI,KAAK;GACpB,CAAC,EAAA,CACuB,cAAe,cAAyB,cAAwB;EAC1F;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,UAAU;GACjB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,KAAK;GAAU;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,MAAM,OAAO,MAAM,MAAM,eAAe,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,QAAQ;GAClF,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,UAAU,IAAI,OAAO,QAAQ;GAEvD,OAAO;EACT;CACF,CAAC;AACH"}
|
|
@@ -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 ErrorBody,\n Invite,\n Member,\n MemberIdParam,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n Organization,\n UpdateMemberRole,\n UpdateOrganization,\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 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',\n body: UpdateOrganization,\n response: { 200: Organization, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const org = await container.orgProvider.updateOrganization(req.headers, scope.authOrgId, req.body);\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 organization members',\n querystring: MembersQuery,\n response: { 200: MembersPage },\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\",\n params: MemberIdParam,\n body: UpdateMemberRole,\n response: { 200: Member, 404: ErrorBody, 409: ErrorBody },\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(ctx, req.params.id, req.body.role);\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',\n params: MemberIdParam,\n response: { 204: z.void(), 404: ErrorBody, 409: ErrorBody },\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(ctx, req.params.id);\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',\n body: CreateInvite,\n response: { 201: Invite, 409: ErrorBody },\n },\n handler: async (req) => {\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 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 req.log.debug({ invite }, 'created invite');\n return invite;\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 params: InviteTokenParam,\n response: { 200: GetInviteResult, 404: ErrorBody },\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 logged-in account',\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, 400: ErrorBody },\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 CreateInvite,\n CreateOrganization,\n ErrorBody,\n Invite,\n Member,\n MemberIdParam,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n Organization,\n UpdateMemberRole,\n UpdateOrganization,\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 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',\n body: UpdateOrganization,\n response: { 200: Organization, 409: ErrorBody },\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 organization members',\n querystring: MembersQuery,\n response: { 200: MembersPage },\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\",\n params: MemberIdParam,\n body: UpdateMemberRole,\n response: { 200: Member, 404: ErrorBody, 409: ErrorBody },\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(ctx, req.params.id, req.body.role);\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',\n params: MemberIdParam,\n response: { 204: z.void(), 404: ErrorBody, 409: ErrorBody },\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(ctx, req.params.id);\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',\n body: CreateInvite,\n response: { 201: Invite, 409: ErrorBody },\n },\n handler: async (req) => {\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 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 req.log.debug({ invite }, 'created invite');\n return invite;\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 params: InviteTokenParam,\n response: { 200: GetInviteResult, 404: ErrorBody },\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 logged-in account',\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, 400: ErrorBody },\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,6 +1,6 @@
|
|
|
1
1
|
import { resolveScope } from "../scope.js";
|
|
2
2
|
import { ErrorBody } from "../schemas/shared.js";
|
|
3
|
-
import { Student, StudentIdParam, StudentsPage, StudentsQuery, UpdateStudentBody } from "../schemas/students.js";
|
|
3
|
+
import { Student, StudentAnalytics, StudentIdParam, StudentsPage, StudentsQuery, UpdateStudentBody } from "../schemas/students.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -14,7 +14,7 @@ async function studentsRoutes(app, container) {
|
|
|
14
14
|
preHandler: app.requireOrgSession,
|
|
15
15
|
schema: {
|
|
16
16
|
operationId: "listStudents",
|
|
17
|
-
tags: ["
|
|
17
|
+
tags: ["Organizations"],
|
|
18
18
|
summary: "List students",
|
|
19
19
|
querystring: StudentsQuery,
|
|
20
20
|
response: { 200: StudentsPage }
|
|
@@ -30,7 +30,7 @@ async function studentsRoutes(app, container) {
|
|
|
30
30
|
preHandler: app.requireOrgSession,
|
|
31
31
|
schema: {
|
|
32
32
|
operationId: "getStudent",
|
|
33
|
-
tags: ["
|
|
33
|
+
tags: ["Organizations"],
|
|
34
34
|
summary: "Get a student by id",
|
|
35
35
|
params: StudentIdParam,
|
|
36
36
|
response: {
|
|
@@ -45,13 +45,34 @@ async function studentsRoutes(app, container) {
|
|
|
45
45
|
return student;
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
r.route({
|
|
49
|
+
method: "GET",
|
|
50
|
+
url: "/api/students/:id/analytics",
|
|
51
|
+
preHandler: app.requireOrgSession,
|
|
52
|
+
schema: {
|
|
53
|
+
operationId: "getStudentAnalytics",
|
|
54
|
+
tags: ["Reporting"],
|
|
55
|
+
summary: "A student's learner record: per-course progress and completion",
|
|
56
|
+
params: StudentIdParam,
|
|
57
|
+
response: {
|
|
58
|
+
200: StudentAnalytics,
|
|
59
|
+
404: ErrorBody
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
handler: async (req) => {
|
|
63
|
+
const scope = await resolveScope(container, req);
|
|
64
|
+
const analytics = await students.analytics(scope.orgId, req.params.id);
|
|
65
|
+
if (!analytics) throw new NotFoundError("Student", req.params.id);
|
|
66
|
+
return analytics;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
48
69
|
r.route({
|
|
49
70
|
method: "PATCH",
|
|
50
71
|
url: "/api/students/:id",
|
|
51
72
|
preHandler: app.requireOrgSession,
|
|
52
73
|
schema: {
|
|
53
74
|
operationId: "updateStudent",
|
|
54
|
-
tags: ["
|
|
75
|
+
tags: ["Organizations"],
|
|
55
76
|
summary: "Update a student's profile",
|
|
56
77
|
params: StudentIdParam,
|
|
57
78
|
body: UpdateStudentBody,
|
|
@@ -79,7 +100,7 @@ async function studentsRoutes(app, container) {
|
|
|
79
100
|
preHandler: app.requireOrgSession,
|
|
80
101
|
schema: {
|
|
81
102
|
operationId: "resendStudentInvite",
|
|
82
|
-
tags: ["
|
|
103
|
+
tags: ["Organizations"],
|
|
83
104
|
summary: "Re-send a pending student's invite email",
|
|
84
105
|
params: StudentIdParam,
|
|
85
106
|
response: {
|
|
@@ -104,7 +125,7 @@ async function studentsRoutes(app, container) {
|
|
|
104
125
|
preHandler: app.requireOrgSession,
|
|
105
126
|
schema: {
|
|
106
127
|
operationId: "deleteStudent",
|
|
107
|
-
tags: ["
|
|
128
|
+
tags: ["Organizations"],
|
|
108
129
|
summary: "Delete a student",
|
|
109
130
|
params: StudentIdParam,
|
|
110
131
|
response: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["// HTTP routes for students: the reporting-backed list/read endpoints, the\n// delete, and re-issuing a pending invite. There is no create here — adding a\n// student IS inviting one, at /api/organizations/invites, which provisions the\n// student record up front.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n ErrorBody,\n Student,\n StudentIdParam,\n StudentsPage,\n StudentsQuery,\n UpdateStudentBody,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } 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.students;\n\n r.route({\n method: 'GET',\n url: '/api/students',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listStudents',\n tags: ['
|
|
1
|
+
{"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["// HTTP routes for students: the reporting-backed list/read endpoints, the\n// delete, and re-issuing a pending invite. There is no create here — adding a\n// student IS inviting one, at /api/organizations/invites, which provisions the\n// student record up front.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n ErrorBody,\n Student,\n StudentAnalytics,\n StudentIdParam,\n StudentsPage,\n StudentsQuery,\n UpdateStudentBody,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } 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.students;\n\n r.route({\n method: 'GET',\n url: '/api/students',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listStudents',\n tags: ['Organizations'],\n summary: 'List students',\n querystring: StudentsQuery,\n response: { 200: StudentsPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return students.list(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudent',\n tags: ['Organizations'],\n summary: 'Get a student by id',\n params: StudentIdParam,\n response: { 200: Student, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const student = await students.get(scope.orgId, req.params.id);\n if (!student) {\n throw new NotFoundError('Student', req.params.id);\n }\n return student;\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 student's learner record: per-course progress and completion\",\n params: StudentIdParam,\n response: { 200: StudentAnalytics, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const analytics = await students.analytics(scope.orgId, req.params.id);\n if (!analytics) {\n throw new NotFoundError('Student', req.params.id);\n }\n return analytics;\n },\n });\n\n // Corrects the person behind the student row. The write lands in identity\n // (the person is global); organizations owns the rule that this org user is\n // a student here, and reconciles their pending invite when the address moves.\n r.route({\n method: 'PATCH',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateStudent',\n tags: ['Organizations'],\n summary: \"Update a student's profile\",\n params: StudentIdParam,\n body: UpdateStudentBody,\n response: { 200: Student, 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n await container.organizations.updateStudent({\n orgId: scope.orgId,\n orgUserId: req.params.id,\n ...req.body,\n });\n // Re-read through the report so the response is the same shape the list\n // and detail GETs return, aggregates included.\n const student = await students.get(scope.orgId, req.params.id);\n if (!student) {\n throw new NotFoundError('Student', req.params.id);\n }\n return student;\n },\n });\n\n // Re-mints the token on the student's pending invite and emails it. Lives\n // here rather than under invites because the admin app knows the student,\n // not the invitation.\n r.route({\n method: 'POST',\n url: '/api/students/:id/invite/resend',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resendStudentInvite',\n tags: ['Organizations'],\n summary: \"Re-send a pending student's invite email\",\n params: StudentIdParam,\n response: { 204: z.void(), 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.resendStudentInvite({\n orgId: scope.orgId,\n orgUserId: req.params.id,\n inviterUserId: scope.userId,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteStudent',\n tags: ['Organizations'],\n summary: 'Delete a student',\n params: StudentIdParam,\n response: { 204: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.deleteOrgUser(scope.orgId, req.params.id);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Container } from "../../app/container.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/routes/webhooks.d.ts
|
|
4
|
+
declare function webhooksRoutes(app: FastifyInstance, container: Container): Promise<void>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { webhooksRoutes };
|
|
7
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { resolveScope } from "../scope.js";
|
|
2
|
+
import { ErrorBody } from "../schemas/shared.js";
|
|
3
|
+
import { AutomationIdParam, CreateWebhookBody, CreatedWebhook, UpdateWebhookBody, Webhook, WebhookSecret } from "../schemas/automations.js";
|
|
4
|
+
import "../schemas/index.js";
|
|
5
|
+
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/http/routes/webhooks.ts
|
|
8
|
+
const DELIVERY_DESCRIPTION = "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
|
+
async function webhooksRoutes(app, container) {
|
|
10
|
+
const r = app.withTypeProvider();
|
|
11
|
+
const automations = container.automations;
|
|
12
|
+
const tags = ["Automations"];
|
|
13
|
+
r.route({
|
|
14
|
+
method: "GET",
|
|
15
|
+
url: "/api/automations/webhooks",
|
|
16
|
+
preHandler: app.requireOrgSession,
|
|
17
|
+
schema: {
|
|
18
|
+
operationId: "listWebhooks",
|
|
19
|
+
tags,
|
|
20
|
+
summary: "List the organization's webhooks",
|
|
21
|
+
response: { 200: z.array(Webhook) }
|
|
22
|
+
},
|
|
23
|
+
handler: async (req) => {
|
|
24
|
+
const scope = await resolveScope(container, req);
|
|
25
|
+
return automations.listWebhooks(scope.orgId);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
r.route({
|
|
29
|
+
method: "POST",
|
|
30
|
+
url: "/api/automations/webhooks",
|
|
31
|
+
preHandler: app.requireOrgSession,
|
|
32
|
+
schema: {
|
|
33
|
+
operationId: "createWebhook",
|
|
34
|
+
tags,
|
|
35
|
+
summary: "Create a webhook — returns its signing secret",
|
|
36
|
+
description: DELIVERY_DESCRIPTION,
|
|
37
|
+
body: CreateWebhookBody,
|
|
38
|
+
response: {
|
|
39
|
+
201: CreatedWebhook,
|
|
40
|
+
400: ErrorBody
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
handler: async (req, reply) => {
|
|
44
|
+
const scope = await resolveScope(container, req);
|
|
45
|
+
const webhook = await automations.createWebhook(scope.orgId, req.body);
|
|
46
|
+
return reply.code(201).send(webhook);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
r.route({
|
|
50
|
+
method: "GET",
|
|
51
|
+
url: "/api/automations/webhooks/:id",
|
|
52
|
+
preHandler: app.requireOrgSession,
|
|
53
|
+
schema: {
|
|
54
|
+
operationId: "getWebhook",
|
|
55
|
+
tags,
|
|
56
|
+
summary: "Get a webhook by id",
|
|
57
|
+
params: AutomationIdParam,
|
|
58
|
+
response: {
|
|
59
|
+
200: Webhook,
|
|
60
|
+
404: ErrorBody
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
handler: async (req) => {
|
|
64
|
+
const scope = await resolveScope(container, req);
|
|
65
|
+
const webhook = await automations.getWebhook(scope.orgId, req.params.id);
|
|
66
|
+
if (!webhook) throw new NotFoundError("Webhook", req.params.id);
|
|
67
|
+
return webhook;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
r.route({
|
|
71
|
+
method: "PATCH",
|
|
72
|
+
url: "/api/automations/webhooks/:id",
|
|
73
|
+
preHandler: app.requireOrgSession,
|
|
74
|
+
schema: {
|
|
75
|
+
operationId: "updateWebhook",
|
|
76
|
+
tags,
|
|
77
|
+
summary: "Change a webhook's URL, events, description or enabled flag",
|
|
78
|
+
params: AutomationIdParam,
|
|
79
|
+
body: UpdateWebhookBody,
|
|
80
|
+
response: {
|
|
81
|
+
200: Webhook,
|
|
82
|
+
400: ErrorBody,
|
|
83
|
+
404: ErrorBody
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
handler: async (req) => {
|
|
87
|
+
const scope = await resolveScope(container, req);
|
|
88
|
+
const webhook = await automations.updateWebhook(scope.orgId, req.params.id, req.body);
|
|
89
|
+
if (!webhook) throw new NotFoundError("Webhook", req.params.id);
|
|
90
|
+
return webhook;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
r.route({
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
url: "/api/automations/webhooks/:id",
|
|
96
|
+
preHandler: app.requireOrgSession,
|
|
97
|
+
schema: {
|
|
98
|
+
operationId: "deleteWebhook",
|
|
99
|
+
tags,
|
|
100
|
+
summary: "Delete a webhook and its signing secret — its delivery history is kept",
|
|
101
|
+
params: AutomationIdParam,
|
|
102
|
+
response: {
|
|
103
|
+
204: z.void(),
|
|
104
|
+
404: ErrorBody
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
handler: async (req, reply) => {
|
|
108
|
+
const scope = await resolveScope(container, req);
|
|
109
|
+
if (!await automations.deleteWebhook(scope.orgId, req.params.id)) throw new NotFoundError("Webhook", req.params.id);
|
|
110
|
+
return reply.code(204).send();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
r.route({
|
|
114
|
+
method: "GET",
|
|
115
|
+
url: "/api/automations/webhooks/:id/secret",
|
|
116
|
+
preHandler: app.requireOrgSession,
|
|
117
|
+
schema: {
|
|
118
|
+
operationId: "getWebhookSecret",
|
|
119
|
+
tags,
|
|
120
|
+
summary: "Reveal a webhook's signing secret",
|
|
121
|
+
params: AutomationIdParam,
|
|
122
|
+
response: {
|
|
123
|
+
200: WebhookSecret,
|
|
124
|
+
404: ErrorBody
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
handler: async (req) => {
|
|
128
|
+
const scope = await resolveScope(container, req);
|
|
129
|
+
const secret = await automations.revealWebhookSecret(scope.orgId, req.params.id);
|
|
130
|
+
if (!secret) throw new NotFoundError("Webhook", req.params.id);
|
|
131
|
+
return secret;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
r.route({
|
|
135
|
+
method: "POST",
|
|
136
|
+
url: "/api/automations/webhooks/:id/secret/rotate",
|
|
137
|
+
preHandler: app.requireOrgSession,
|
|
138
|
+
schema: {
|
|
139
|
+
operationId: "rotateWebhookSecret",
|
|
140
|
+
tags,
|
|
141
|
+
summary: "Replace a webhook's signing secret — the old one stops working immediately",
|
|
142
|
+
params: AutomationIdParam,
|
|
143
|
+
response: {
|
|
144
|
+
200: WebhookSecret,
|
|
145
|
+
404: ErrorBody
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
handler: async (req) => {
|
|
149
|
+
const scope = await resolveScope(container, req);
|
|
150
|
+
const secret = await automations.rotateWebhookSecret(scope.orgId, req.params.id);
|
|
151
|
+
if (!secret) throw new NotFoundError("Webhook", req.params.id);
|
|
152
|
+
return secret;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { webhooksRoutes };
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +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 ErrorBody,\n UpdateWebhookBody,\n Webhook,\n WebhookSecret,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nconst DELIVERY_DESCRIPTION =\n \"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 response: { 200: z.array(Webhook) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.listWebhooks(scope.orgId);\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 — returns its signing secret',\n description: DELIVERY_DESCRIPTION,\n body: CreateWebhookBody,\n response: { 201: CreatedWebhook, 400: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const webhook = await automations.createWebhook(scope.orgId, req.body);\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 by id',\n params: AutomationIdParam,\n response: { 200: Webhook, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const webhook = await automations.getWebhook(scope.orgId, req.params.id);\n if (!webhook) {\n throw new NotFoundError('Webhook', req.params.id);\n }\n return webhook;\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 params: AutomationIdParam,\n body: UpdateWebhookBody,\n response: { 200: Webhook, 400: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const webhook = await automations.updateWebhook(scope.orgId, req.params.id, req.body);\n if (!webhook) {\n throw new NotFoundError('Webhook', req.params.id);\n }\n return webhook;\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 — its delivery history is kept',\n params: AutomationIdParam,\n response: { 204: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const deleted = await automations.deleteWebhook(scope.orgId, req.params.id);\n if (!deleted) {\n throw new NotFoundError('Webhook', req.params.id);\n }\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 params: AutomationIdParam,\n response: { 200: WebhookSecret, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const secret = await automations.revealWebhookSecret(scope.orgId, req.params.id);\n if (!secret) {\n throw new NotFoundError('Webhook', req.params.id);\n }\n return secret;\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 — the old one stops working immediately\",\n params: AutomationIdParam,\n response: { 200: WebhookSecret, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const secret = await automations.rotateWebhookSecret(scope.orgId, req.params.id);\n if (!secret) {\n throw new NotFoundError('Webhook', req.params.id);\n }\n return secret;\n },\n });\n}\n"],"mappings":"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"}
|
package/dist/http/routes.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { coursesRoutes } from "./routes/courses.js";
|
|
2
2
|
import { downloadsRoutes } from "./routes/downloads.js";
|
|
3
|
+
import { bundlesRoutes } from "./routes/bundles.js";
|
|
3
4
|
import { learnRoutes } from "./routes/learn/index.js";
|
|
4
5
|
import { discussionRoutes } from "./routes/discussion.js";
|
|
5
6
|
import { activitiesRoutes } from "./routes/activities.js";
|
|
6
7
|
import { studentsRoutes } from "./routes/students.js";
|
|
7
8
|
import { entitlementsRoutes } from "./routes/entitlements.js";
|
|
8
9
|
import { automationsRoutes } from "./routes/automations.js";
|
|
10
|
+
import { webhooksRoutes } from "./routes/webhooks.js";
|
|
9
11
|
import { organizationsRoutes } from "./routes/organizations.js";
|
|
10
12
|
import { dashboardRoutes } from "./routes/dashboard.js";
|
|
13
|
+
import { courseAnalyticsRoutes } from "./routes/course-analytics.js";
|
|
11
14
|
import { assetsRoutes } from "./routes/assets.js";
|
|
12
15
|
import { integrationsRoutes } from "./routes/integrations.js";
|
|
13
16
|
//#region src/http/routes.ts
|
|
@@ -20,13 +23,16 @@ function registerRoutes(app, container) {
|
|
|
20
23
|
instance.addHook("onRequest", instance.requireOrgSession);
|
|
21
24
|
await coursesRoutes(instance, container);
|
|
22
25
|
await downloadsRoutes(instance, container);
|
|
26
|
+
await bundlesRoutes(instance, container);
|
|
23
27
|
await learnRoutes(instance, container);
|
|
24
28
|
await discussionRoutes(instance, container);
|
|
25
29
|
await activitiesRoutes(instance, container);
|
|
26
30
|
await studentsRoutes(instance, container);
|
|
27
31
|
await entitlementsRoutes(instance, container);
|
|
28
32
|
await automationsRoutes(instance, container);
|
|
33
|
+
await webhooksRoutes(instance, container);
|
|
29
34
|
await dashboardRoutes(instance, container);
|
|
35
|
+
await courseAnalyticsRoutes(instance, container);
|
|
30
36
|
await assetsRoutes(instance, container);
|
|
31
37
|
await integrationsRoutes(instance, container);
|
|
32
38
|
});
|
package/dist/http/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","names":[],"sources":["../../src/http/routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { Container } from '../app/container.js';\nimport { coursesRoutes } from './routes/courses.js';\nimport { downloadsRoutes } from './routes/downloads.js';\nimport { learnRoutes } from './routes/learn/index.js';\nimport { discussionRoutes } from './routes/discussion.js';\nimport { activitiesRoutes } from './routes/activities.js';\nimport { studentsRoutes } from './routes/students.js';\nimport { entitlementsRoutes } from './routes/entitlements.js';\nimport { automationsRoutes } from './routes/automations.js';\nimport { organizationsRoutes } from './routes/organizations.js';\nimport { dashboardRoutes } from './routes/dashboard.js';\nimport { assetsRoutes } from './routes/assets.js';\nimport { integrationsRoutes } from './routes/integrations.js';\n\nexport function registerRoutes(
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/http/routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { Container } from '../app/container.js';\nimport { coursesRoutes } from './routes/courses.js';\nimport { downloadsRoutes } from './routes/downloads.js';\nimport { bundlesRoutes } from './routes/bundles.js';\nimport { learnRoutes } from './routes/learn/index.js';\nimport { discussionRoutes } from './routes/discussion.js';\nimport { activitiesRoutes } from './routes/activities.js';\nimport { studentsRoutes } from './routes/students.js';\nimport { entitlementsRoutes } from './routes/entitlements.js';\nimport { automationsRoutes } from './routes/automations.js';\nimport { webhooksRoutes } from './routes/webhooks.js';\nimport { organizationsRoutes } from './routes/organizations.js';\nimport { dashboardRoutes } from './routes/dashboard.js';\nimport { courseAnalyticsRoutes } from './routes/course-analytics.js';\nimport { assetsRoutes } from './routes/assets.js';\nimport { integrationsRoutes } from './routes/integrations.js';\n\nexport function registerRoutes(app: FastifyInstance, container: Container): void {\n app.get('/health', async () => ({ status: 'ok' }));\n\n app.register(async (instance) => {\n // instance.addHook('onRequest', instance.requireSession);\n await organizationsRoutes(instance, container);\n });\n\n app.register(async (instance) => {\n instance.addHook('onRequest', instance.requireOrgSession);\n await coursesRoutes(instance, container);\n await downloadsRoutes(instance, container);\n await bundlesRoutes(instance, container);\n await learnRoutes(instance, container);\n await discussionRoutes(instance, container);\n await activitiesRoutes(instance, container);\n await studentsRoutes(instance, container);\n await entitlementsRoutes(instance, container);\n await automationsRoutes(instance, container);\n await webhooksRoutes(instance, container);\n await dashboardRoutes(instance, container);\n await courseAnalyticsRoutes(instance, container);\n await assetsRoutes(instance, container);\n await integrationsRoutes(instance, container);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAgB,eAAe,KAAsB,WAA4B;CAC/E,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,KAAK,EAAE;CAEjD,IAAI,SAAS,OAAO,aAAa;EAE/B,MAAM,oBAAoB,UAAU,SAAS;CAC/C,CAAC;CAED,IAAI,SAAS,OAAO,aAAa;EAC/B,SAAS,QAAQ,aAAa,SAAS,iBAAiB;EACxD,MAAM,cAAc,UAAU,SAAS;EACvC,MAAM,gBAAgB,UAAU,SAAS;EACzC,MAAM,cAAc,UAAU,SAAS;EACvC,MAAM,YAAY,UAAU,SAAS;EACrC,MAAM,iBAAiB,UAAU,SAAS;EAC1C,MAAM,iBAAiB,UAAU,SAAS;EAC1C,MAAM,eAAe,UAAU,SAAS;EACxC,MAAM,mBAAmB,UAAU,SAAS;EAC5C,MAAM,kBAAkB,UAAU,SAAS;EAC3C,MAAM,eAAe,UAAU,SAAS;EACxC,MAAM,gBAAgB,UAAU,SAAS;EACzC,MAAM,sBAAsB,UAAU,SAAS;EAC/C,MAAM,aAAa,UAAU,SAAS;EACtC,MAAM,mBAAmB,UAAU,SAAS;CAC9C,CAAC;AACH"}
|
|
@@ -11,6 +11,16 @@ declare const Activity: z.ZodObject<{
|
|
|
11
11
|
updatedAt: z.ZodCoercedDate<unknown>;
|
|
12
12
|
}, z.core.$strict>;
|
|
13
13
|
type Activity = z.infer<typeof Activity>;
|
|
14
|
+
declare const ActivityAsset: z.ZodObject<{
|
|
15
|
+
orgId: z.ZodString;
|
|
16
|
+
id: z.ZodString;
|
|
17
|
+
activityId: z.ZodString;
|
|
18
|
+
assetId: z.ZodString;
|
|
19
|
+
seq: z.ZodNumber;
|
|
20
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
21
|
+
updatedAt: z.ZodCoercedDate<unknown>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
type ActivityAsset = z.infer<typeof ActivityAsset>;
|
|
14
24
|
declare const Module: z.ZodObject<{
|
|
15
25
|
orgId: z.ZodString;
|
|
16
26
|
id: z.ZodString;
|
|
@@ -32,6 +42,29 @@ declare const ModuleList: z.ZodArray<z.ZodObject<{
|
|
|
32
42
|
updatedAt: z.ZodCoercedDate<unknown>;
|
|
33
43
|
}, z.core.$strict>>;
|
|
34
44
|
type ModuleList = z.infer<typeof ModuleList>;
|
|
45
|
+
/** A course's activities as bare rows, ordered by (moduleId, seq). */
|
|
46
|
+
declare const ActivityList: z.ZodArray<z.ZodObject<{
|
|
47
|
+
orgId: z.ZodString;
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
moduleId: z.ZodString;
|
|
50
|
+
courseId: z.ZodString;
|
|
51
|
+
seq: z.ZodNumber;
|
|
52
|
+
settings: z.ZodNullable<z.ZodType<import("@headless-lms/core/types").JsonValue, unknown, z.core.$ZodTypeInternals<import("@headless-lms/core/types").JsonValue, unknown>>>;
|
|
53
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
54
|
+
updatedAt: z.ZodCoercedDate<unknown>;
|
|
55
|
+
}, z.core.$strict>>;
|
|
56
|
+
type ActivityList = z.infer<typeof ActivityList>;
|
|
57
|
+
/** A course's activity→asset link rows, ordered by (activityId, seq). */
|
|
58
|
+
declare const ActivityAssetList: z.ZodArray<z.ZodObject<{
|
|
59
|
+
orgId: z.ZodString;
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
activityId: z.ZodString;
|
|
62
|
+
assetId: z.ZodString;
|
|
63
|
+
seq: z.ZodNumber;
|
|
64
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
65
|
+
updatedAt: z.ZodCoercedDate<unknown>;
|
|
66
|
+
}, z.core.$strict>>;
|
|
67
|
+
type ActivityAssetList = z.infer<typeof ActivityAssetList>;
|
|
35
68
|
declare const CourseIdPathParam: z.ZodObject<{
|
|
36
69
|
courseId: z.ZodString;
|
|
37
70
|
}, z.core.$strip>;
|
|
@@ -65,5 +98,5 @@ declare const SaveActivity: z.ZodObject<{
|
|
|
65
98
|
}, z.core.$strict>;
|
|
66
99
|
type SaveActivity = z.infer<typeof SaveActivity>;
|
|
67
100
|
//#endregion
|
|
68
|
-
export { Activity, ActivityPathParam, CourseIdPathParam, CreateModule, Module, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule };
|
|
101
|
+
export { Activity, ActivityAsset, ActivityAssetList, ActivityList, ActivityPathParam, CourseIdPathParam, CreateModule, Module, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule };
|
|
69
102
|
//# sourceMappingURL=activities.d.ts.map
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { activitySchema, moduleSchema, saveActivityInputSchema } from "@headless-lms/core/schemas";
|
|
2
|
+
import { activityAssetSchema, activitySchema, moduleSchema, saveActivityInputSchema } from "@headless-lms/core/schemas";
|
|
3
3
|
//#region src/http/schemas/activities.ts
|
|
4
4
|
const Activity = activitySchema;
|
|
5
|
+
const ActivityAsset = activityAssetSchema;
|
|
5
6
|
const Module = moduleSchema;
|
|
6
7
|
/** Module/activity-write endpoints return the full, reordered module list. */
|
|
7
8
|
const ModuleList = z.array(Module);
|
|
9
|
+
/** A course's activities as bare rows, ordered by (moduleId, seq). */
|
|
10
|
+
const ActivityList = z.array(Activity);
|
|
11
|
+
/** A course's activity→asset link rows, ordered by (activityId, seq). */
|
|
12
|
+
const ActivityAssetList = z.array(ActivityAsset);
|
|
8
13
|
const CourseIdPathParam = z.object({ courseId: z.string() });
|
|
9
14
|
const ModulePathParam = z.object({
|
|
10
15
|
courseId: z.string(),
|
|
@@ -20,6 +25,6 @@ const UpdateModule = z.object({ title: z.string().min(1) });
|
|
|
20
25
|
const ReorderInput = z.object({ orderedIds: z.array(z.string()) });
|
|
21
26
|
const SaveActivity = saveActivityInputSchema;
|
|
22
27
|
//#endregion
|
|
23
|
-
export { Activity, ActivityPathParam, CourseIdPathParam, CreateModule, Module, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule };
|
|
28
|
+
export { Activity, ActivityAsset, ActivityAssetList, ActivityList, ActivityPathParam, CourseIdPathParam, CreateModule, Module, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule };
|
|
24
29
|
|
|
25
30
|
//# sourceMappingURL=activities.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activities.js","names":[],"sources":["../../../src/http/schemas/activities.ts"],"sourcesContent":["// Course modules + activities resource schemas. A module holds an ordered list\n// of activities directly; each activity is a uniform, orderable unit placing an\n// opaque settings blob at a position (`seq`) with a set of linked assets.\nimport { z } from
|
|
1
|
+
{"version":3,"file":"activities.js","names":[],"sources":["../../../src/http/schemas/activities.ts"],"sourcesContent":["// Course modules + activities resource schemas. A module holds an ordered list\n// of activities directly; each activity is a uniform, orderable unit placing an\n// opaque settings blob at a position (`seq`) with a set of linked assets.\nimport { z } from 'zod';\nimport {\n activityAssetSchema,\n activitySchema,\n moduleSchema,\n saveActivityInputSchema,\n} from '@headless-lms/core/schemas';\n\nexport const Activity = activitySchema;\nexport type Activity = z.infer<typeof Activity>;\n\nexport const ActivityAsset = activityAssetSchema;\nexport type ActivityAsset = z.infer<typeof ActivityAsset>;\n\nexport const Module = moduleSchema;\nexport type Module = z.infer<typeof Module>;\n\n/** Module/activity-write endpoints return the full, reordered module list. */\nexport const ModuleList = z.array(Module);\nexport type ModuleList = z.infer<typeof ModuleList>;\n\n/** A course's activities as bare rows, ordered by (moduleId, seq). */\nexport const ActivityList = z.array(Activity);\nexport type ActivityList = z.infer<typeof ActivityList>;\n\n/** A course's activity→asset link rows, ordered by (activityId, seq). */\nexport const ActivityAssetList = z.array(ActivityAsset);\nexport type ActivityAssetList = z.infer<typeof ActivityAssetList>;\n\nexport const CourseIdPathParam = z.object({ courseId: z.string() });\nexport type CourseIdPathParam = z.infer<typeof CourseIdPathParam>;\n\nexport const ModulePathParam = z.object({ courseId: z.string(), moduleId: z.string() });\nexport type ModulePathParam = z.infer<typeof ModulePathParam>;\n\nexport const ActivityPathParam = z.object({\n courseId: z.string(),\n moduleId: z.string(),\n activityId: z.string(),\n});\nexport type ActivityPathParam = z.infer<typeof ActivityPathParam>;\n\nexport const CreateModule = z.object({ title: z.string().min(1) });\nexport type CreateModule = z.infer<typeof CreateModule>;\n\nexport const UpdateModule = z.object({ title: z.string().min(1) });\nexport type UpdateModule = z.infer<typeof UpdateModule>;\n\nexport const ReorderInput = z.object({ orderedIds: z.array(z.string()) });\nexport type ReorderInput = z.infer<typeof ReorderInput>;\n\nexport const SaveActivity = saveActivityInputSchema;\nexport type SaveActivity = z.infer<typeof SaveActivity>;\n"],"mappings":";;;AAWA,MAAa,WAAW;AAGxB,MAAa,gBAAgB;AAG7B,MAAa,SAAS;;AAItB,MAAa,aAAa,EAAE,MAAM,MAAM;;AAIxC,MAAa,eAAe,EAAE,MAAM,QAAQ;;AAI5C,MAAa,oBAAoB,EAAE,MAAM,aAAa;AAGtD,MAAa,oBAAoB,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AAGlE,MAAa,kBAAkB,EAAE,OAAO;CAAE,UAAU,EAAE,OAAO;CAAG,UAAU,EAAE,OAAO;AAAE,CAAC;AAGtF,MAAa,oBAAoB,EAAE,OAAO;CACxC,UAAU,EAAE,OAAO;CACnB,UAAU,EAAE,OAAO;CACnB,YAAY,EAAE,OAAO;AACvB,CAAC;AAGD,MAAa,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAGjE,MAAa,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAGjE,MAAa,eAAe,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAGxE,MAAa,eAAe"}
|