@headless-lms/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/app/container.d.ts +124 -0
  4. package/dist/app/container.js +312 -0
  5. package/dist/app/container.js.map +1 -0
  6. package/dist/app/db.d.ts +2 -0
  7. package/dist/app/db.js +2 -0
  8. package/dist/app/integrations.d.ts +7 -0
  9. package/dist/app/integrations.js +44 -0
  10. package/dist/app/integrations.js.map +1 -0
  11. package/dist/app/notifications.d.ts +7 -0
  12. package/dist/app/notifications.js +23 -0
  13. package/dist/app/notifications.js.map +1 -0
  14. package/dist/http/config.d.ts +25 -0
  15. package/dist/http/config.js +1 -0
  16. package/dist/http/fastify.d.ts +35 -0
  17. package/dist/http/plugins/auth.d.ts +11 -0
  18. package/dist/http/plugins/auth.js +47 -0
  19. package/dist/http/plugins/auth.js.map +1 -0
  20. package/dist/http/plugins/cors.d.ts +7 -0
  21. package/dist/http/plugins/cors.js +22 -0
  22. package/dist/http/plugins/cors.js.map +1 -0
  23. package/dist/http/plugins/error-handler.d.ts +6 -0
  24. package/dist/http/plugins/error-handler.js +64 -0
  25. package/dist/http/plugins/error-handler.js.map +1 -0
  26. package/dist/http/plugins/openapi.d.ts +7 -0
  27. package/dist/http/plugins/openapi.js +63 -0
  28. package/dist/http/plugins/openapi.js.map +1 -0
  29. package/dist/http/routes/activities.d.ts +7 -0
  30. package/dist/http/routes/activities.js +163 -0
  31. package/dist/http/routes/activities.js.map +1 -0
  32. package/dist/http/routes/assets.d.ts +7 -0
  33. package/dist/http/routes/assets.js +168 -0
  34. package/dist/http/routes/assets.js.map +1 -0
  35. package/dist/http/routes/automations.d.ts +7 -0
  36. package/dist/http/routes/automations.js +158 -0
  37. package/dist/http/routes/automations.js.map +1 -0
  38. package/dist/http/routes/courses.d.ts +7 -0
  39. package/dist/http/routes/courses.js +130 -0
  40. package/dist/http/routes/courses.js.map +1 -0
  41. package/dist/http/routes/dashboard.d.ts +7 -0
  42. package/dist/http/routes/dashboard.js +42 -0
  43. package/dist/http/routes/dashboard.js.map +1 -0
  44. package/dist/http/routes/discussion.d.ts +7 -0
  45. package/dist/http/routes/discussion.js +122 -0
  46. package/dist/http/routes/discussion.js.map +1 -0
  47. package/dist/http/routes/downloads.d.ts +7 -0
  48. package/dist/http/routes/downloads.js +208 -0
  49. package/dist/http/routes/downloads.js.map +1 -0
  50. package/dist/http/routes/entitlements.d.ts +7 -0
  51. package/dist/http/routes/entitlements.js +69 -0
  52. package/dist/http/routes/entitlements.js.map +1 -0
  53. package/dist/http/routes/identity.d.ts +6 -0
  54. package/dist/http/routes/identity.js +6 -0
  55. package/dist/http/routes/identity.js.map +1 -0
  56. package/dist/http/routes/integrations.d.ts +7 -0
  57. package/dist/http/routes/integrations.js +210 -0
  58. package/dist/http/routes/integrations.js.map +1 -0
  59. package/dist/http/routes/learn/assets.d.ts +8 -0
  60. package/dist/http/routes/learn/assets.js +32 -0
  61. package/dist/http/routes/learn/assets.js.map +1 -0
  62. package/dist/http/routes/learn/comments.d.ts +7 -0
  63. package/dist/http/routes/learn/comments.js +175 -0
  64. package/dist/http/routes/learn/comments.js.map +1 -0
  65. package/dist/http/routes/learn/courses.d.ts +7 -0
  66. package/dist/http/routes/learn/courses.js +75 -0
  67. package/dist/http/routes/learn/courses.js.map +1 -0
  68. package/dist/http/routes/learn/downloads.d.ts +7 -0
  69. package/dist/http/routes/learn/downloads.js +76 -0
  70. package/dist/http/routes/learn/downloads.js.map +1 -0
  71. package/dist/http/routes/learn/index.d.ts +7 -0
  72. package/dist/http/routes/learn/index.js +19 -0
  73. package/dist/http/routes/learn/index.js.map +1 -0
  74. package/dist/http/routes/learn/org.d.ts +7 -0
  75. package/dist/http/routes/learn/org.js +50 -0
  76. package/dist/http/routes/learn/org.js.map +1 -0
  77. package/dist/http/routes/learn/progress.d.ts +7 -0
  78. package/dist/http/routes/learn/progress.js +63 -0
  79. package/dist/http/routes/learn/progress.js.map +1 -0
  80. package/dist/http/routes/organizations.d.ts +7 -0
  81. package/dist/http/routes/organizations.js +219 -0
  82. package/dist/http/routes/organizations.js.map +1 -0
  83. package/dist/http/routes/progress.d.ts +6 -0
  84. package/dist/http/routes/progress.js +6 -0
  85. package/dist/http/routes/progress.js.map +1 -0
  86. package/dist/http/routes/students.d.ts +7 -0
  87. package/dist/http/routes/students.js +125 -0
  88. package/dist/http/routes/students.js.map +1 -0
  89. package/dist/http/routes.d.ts +7 -0
  90. package/dist/http/routes.js +37 -0
  91. package/dist/http/routes.js.map +1 -0
  92. package/dist/http/schemas/activities.d.ts +69 -0
  93. package/dist/http/schemas/activities.js +25 -0
  94. package/dist/http/schemas/activities.js.map +1 -0
  95. package/dist/http/schemas/assets.d.ts +142 -0
  96. package/dist/http/schemas/assets.js +24 -0
  97. package/dist/http/schemas/assets.js.map +1 -0
  98. package/dist/http/schemas/automations.d.ts +154 -0
  99. package/dist/http/schemas/automations.js +26 -0
  100. package/dist/http/schemas/automations.js.map +1 -0
  101. package/dist/http/schemas/content.d.ts +113 -0
  102. package/dist/http/schemas/content.js +29 -0
  103. package/dist/http/schemas/content.js.map +1 -0
  104. package/dist/http/schemas/dashboard.d.ts +26 -0
  105. package/dist/http/schemas/dashboard.js +18 -0
  106. package/dist/http/schemas/dashboard.js.map +1 -0
  107. package/dist/http/schemas/discussion.d.ts +340 -0
  108. package/dist/http/schemas/discussion.js +78 -0
  109. package/dist/http/schemas/discussion.js.map +1 -0
  110. package/dist/http/schemas/downloads.d.ts +110 -0
  111. package/dist/http/schemas/downloads.js +34 -0
  112. package/dist/http/schemas/downloads.js.map +1 -0
  113. package/dist/http/schemas/entitlements.d.ts +97 -0
  114. package/dist/http/schemas/entitlements.js +41 -0
  115. package/dist/http/schemas/entitlements.js.map +1 -0
  116. package/dist/http/schemas/index.d.ts +17 -0
  117. package/dist/http/schemas/index.js +17 -0
  118. package/dist/http/schemas/integrations.d.ts +93 -0
  119. package/dist/http/schemas/integrations.js +47 -0
  120. package/dist/http/schemas/integrations.js.map +1 -0
  121. package/dist/http/schemas/invites.d.ts +95 -0
  122. package/dist/http/schemas/invites.js +62 -0
  123. package/dist/http/schemas/invites.js.map +1 -0
  124. package/dist/http/schemas/learn.d.ts +142 -0
  125. package/dist/http/schemas/learn.js +58 -0
  126. package/dist/http/schemas/learn.js.map +1 -0
  127. package/dist/http/schemas/members.d.ts +88 -0
  128. package/dist/http/schemas/members.js +27 -0
  129. package/dist/http/schemas/members.js.map +1 -0
  130. package/dist/http/schemas/organizations.d.ts +22 -0
  131. package/dist/http/schemas/organizations.js +20 -0
  132. package/dist/http/schemas/organizations.js.map +1 -0
  133. package/dist/http/schemas/settings.d.ts +23 -0
  134. package/dist/http/schemas/settings.js +20 -0
  135. package/dist/http/schemas/settings.js.map +1 -0
  136. package/dist/http/schemas/shared.d.ts +57 -0
  137. package/dist/http/schemas/shared.js +54 -0
  138. package/dist/http/schemas/shared.js.map +1 -0
  139. package/dist/http/schemas/students.d.ts +64 -0
  140. package/dist/http/schemas/students.js +25 -0
  141. package/dist/http/schemas/students.js.map +1 -0
  142. package/dist/http/scope.d.ts +22 -0
  143. package/dist/http/scope.js +28 -0
  144. package/dist/http/scope.js.map +1 -0
  145. package/dist/http/server.d.ts +8 -0
  146. package/dist/http/server.js +33 -0
  147. package/dist/http/server.js.map +1 -0
  148. package/dist/http/web-bridge.d.ts +9 -0
  149. package/dist/http/web-bridge.js +27 -0
  150. package/dist/http/web-bridge.js.map +1 -0
  151. package/dist/index.d.ts +15 -0
  152. package/dist/index.js +16 -0
  153. package/dist/index.js.map +1 -0
  154. package/package.json +56 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloads.js","names":[],"sources":["../../../../src/http/routes/learn/downloads.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n DownloadAssetParams,\n DownloadIdParam,\n ErrorBody,\n LearnDownload,\n LearnDownloads,\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 learnDownloadsRoutes(\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: 'GET',\n url: '/api/learn/downloads',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listLearnDownloads',\n tags: ['Learn'],\n summary: 'Downloads the student is actively entitled to',\n response: { 200: LearnDownloads },\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 learn.listDownloads(req.orgId, orgUser.id);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/learn/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnDownload',\n tags: ['Learn'],\n summary: 'One entitled download and its ordered assets',\n params: DownloadIdParam,\n response: { 200: LearnDownload, 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 result = await learn.getDownload(req.orgId, orgUser.id, req.params.downloadId);\n if (!result) {\n throw new NotFoundError('Download', req.params.downloadId);\n }\n return result;\n },\n });\n\n // One stable, resolvable URL per asset. Drops into an <a href download> in\n // any frontend — no JS, no SDK, no CORS preflight. The signed target is\n // minted per request and short-lived.\n r.route({\n method: 'GET',\n url: '/api/learn/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnDownloadAsset',\n tags: ['Learn'],\n summary: 'Redirect to a short-lived signed URL for an entitled asset',\n params: DownloadAssetParams,\n response: { 302: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n const signed = await learn.downloadAssetUrl(\n req.orgId,\n orgUser.id,\n req.params.downloadId,\n req.params.assetId,\n );\n if (!signed) {\n // Always 404, never 403 — a 403 confirms the resource exists to\n // someone not entitled to it.\n throw new NotFoundError('Download asset', req.params.assetId);\n }\n return reply\n .header('cache-control', 'no-store')\n .header('referrer-policy', 'no-referrer')\n .redirect(signed.url, 302);\n },\n });\n}\n"],"mappings":";;;;;;;;AAcA,eAAsB,qBACpB,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,OAAO;GACd,SAAS;GACT,UAAU,EAAE,KAAK,eAAe;EAClC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,OAAO,MAAM,cAAc,IAAI,OAAO,QAAQ,EAAE;EAClD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,OAAO;GACd,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAe,KAAK;GAAU;EACjD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,MAAM,SAAS,MAAM,MAAM,YAAY,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,UAAU;GACnF,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,YAAY,IAAI,OAAO,UAAU;GAE3D,OAAO;EACT;CACF,CAAC;CAKD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,OAAO;GACd,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,KAAK;GAAU;EAC5C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,MAAM,SAAS,MAAM,MAAM,iBACzB,IAAI,OACJ,QAAQ,IACR,IAAI,OAAO,YACX,IAAI,OAAO,OACb;GACA,IAAI,CAAC,QAGH,MAAM,IAAI,cAAc,kBAAkB,IAAI,OAAO,OAAO;GAE9D,OAAO,MACJ,OAAO,iBAAiB,UAAU,CAAC,CACnC,OAAO,mBAAmB,aAAa,CAAC,CACxC,SAAS,OAAO,KAAK,GAAG;EAC7B;CACF,CAAC;AACH"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/learn/index.d.ts
4
+ declare function learnRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnRoutes };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { learnAssetsRoutes } from "./assets.js";
2
+ import { learnCommentsRoutes } from "./comments.js";
3
+ import { learnCoursesRoutes } from "./courses.js";
4
+ import { learnDownloadsRoutes } from "./downloads.js";
5
+ import { learnOrgRoutes } from "./org.js";
6
+ import { learnProgressRoutes } from "./progress.js";
7
+ //#region src/http/routes/learn/index.ts
8
+ async function learnRoutes(app, container) {
9
+ await learnOrgRoutes(app, container);
10
+ await learnCoursesRoutes(app, container);
11
+ await learnProgressRoutes(app, container);
12
+ await learnCommentsRoutes(app, container);
13
+ await learnAssetsRoutes(app, container);
14
+ await learnDownloadsRoutes(app, container);
15
+ }
16
+ //#endregion
17
+ export { learnRoutes };
18
+
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/http/routes/learn/index.ts"],"sourcesContent":["// HTTP routes for the student-facing Learn surface. Session-guarded like every\n// back-office route, but scoped by the session's `org_users` row —\n// the reads gate on the person's enrollments, not on staff role. One file per\n// resource; everything under `/api/learn` lives in this directory and nowhere\n// else.\nimport type { FastifyInstance } from 'fastify';\nimport type { Container } from '../../../app/container.js';\nimport { learnAssetsRoutes } from './assets.js';\nimport { learnCommentsRoutes } from './comments.js';\nimport { learnCoursesRoutes } from './courses.js';\nimport { learnDownloadsRoutes } from './downloads.js';\nimport { learnOrgRoutes } from './org.js';\nimport { learnProgressRoutes } from './progress.js';\n\nexport async function learnRoutes(app: FastifyInstance, container: Container): Promise<void> {\n await learnOrgRoutes(app, container);\n await learnCoursesRoutes(app, container);\n await learnProgressRoutes(app, container);\n await learnCommentsRoutes(app, container);\n await learnAssetsRoutes(app, container);\n await learnDownloadsRoutes(app, container);\n}\n"],"mappings":";;;;;;;AAcA,eAAsB,YAAY,KAAsB,WAAqC;CAC3F,MAAM,eAAe,KAAK,SAAS;CACnC,MAAM,mBAAmB,KAAK,SAAS;CACvC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,oBAAoB,KAAK,SAAS;CACxC,MAAM,kBAAkB,KAAK,SAAS;CACtC,MAAM,qBAAqB,KAAK,SAAS;AAC3C"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/learn/org.d.ts
4
+ declare function learnOrgRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnOrgRoutes };
7
+ //# sourceMappingURL=org.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { UnauthorizedError } from "../../plugins/auth.js";
2
+ import { LearnOrg, LearnViewer } from "../../schemas/learn.js";
3
+ import "../../schemas/index.js";
4
+ //#region src/http/routes/learn/org.ts
5
+ async function learnOrgRoutes(app, container) {
6
+ const r = app.withTypeProvider();
7
+ r.route({
8
+ method: "GET",
9
+ url: "/api/learn/org",
10
+ preHandler: app.requireOrgSession,
11
+ schema: {
12
+ operationId: "getLearnOrg",
13
+ tags: ["Learn"],
14
+ summary: "Get the portal org's public identity (branding)",
15
+ response: { 200: LearnOrg }
16
+ },
17
+ handler: async (req) => {
18
+ const org = await container.organizations.getById(req.orgId);
19
+ if (!org) throw new UnauthorizedError("session organization not found");
20
+ return {
21
+ id: org.id,
22
+ name: org.name,
23
+ slug: org.slug
24
+ };
25
+ }
26
+ });
27
+ r.route({
28
+ method: "GET",
29
+ url: "/api/learn/viewer",
30
+ preHandler: app.requireOrgSession,
31
+ schema: {
32
+ operationId: "getLearnViewer",
33
+ tags: ["Learn"],
34
+ summary: "Get the caller's identity within the session's org",
35
+ response: { 200: LearnViewer }
36
+ },
37
+ handler: async (req) => {
38
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
39
+ if (!orgUser) throw new UnauthorizedError();
40
+ return {
41
+ orgUserId: orgUser.id,
42
+ role: orgUser.role
43
+ };
44
+ }
45
+ });
46
+ }
47
+ //#endregion
48
+ export { learnOrgRoutes };
49
+
50
+ //# sourceMappingURL=org.js.map
@@ -0,0 +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: ['Learn'],\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: ['Learn'],\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,OAAO;GACd,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,OAAO;GACd,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"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/learn/progress.d.ts
4
+ declare function learnProgressRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnProgressRoutes };
7
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { UnauthorizedError } from "../../plugins/auth.js";
2
+ import { ErrorBody } from "../../schemas/shared.js";
3
+ import { CourseProgress, LearnCourseIdParam, ProgressStatus, ReportProgress } from "../../schemas/learn.js";
4
+ import "../../schemas/index.js";
5
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
6
+ //#region src/http/routes/learn/progress.ts
7
+ async function learnProgressRoutes(app, container) {
8
+ const r = app.withTypeProvider();
9
+ const learn = container.reporting.learn;
10
+ r.route({
11
+ method: "POST",
12
+ url: "/api/learn/progress",
13
+ preHandler: app.requireOrgSession,
14
+ schema: {
15
+ operationId: "reportProgress",
16
+ tags: ["Learn"],
17
+ summary: "Report usage on a target; the progress service decides completion",
18
+ body: ReportProgress,
19
+ response: {
20
+ 200: ProgressStatus,
21
+ 404: ErrorBody
22
+ }
23
+ },
24
+ handler: async (req) => {
25
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
26
+ if (!orgUser) throw new UnauthorizedError();
27
+ const activity = await container.content.getActivity(req.orgId, req.body.activity);
28
+ if (!activity) throw new NotFoundError("Activity", req.body.activity);
29
+ if (!await learn.getCourse(req.orgId, orgUser.id, activity.courseId)) throw new NotFoundError("Activity", req.body.activity);
30
+ return { status: (await container.progress.report(req.orgId, {
31
+ orgUserId: orgUser.id,
32
+ activityId: req.body.activity,
33
+ reports: req.body.reports
34
+ })).completedAt ? "completed" : "in-progress" };
35
+ }
36
+ });
37
+ r.route({
38
+ method: "GET",
39
+ url: "/api/learn/courses/:courseId/progress",
40
+ preHandler: app.requireOrgSession,
41
+ schema: {
42
+ operationId: "getLearnCourseProgress",
43
+ tags: ["Learn"],
44
+ summary: "The student's progress in one course",
45
+ params: LearnCourseIdParam,
46
+ response: {
47
+ 200: CourseProgress,
48
+ 404: ErrorBody
49
+ }
50
+ },
51
+ handler: async (req) => {
52
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
53
+ if (!orgUser) throw new UnauthorizedError();
54
+ const view = await learn.courseProgress(req.orgId, orgUser.id, req.params.courseId);
55
+ if (!view) throw new NotFoundError("Course", req.params.courseId);
56
+ return view;
57
+ }
58
+ });
59
+ }
60
+ //#endregion
61
+ export { learnProgressRoutes };
62
+
63
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +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: ['Learn'],\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: ['Learn'],\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,OAAO;GACd,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,OAAO;GACd,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"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/organizations.d.ts
4
+ declare function organizationsRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { organizationsRoutes };
7
+ //# sourceMappingURL=organizations.d.ts.map
@@ -0,0 +1,219 @@
1
+ import { resolveScope } from "../scope.js";
2
+ import { ErrorBody } from "../schemas/shared.js";
3
+ import { CreateOrganization, Organization, UpdateOrganization } from "../schemas/organizations.js";
4
+ import { Member, MemberIdParam, MembersPage, MembersQuery, UpdateMemberRole } from "../schemas/members.js";
5
+ import { AcceptInvite, AcceptInviteResult, CreateInvite, GetInviteResult, Invite, InviteTokenParam } from "../schemas/invites.js";
6
+ import "../schemas/index.js";
7
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
8
+ import { z } from "zod";
9
+ //#region src/http/routes/organizations.ts
10
+ async function organizationsRoutes(app, container) {
11
+ const r = app.withTypeProvider();
12
+ const organizations = container.organizations;
13
+ const tags = ["Organizations"];
14
+ r.route({
15
+ method: "POST",
16
+ url: "/api/organizations",
17
+ preHandler: app.requireSession,
18
+ schema: {
19
+ operationId: "createOrganization",
20
+ tags,
21
+ summary: "Create an organization and make it active",
22
+ body: CreateOrganization,
23
+ response: { 201: Organization }
24
+ },
25
+ handler: async (req, reply) => {
26
+ const org = await container.orgProvider.createOrganization(req.headers, req.body);
27
+ return reply.code(201).send({
28
+ name: org.name,
29
+ slug: org.slug,
30
+ id: org.id,
31
+ createdAt: org.createdAt.toISOString()
32
+ });
33
+ }
34
+ });
35
+ r.route({
36
+ method: "PATCH",
37
+ url: "/api/organizations",
38
+ preHandler: app.requireOrgSession,
39
+ schema: {
40
+ operationId: "updateOrganization",
41
+ tags,
42
+ summary: "Update the active organization",
43
+ body: UpdateOrganization,
44
+ response: {
45
+ 200: Organization,
46
+ 409: ErrorBody
47
+ }
48
+ },
49
+ handler: async (req, reply) => {
50
+ const scope = await resolveScope(container, req);
51
+ const org = await container.orgProvider.updateOrganization(req.headers, scope.authOrgId, req.body);
52
+ return reply.send({
53
+ id: org.id,
54
+ name: org.name,
55
+ slug: org.slug,
56
+ createdAt: org.createdAt.toISOString()
57
+ });
58
+ }
59
+ });
60
+ r.route({
61
+ method: "GET",
62
+ url: "/api/organizations/members",
63
+ preHandler: app.requireOrgSession,
64
+ schema: {
65
+ operationId: "listMembers",
66
+ tags,
67
+ summary: "List organization members",
68
+ querystring: MembersQuery,
69
+ response: { 200: MembersPage }
70
+ },
71
+ handler: async (req) => {
72
+ const scope = await resolveScope(container, req);
73
+ return organizations.listMembers(scope.orgId, req.query);
74
+ }
75
+ });
76
+ r.route({
77
+ method: "PATCH",
78
+ url: "/api/organizations/members/:id/role",
79
+ preHandler: app.requireOrgSession,
80
+ schema: {
81
+ operationId: "updateMemberRole",
82
+ tags,
83
+ summary: "Change a member's role",
84
+ params: MemberIdParam,
85
+ body: UpdateMemberRole,
86
+ response: {
87
+ 200: Member,
88
+ 404: ErrorBody,
89
+ 409: ErrorBody
90
+ }
91
+ },
92
+ handler: async (req) => {
93
+ const scope = await resolveScope(container, req);
94
+ const ctx = {
95
+ orgId: scope.orgId,
96
+ authOrgId: scope.authOrgId,
97
+ headers: req.headers
98
+ };
99
+ const member = await organizations.updateMemberRole(ctx, req.params.id, req.body.role);
100
+ if (!member) throw new NotFoundError("Member", req.params.id);
101
+ return member;
102
+ }
103
+ });
104
+ r.route({
105
+ method: "DELETE",
106
+ url: "/api/organizations/members/:id",
107
+ preHandler: app.requireOrgSession,
108
+ schema: {
109
+ operationId: "removeMember",
110
+ tags,
111
+ summary: "Remove an organization member",
112
+ params: MemberIdParam,
113
+ response: {
114
+ 204: z.void(),
115
+ 404: ErrorBody,
116
+ 409: ErrorBody
117
+ }
118
+ },
119
+ handler: async (req, reply) => {
120
+ const scope = await resolveScope(container, req);
121
+ const ctx = {
122
+ orgId: scope.orgId,
123
+ authOrgId: scope.authOrgId,
124
+ headers: req.headers
125
+ };
126
+ if (!await organizations.removeMember(ctx, req.params.id)) throw new NotFoundError("Member", req.params.id);
127
+ return reply.code(204).send();
128
+ }
129
+ });
130
+ r.route({
131
+ method: "POST",
132
+ url: "/api/organizations/invites",
133
+ preHandler: app.requireOrgSession,
134
+ schema: {
135
+ operationId: "createInvite",
136
+ tags,
137
+ summary: "Invite a member or student into the active organization",
138
+ body: CreateInvite,
139
+ response: {
140
+ 201: Invite,
141
+ 409: ErrorBody
142
+ }
143
+ },
144
+ handler: async (req) => {
145
+ const scope = await resolveScope(container, req);
146
+ const invite = await organizations.createInvite({
147
+ orgId: scope.orgId,
148
+ inviterUserId: scope.userId,
149
+ email: req.body.email,
150
+ role: req.body.role,
151
+ ...req.body.firstName !== void 0 && { firstName: req.body.firstName },
152
+ ...req.body.lastName !== void 0 && { lastName: req.body.lastName },
153
+ sendEmail: req.body.sendEmail
154
+ });
155
+ req.log.debug({ invite }, "created invite");
156
+ return invite;
157
+ }
158
+ });
159
+ r.route({
160
+ method: "GET",
161
+ url: "/api/organizations/invites/:token",
162
+ schema: {
163
+ operationId: "getInvite",
164
+ tags,
165
+ summary: "Read an invite by its token",
166
+ params: InviteTokenParam,
167
+ response: {
168
+ 200: GetInviteResult,
169
+ 404: ErrorBody
170
+ }
171
+ },
172
+ handler: async (req) => {
173
+ const invite = await organizations.peekInvite(req.params.token);
174
+ if (!invite) throw new NotFoundError("Invite", "token");
175
+ const person = await container.identity.getUserByEmail(invite.email);
176
+ return {
177
+ email: invite.email,
178
+ firstName: person?.firstName ?? null,
179
+ lastName: person?.lastName ?? null
180
+ };
181
+ }
182
+ });
183
+ r.route({
184
+ method: "POST",
185
+ url: "/api/organizations/invites/accept",
186
+ preHandler: app.requireSession,
187
+ schema: {
188
+ operationId: "acceptInvite",
189
+ tags,
190
+ summary: "Accept an invite with the logged-in account",
191
+ body: AcceptInvite,
192
+ response: {
193
+ 200: AcceptInviteResult,
194
+ 400: ErrorBody
195
+ }
196
+ },
197
+ handler: async (req) => {
198
+ const user = await container.identity.getUserByExternalId(req.authUser.id);
199
+ if (!user) throw new NotFoundError("User", req.authUser.id);
200
+ const accepted = await container.organizations.acceptInvite({
201
+ token: req.body.token,
202
+ userId: user.id,
203
+ email: req.authUser.email
204
+ });
205
+ const org = await container.organizations.getById(accepted.orgId);
206
+ if (!org) throw new NotFoundError("Organization", accepted.orgId);
207
+ const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);
208
+ req.log.info({
209
+ accepted,
210
+ stamped
211
+ }, "accepted invite");
212
+ return {};
213
+ }
214
+ });
215
+ }
216
+ //#endregion
217
+ export { organizationsRoutes };
218
+
219
+ //# sourceMappingURL=organizations.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,6 @@
1
+ import { FastifyInstance } from "fastify";
2
+ //#region src/http/routes/progress.d.ts
3
+ declare function progressRoutes(_app: FastifyInstance): Promise<void>;
4
+ //#endregion
5
+ export { progressRoutes };
6
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1,6 @@
1
+ //#region src/http/routes/progress.ts
2
+ async function progressRoutes(_app) {}
3
+ //#endregion
4
+ export { progressRoutes };
5
+
6
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","names":[],"sources":["../../../src/http/routes/progress.ts"],"sourcesContent":["// HTTP routes for the progress context. Stub.\nimport type { FastifyInstance } from 'fastify';\n\nexport async function progressRoutes(_app: FastifyInstance): Promise<void> {\n // register progress routes here\n}\n"],"mappings":";AAGA,eAAsB,eAAe,MAAsC,CAE3E"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/students.d.ts
4
+ declare function studentsRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { studentsRoutes };
7
+ //# sourceMappingURL=students.d.ts.map
@@ -0,0 +1,125 @@
1
+ import { resolveScope } from "../scope.js";
2
+ import { ErrorBody } from "../schemas/shared.js";
3
+ import { Student, StudentIdParam, StudentsPage, StudentsQuery, UpdateStudentBody } from "../schemas/students.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/students.ts
8
+ async function studentsRoutes(app, container) {
9
+ const r = app.withTypeProvider();
10
+ const students = container.reporting.students;
11
+ r.route({
12
+ method: "GET",
13
+ url: "/api/students",
14
+ preHandler: app.requireOrgSession,
15
+ schema: {
16
+ operationId: "listStudents",
17
+ tags: ["Students"],
18
+ summary: "List students",
19
+ querystring: StudentsQuery,
20
+ response: { 200: StudentsPage }
21
+ },
22
+ handler: async (req) => {
23
+ const scope = await resolveScope(container, req);
24
+ return students.list(scope.orgId, req.query);
25
+ }
26
+ });
27
+ r.route({
28
+ method: "GET",
29
+ url: "/api/students/:id",
30
+ preHandler: app.requireOrgSession,
31
+ schema: {
32
+ operationId: "getStudent",
33
+ tags: ["Students"],
34
+ summary: "Get a student by id",
35
+ params: StudentIdParam,
36
+ response: {
37
+ 200: Student,
38
+ 404: ErrorBody
39
+ }
40
+ },
41
+ handler: async (req) => {
42
+ const scope = await resolveScope(container, req);
43
+ const student = await students.get(scope.orgId, req.params.id);
44
+ if (!student) throw new NotFoundError("Student", req.params.id);
45
+ return student;
46
+ }
47
+ });
48
+ r.route({
49
+ method: "PATCH",
50
+ url: "/api/students/:id",
51
+ preHandler: app.requireOrgSession,
52
+ schema: {
53
+ operationId: "updateStudent",
54
+ tags: ["Students"],
55
+ summary: "Update a student's profile",
56
+ params: StudentIdParam,
57
+ body: UpdateStudentBody,
58
+ response: {
59
+ 200: Student,
60
+ 404: ErrorBody,
61
+ 409: ErrorBody
62
+ }
63
+ },
64
+ handler: async (req) => {
65
+ const scope = await resolveScope(container, req);
66
+ await container.organizations.updateStudent({
67
+ orgId: scope.orgId,
68
+ orgUserId: req.params.id,
69
+ ...req.body
70
+ });
71
+ const student = await students.get(scope.orgId, req.params.id);
72
+ if (!student) throw new NotFoundError("Student", req.params.id);
73
+ return student;
74
+ }
75
+ });
76
+ r.route({
77
+ method: "POST",
78
+ url: "/api/students/:id/invite/resend",
79
+ preHandler: app.requireOrgSession,
80
+ schema: {
81
+ operationId: "resendStudentInvite",
82
+ tags: ["Students"],
83
+ summary: "Re-send a pending student's invite email",
84
+ params: StudentIdParam,
85
+ response: {
86
+ 204: z.void(),
87
+ 404: ErrorBody,
88
+ 409: ErrorBody
89
+ }
90
+ },
91
+ handler: async (req, reply) => {
92
+ const scope = await resolveScope(container, req);
93
+ await container.organizations.resendStudentInvite({
94
+ orgId: scope.orgId,
95
+ orgUserId: req.params.id,
96
+ inviterUserId: scope.userId
97
+ });
98
+ return reply.code(204).send();
99
+ }
100
+ });
101
+ r.route({
102
+ method: "DELETE",
103
+ url: "/api/students/:id",
104
+ preHandler: app.requireOrgSession,
105
+ schema: {
106
+ operationId: "deleteStudent",
107
+ tags: ["Students"],
108
+ summary: "Delete a student",
109
+ params: StudentIdParam,
110
+ response: {
111
+ 204: z.void(),
112
+ 404: ErrorBody
113
+ }
114
+ },
115
+ handler: async (req, reply) => {
116
+ const scope = await resolveScope(container, req);
117
+ await container.organizations.deleteOrgUser(scope.orgId, req.params.id);
118
+ return reply.code(204).send();
119
+ }
120
+ });
121
+ }
122
+ //#endregion
123
+ export { studentsRoutes };
124
+
125
+ //# sourceMappingURL=students.js.map
@@ -0,0 +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: ['Students'],\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: ['Students'],\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 // 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: ['Students'],\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: ['Students'],\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: ['Students'],\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":";;;;;;;AAmBA,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,UAAU;GACjB,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,aAAa;EAChC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,SAAS,KAAK,MAAM,OAAO,IAAI,KAAK;EAC7C;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;IAAS,KAAK;GAAU;EAC3C;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,MAAM,SAAS,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE;GAC7D,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAKD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,UAAU;GACjB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAS,KAAK;IAAW,KAAK;GAAU;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,cAAc;IAC1C,OAAO,MAAM;IACb,WAAW,IAAI,OAAO;IACtB,GAAG,IAAI;GACT,CAAC;GAGD,MAAM,UAAU,MAAM,SAAS,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE;GAC7D,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAKD,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,EAAE,KAAK;IAAG,KAAK;IAAW,KAAK;GAAU;EAC5D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,oBAAoB;IAChD,OAAO,MAAM;IACb,WAAW,IAAI,OAAO;IACtB,eAAe,MAAM;GACvB,CAAC;GACD,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,CAAC,UAAU;GACjB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,KAAK;GAAU;EAC5C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,cAAc,MAAM,OAAO,IAAI,OAAO,EAAE;GACtE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes.d.ts
4
+ declare function registerRoutes(app: FastifyInstance, container: Container): void;
5
+ //#endregion
6
+ export { registerRoutes };
7
+ //# sourceMappingURL=routes.d.ts.map