@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,210 @@
1
+ import { ErrorBody } from "../schemas/shared.js";
2
+ import { ActionInvocationResult, AvailableIntegrationsList, ConfigureRequest, ConnectRequest, Connection, ConnectionActionParams, ConnectionIdParam, ConnectionsList, InvokeActionRequest, ReconnectRequest } from "../schemas/integrations.js";
3
+ import "../schemas/index.js";
4
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
5
+ import { z } from "zod";
6
+ //#region src/http/routes/integrations.ts
7
+ function toApi(connection) {
8
+ const { credentialRef: _credentialRef, ...rest } = connection;
9
+ return rest;
10
+ }
11
+ /** Resolve the session's active org to the domain org id, or 400 and return null. */
12
+ async function resolveOrgId(req, reply) {
13
+ if (!req.orgId) {
14
+ await reply.code(400).send({
15
+ error: "no_active_org",
16
+ message: "No active organization"
17
+ });
18
+ return null;
19
+ }
20
+ return req.orgId;
21
+ }
22
+ async function integrationsRoutes(app, container) {
23
+ const r = app.withTypeProvider();
24
+ const integrations = container.integrations;
25
+ const tags = ["Integrations"];
26
+ r.route({
27
+ method: "GET",
28
+ url: "/api/integrations/available",
29
+ preHandler: app.requireOrgSession,
30
+ schema: {
31
+ operationId: "listAvailableIntegrations",
32
+ tags,
33
+ summary: "List the integrations this deployment supports, with their config schemas",
34
+ response: {
35
+ 200: AvailableIntegrationsList,
36
+ 401: ErrorBody
37
+ }
38
+ },
39
+ handler: async () => integrations.available()
40
+ });
41
+ r.route({
42
+ method: "GET",
43
+ url: "/api/integrations",
44
+ preHandler: app.requireOrgSession,
45
+ schema: {
46
+ operationId: "listConnections",
47
+ tags,
48
+ summary: "List the organization's integration connections",
49
+ response: {
50
+ 200: ConnectionsList,
51
+ 400: ErrorBody,
52
+ 401: ErrorBody
53
+ }
54
+ },
55
+ handler: async (req, reply) => {
56
+ const orgId = await resolveOrgId(req, reply);
57
+ if (!orgId) return;
58
+ return (await integrations.list(orgId)).map(toApi);
59
+ }
60
+ });
61
+ r.route({
62
+ method: "POST",
63
+ url: "/api/integrations",
64
+ preHandler: app.requireOrgSession,
65
+ schema: {
66
+ operationId: "connectIntegration",
67
+ tags,
68
+ summary: "Connect an integration (stores its secrets encrypted)",
69
+ body: ConnectRequest,
70
+ response: {
71
+ 201: Connection,
72
+ 400: ErrorBody,
73
+ 401: ErrorBody,
74
+ 409: ErrorBody
75
+ }
76
+ },
77
+ handler: async (req, reply) => {
78
+ const orgId = await resolveOrgId(req, reply);
79
+ if (!orgId) return;
80
+ const connection = await integrations.connect(orgId, req.body);
81
+ return reply.code(201).send(toApi(connection));
82
+ }
83
+ });
84
+ r.route({
85
+ method: "GET",
86
+ url: "/api/integrations/:id",
87
+ preHandler: app.requireOrgSession,
88
+ schema: {
89
+ operationId: "getConnection",
90
+ tags,
91
+ summary: "Get a connection",
92
+ params: ConnectionIdParam,
93
+ response: {
94
+ 200: Connection,
95
+ 400: ErrorBody,
96
+ 401: ErrorBody,
97
+ 404: ErrorBody
98
+ }
99
+ },
100
+ handler: async (req, reply) => {
101
+ const orgId = await resolveOrgId(req, reply);
102
+ if (!orgId) return;
103
+ const connection = await integrations.get(orgId, req.params.id);
104
+ if (!connection) throw new NotFoundError("Connection", req.params.id);
105
+ return toApi(connection);
106
+ }
107
+ });
108
+ r.route({
109
+ method: "PATCH",
110
+ url: "/api/integrations/:id",
111
+ preHandler: app.requireOrgSession,
112
+ schema: {
113
+ operationId: "configureConnection",
114
+ tags,
115
+ summary: "Change a connection's configuration or active flag",
116
+ params: ConnectionIdParam,
117
+ body: ConfigureRequest,
118
+ response: {
119
+ 200: Connection,
120
+ 400: ErrorBody,
121
+ 401: ErrorBody,
122
+ 404: ErrorBody
123
+ }
124
+ },
125
+ handler: async (req, reply) => {
126
+ const orgId = await resolveOrgId(req, reply);
127
+ if (!orgId) return;
128
+ const connection = await integrations.configure(orgId, req.params.id, req.body);
129
+ if (!connection) throw new NotFoundError("Connection", req.params.id);
130
+ return toApi(connection);
131
+ }
132
+ });
133
+ r.route({
134
+ method: "POST",
135
+ url: "/api/integrations/:id/reconnect",
136
+ preHandler: app.requireOrgSession,
137
+ schema: {
138
+ operationId: "reconnectIntegration",
139
+ tags,
140
+ summary: "Replace a connection's secrets (re-authenticate)",
141
+ params: ConnectionIdParam,
142
+ body: ReconnectRequest,
143
+ response: {
144
+ 200: Connection,
145
+ 400: ErrorBody,
146
+ 401: ErrorBody,
147
+ 404: ErrorBody
148
+ }
149
+ },
150
+ handler: async (req, reply) => {
151
+ const orgId = await resolveOrgId(req, reply);
152
+ if (!orgId) return;
153
+ const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);
154
+ if (!connection) throw new NotFoundError("Connection", req.params.id);
155
+ return toApi(connection);
156
+ }
157
+ });
158
+ r.route({
159
+ method: "POST",
160
+ url: "/api/integrations/:id/actions/:actionId",
161
+ preHandler: app.requireOrgSession,
162
+ schema: {
163
+ operationId: "invokeConnectionAction",
164
+ tags,
165
+ summary: "Invoke one of a connection's actions (e.g. a listing that feeds a picker)",
166
+ params: ConnectionActionParams,
167
+ body: InvokeActionRequest,
168
+ response: {
169
+ 200: ActionInvocationResult,
170
+ 400: ErrorBody,
171
+ 401: ErrorBody,
172
+ 404: ErrorBody,
173
+ 409: ErrorBody,
174
+ 502: ErrorBody
175
+ }
176
+ },
177
+ handler: async (req, reply) => {
178
+ const orgId = await resolveOrgId(req, reply);
179
+ if (!orgId) return;
180
+ return { output: await integrations.invoke(orgId, req.params.id, req.params.actionId, req.body.input ?? {}) };
181
+ }
182
+ });
183
+ r.route({
184
+ method: "DELETE",
185
+ url: "/api/integrations/:id",
186
+ preHandler: app.requireOrgSession,
187
+ schema: {
188
+ operationId: "disconnectIntegration",
189
+ tags,
190
+ summary: "Disconnect an integration (destroys its stored secrets)",
191
+ params: ConnectionIdParam,
192
+ response: {
193
+ 204: z.void(),
194
+ 400: ErrorBody,
195
+ 401: ErrorBody,
196
+ 404: ErrorBody
197
+ }
198
+ },
199
+ handler: async (req, reply) => {
200
+ const orgId = await resolveOrgId(req, reply);
201
+ if (!orgId) return;
202
+ if (!await integrations.disconnect(orgId, req.params.id)) throw new NotFoundError("Connection", req.params.id);
203
+ return reply.code(204).send();
204
+ }
205
+ });
206
+ }
207
+ //#endregion
208
+ export { integrationsRoutes };
209
+
210
+ //# sourceMappingURL=integrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations.js","names":[],"sources":["../../../src/http/routes/integrations.ts"],"sourcesContent":["// HTTP routes for integrations (an org's connections to external services).\n// All require a session with an active organization. Credentials are\n// write-only: accepted on connect/reconnect, never present in a response —\n// `toApi` strips the credentialRef so not even the reference leaks.\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActionInvocationResult,\n AvailableIntegrationsList,\n ConfigureRequest,\n ConnectRequest,\n Connection,\n ConnectionActionParams,\n ConnectionIdParam,\n ConnectionsList,\n ErrorBody,\n InvokeActionRequest,\n ReconnectRequest,\n} from '../schemas/index.js';\nimport type { Connection as DomainConnection } from '@headless-lms/core/integrations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\n\nfunction toApi(connection: DomainConnection): Connection {\n const { credentialRef: _credentialRef, ...rest } = connection;\n return rest;\n}\n\n/** Resolve the session's active org to the domain org id, or 400 and return null. */\nasync function resolveOrgId(\n req: FastifyRequest,\n reply: FastifyReply,\n): Promise<string | null> {\n if (!req.orgId) {\n await reply.code(400).send({ error: 'no_active_org', message: 'No active organization' });\n return null;\n }\n return req.orgId;\n}\n\nexport async function integrationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const integrations = container.integrations;\n const tags = ['Integrations'];\n\n r.route({\n method: 'GET',\n url: '/api/integrations/available',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAvailableIntegrations',\n tags,\n summary: 'List the integrations this deployment supports, with their config schemas',\n response: { 200: AvailableIntegrationsList, 401: ErrorBody },\n },\n handler: async () => integrations.available(),\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listConnections',\n tags,\n summary: \"List the organization's integration connections\",\n response: { 200: ConnectionsList, 400: ErrorBody, 401: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n return (await integrations.list(orgId)).map(toApi);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'connectIntegration',\n tags,\n summary: 'Connect an integration (stores its secrets encrypted)',\n body: ConnectRequest,\n response: { 201: Connection, 400: ErrorBody, 401: ErrorBody, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.connect(orgId, req.body);\n return reply.code(201).send(toApi(connection));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getConnection',\n tags,\n summary: 'Get a connection',\n params: ConnectionIdParam,\n response: { 200: Connection, 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.get(orgId, req.params.id);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/integrations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'configureConnection',\n tags,\n summary: \"Change a connection's configuration or active flag\",\n params: ConnectionIdParam,\n body: ConfigureRequest,\n response: { 200: Connection, 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.configure(orgId, req.params.id, req.body);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/reconnect',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reconnectIntegration',\n tags,\n summary: \"Replace a connection's secrets (re-authenticate)\",\n params: ConnectionIdParam,\n body: ReconnectRequest,\n response: { 200: Connection, 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/actions/:actionId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'invokeConnectionAction',\n tags,\n summary: \"Invoke one of a connection's actions (e.g. a listing that feeds a picker)\",\n params: ConnectionActionParams,\n body: InvokeActionRequest,\n response: {\n 200: ActionInvocationResult,\n 400: ErrorBody,\n 401: ErrorBody,\n 404: ErrorBody,\n 409: ErrorBody,\n 502: ErrorBody,\n },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const output = await integrations.invoke(\n orgId,\n req.params.id,\n req.params.actionId,\n req.body.input ?? {},\n );\n return { output: output as Record<string, unknown> };\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/integrations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'disconnectIntegration',\n tags,\n summary: 'Disconnect an integration (destroys its stored secrets)',\n params: ConnectionIdParam,\n response: { 204: z.void(), 400: ErrorBody, 401: ErrorBody, 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const removed = await integrations.disconnect(orgId, req.params.id);\n if (!removed) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { Container } from "../../../app/container.js";
2
+ import "../../../index.js";
3
+ import { FastifyInstance } from "fastify";
4
+ //#region src/http/routes/learn/assets.d.ts
5
+ declare function learnAssetsRoutes(app: FastifyInstance, container: Container): Promise<void>;
6
+ //#endregion
7
+ export { learnAssetsRoutes };
8
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { ErrorBody } from "../../schemas/shared.js";
2
+ import { AssetIdParam, DownloadTicket } from "../../schemas/assets.js";
3
+ import "../../schemas/index.js";
4
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
5
+ //#region src/http/routes/learn/assets.ts
6
+ async function learnAssetsRoutes(app, container) {
7
+ app.withTypeProvider().route({
8
+ method: "POST",
9
+ url: "/api/learn/assets/:id/url",
10
+ preHandler: app.requireOrgSession,
11
+ schema: {
12
+ operationId: "getAssetDownloadUrl",
13
+ tags: ["Learn"],
14
+ summary: "Get a short-lived presigned URL to serve an asset to the student",
15
+ params: AssetIdParam,
16
+ response: {
17
+ 200: DownloadTicket,
18
+ 401: ErrorBody,
19
+ 404: ErrorBody
20
+ }
21
+ },
22
+ handler: async (req) => {
23
+ const ticket = await container.assets.requestDownload(req.orgId, req.params.id);
24
+ if (!ticket) throw new NotFoundError("Asset", req.params.id);
25
+ return ticket;
26
+ }
27
+ });
28
+ }
29
+ //#endregion
30
+ export { learnAssetsRoutes };
31
+
32
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../../../src/http/routes/learn/assets.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { AssetIdParam, DownloadTicket, ErrorBody } from '../../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '@headless-lms/server';\n\nexport async function learnAssetsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n // Content embeds assets by stable `assetId`; the URL persisted at authoring\n // time is a long-expired presign. The student surface mints a fresh\n // short-lived ticket here, scoped to the session's portal org.\n r.route({\n method: 'POST',\n url: '/api/learn/assets/:id/url',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getAssetDownloadUrl',\n tags: ['Learn'],\n summary: 'Get a short-lived presigned URL to serve an asset to the student',\n params: AssetIdParam,\n response: { 200: DownloadTicket, 401: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n\n // No body: the student surface never renames a download, so the asset's\n // own filename stands.\n const ticket = await container.assets.requestDownload(req.orgId, req.params.id);\n if (!ticket) {\n throw new NotFoundError('Asset', req.params.id);\n }\n return ticket;\n },\n });\n}\n"],"mappings":";;;;;AAMA,eAAsB,kBAAkB,KAAsB,WAAqC;CAMjG,IALc,iBAKd,CAAC,CAAC,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;IAAW,KAAK;GAAU;EAClE;EACA,SAAS,OAAO,QAAQ;GAItB,MAAM,SAAS,MAAM,UAAU,OAAO,gBAAgB,IAAI,OAAO,IAAI,OAAO,EAAE;GAC9E,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,SAAS,IAAI,OAAO,EAAE;GAEhD,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/learn/comments.d.ts
4
+ declare function learnCommentsRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnCommentsRoutes };
7
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1,175 @@
1
+ import { UnauthorizedError } from "../../plugins/auth.js";
2
+ import { ErrorBody } from "../../schemas/shared.js";
3
+ import { ActivityComments, Comment, CommentIdParam, CommentReactions, CommentReport, CommentView, DiscussionActivityParam, EditComment, PostComment, ReportComment, SetCommentReaction } from "../../schemas/discussion.js";
4
+ import "../../schemas/index.js";
5
+ //#region src/http/routes/learn/comments.ts
6
+ async function learnCommentsRoutes(app, container) {
7
+ const r = app.withTypeProvider();
8
+ const discussion = container.discussion;
9
+ r.route({
10
+ method: "GET",
11
+ url: "/api/learn/activities/:activityId/comments",
12
+ preHandler: app.requireOrgSession,
13
+ schema: {
14
+ operationId: "listActivityComments",
15
+ tags: ["Learn"],
16
+ summary: "Read an activity's comments",
17
+ params: DiscussionActivityParam,
18
+ response: {
19
+ 200: ActivityComments,
20
+ 404: ErrorBody
21
+ }
22
+ },
23
+ handler: async (req) => {
24
+ const orgId = req.orgId;
25
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
26
+ if (!orgUser) throw new UnauthorizedError();
27
+ return discussion.activityComments(orgId, req.params.activityId, orgUser);
28
+ }
29
+ });
30
+ r.route({
31
+ method: "POST",
32
+ url: "/api/learn/activities/:activityId/comments",
33
+ preHandler: app.requireOrgSession,
34
+ schema: {
35
+ operationId: "postComment",
36
+ tags: ["Learn"],
37
+ summary: "Post a comment or reply on an activity",
38
+ params: DiscussionActivityParam,
39
+ body: PostComment,
40
+ response: {
41
+ 200: CommentView,
42
+ 403: ErrorBody,
43
+ 404: ErrorBody
44
+ }
45
+ },
46
+ handler: async (req) => {
47
+ const orgId = req.orgId;
48
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
49
+ if (!orgUser) throw new UnauthorizedError();
50
+ return discussion.postComment(orgId, {
51
+ actor: orgUser,
52
+ activityId: req.params.activityId,
53
+ parentId: req.body.parentId,
54
+ body: req.body.body
55
+ });
56
+ }
57
+ });
58
+ r.route({
59
+ method: "PATCH",
60
+ url: "/api/learn/comments/:commentId",
61
+ preHandler: app.requireOrgSession,
62
+ schema: {
63
+ operationId: "editComment",
64
+ tags: ["Learn"],
65
+ summary: "Revise your own comment",
66
+ params: CommentIdParam,
67
+ body: EditComment,
68
+ response: {
69
+ 200: CommentView,
70
+ 403: ErrorBody,
71
+ 404: ErrorBody
72
+ }
73
+ },
74
+ handler: async (req) => {
75
+ const orgId = req.orgId;
76
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
77
+ if (!orgUser) throw new UnauthorizedError();
78
+ return discussion.edit(orgId, req.params.commentId, orgUser, req.body.body);
79
+ }
80
+ });
81
+ r.route({
82
+ method: "DELETE",
83
+ url: "/api/learn/comments/:commentId",
84
+ preHandler: app.requireOrgSession,
85
+ schema: {
86
+ operationId: "deleteComment",
87
+ tags: ["Learn"],
88
+ summary: "Delete a comment",
89
+ params: CommentIdParam,
90
+ response: {
91
+ 200: Comment,
92
+ 403: ErrorBody,
93
+ 404: ErrorBody
94
+ }
95
+ },
96
+ handler: async (req) => {
97
+ const orgId = req.orgId;
98
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
99
+ if (!orgUser) throw new UnauthorizedError();
100
+ return discussion.remove(orgId, req.params.commentId, orgUser);
101
+ }
102
+ });
103
+ r.route({
104
+ method: "PUT",
105
+ url: "/api/learn/comments/:commentId/reaction",
106
+ preHandler: app.requireOrgSession,
107
+ schema: {
108
+ operationId: "setCommentReaction",
109
+ tags: ["Learn"],
110
+ summary: "Set your reaction to a comment, replacing any you already had",
111
+ params: CommentIdParam,
112
+ body: SetCommentReaction,
113
+ response: {
114
+ 200: CommentReactions,
115
+ 403: ErrorBody,
116
+ 404: ErrorBody
117
+ }
118
+ },
119
+ handler: async (req) => {
120
+ const orgId = req.orgId;
121
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
122
+ if (!orgUser) throw new UnauthorizedError();
123
+ return discussion.setReaction(orgId, req.params.commentId, orgUser, req.body.emoji);
124
+ }
125
+ });
126
+ r.route({
127
+ method: "DELETE",
128
+ url: "/api/learn/comments/:commentId/reaction",
129
+ preHandler: app.requireOrgSession,
130
+ schema: {
131
+ operationId: "clearCommentReaction",
132
+ tags: ["Learn"],
133
+ summary: "Withdraw your reaction from a comment",
134
+ params: CommentIdParam,
135
+ response: {
136
+ 200: CommentReactions,
137
+ 403: ErrorBody,
138
+ 404: ErrorBody
139
+ }
140
+ },
141
+ handler: async (req) => {
142
+ const orgId = req.orgId;
143
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
144
+ if (!orgUser) throw new UnauthorizedError();
145
+ return discussion.setReaction(orgId, req.params.commentId, orgUser, null);
146
+ }
147
+ });
148
+ r.route({
149
+ method: "POST",
150
+ url: "/api/learn/comments/:commentId/reports",
151
+ preHandler: app.requireOrgSession,
152
+ schema: {
153
+ operationId: "reportComment",
154
+ tags: ["Learn"],
155
+ summary: "Flag a comment for moderator attention",
156
+ params: CommentIdParam,
157
+ body: ReportComment,
158
+ response: {
159
+ 200: CommentReport,
160
+ 403: ErrorBody,
161
+ 404: ErrorBody
162
+ }
163
+ },
164
+ handler: async (req) => {
165
+ const orgId = req.orgId;
166
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
167
+ if (!orgUser) throw new UnauthorizedError();
168
+ return discussion.reportComment(orgId, req.params.commentId, orgUser, req.body.reason);
169
+ }
170
+ });
171
+ }
172
+ //#endregion
173
+ export { learnCommentsRoutes };
174
+
175
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","names":[],"sources":["../../../../src/http/routes/learn/comments.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n ActivityComments,\n Comment,\n CommentIdParam,\n CommentReactions,\n CommentReport,\n CommentView,\n DiscussionActivityParam,\n EditComment,\n ErrorBody,\n PostComment,\n ReportComment,\n SetCommentReaction,\n} from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { UnauthorizedError } from '../../plugins/auth.js';\n\nexport async function learnCommentsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const discussion = container.discussion;\n\n r.route({\n method: 'GET',\n url: '/api/learn/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listActivityComments',\n tags: ['Learn'],\n summary: \"Read an activity's comments\",\n params: DiscussionActivityParam,\n response: { 200: ActivityComments, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.activityComments(orgId, req.params.activityId, orgUser);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/learn/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'postComment',\n tags: ['Learn'],\n summary: 'Post a comment or reply on an activity',\n params: DiscussionActivityParam,\n body: PostComment,\n response: { 200: CommentView, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.postComment(orgId, {\n actor: orgUser,\n activityId: req.params.activityId,\n parentId: req.body.parentId,\n body: req.body.body,\n });\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/learn/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'editComment',\n tags: ['Learn'],\n summary: 'Revise your own comment',\n params: CommentIdParam,\n body: EditComment,\n response: { 200: CommentView, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.edit(orgId, req.params.commentId, orgUser, req.body.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/learn/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteComment',\n tags: ['Learn'],\n summary: 'Delete a comment',\n params: CommentIdParam,\n response: { 200: Comment, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.remove(orgId, req.params.commentId, orgUser);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/learn/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setCommentReaction',\n tags: ['Learn'],\n summary: 'Set your reaction to a comment, replacing any you already had',\n params: CommentIdParam,\n body: SetCommentReaction,\n response: { 200: CommentReactions, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.setReaction(orgId, req.params.commentId, orgUser, req.body.emoji);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/learn/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'clearCommentReaction',\n tags: ['Learn'],\n summary: 'Withdraw your reaction from a comment',\n params: CommentIdParam,\n response: { 200: CommentReactions, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.setReaction(orgId, req.params.commentId, orgUser, null);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/learn/comments/:commentId/reports',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportComment',\n tags: ['Learn'],\n summary: 'Flag a comment for moderator attention',\n params: CommentIdParam,\n body: ReportComment,\n response: { 200: CommentReport, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgId = req.orgId;\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new UnauthorizedError();\n }\n return discussion.reportComment(orgId, req.params.commentId, orgUser, req.body.reason);\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/learn/courses.d.ts
4
+ declare function learnCoursesRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnCoursesRoutes };
7
+ //# sourceMappingURL=courses.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { UnauthorizedError } from "../../plugins/auth.js";
2
+ import { ErrorBody } from "../../schemas/shared.js";
3
+ import { Course } from "../../schemas/content.js";
4
+ import { LearnCourseIdParam, LearnCourses, LearnModules } from "../../schemas/learn.js";
5
+ import "../../schemas/index.js";
6
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
7
+ //#region src/http/routes/learn/courses.ts
8
+ async function learnCoursesRoutes(app, container) {
9
+ const r = app.withTypeProvider();
10
+ const learn = container.reporting.learn;
11
+ r.route({
12
+ method: "GET",
13
+ url: "/api/learn/courses",
14
+ preHandler: app.requireOrgSession,
15
+ schema: {
16
+ operationId: "listLearnCourses",
17
+ tags: ["Learn"],
18
+ summary: "List the student's enrolled courses",
19
+ response: { 200: LearnCourses }
20
+ },
21
+ handler: async (req) => {
22
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
23
+ if (!orgUser) throw new UnauthorizedError();
24
+ return learn.listCourses(req.orgId, orgUser.id);
25
+ }
26
+ });
27
+ r.route({
28
+ method: "GET",
29
+ url: "/api/learn/courses/:courseId",
30
+ preHandler: app.requireOrgSession,
31
+ schema: {
32
+ operationId: "getLearnCourse",
33
+ tags: ["Learn"],
34
+ summary: "Get one enrolled course",
35
+ params: LearnCourseIdParam,
36
+ response: {
37
+ 200: Course,
38
+ 404: ErrorBody
39
+ }
40
+ },
41
+ handler: async (req) => {
42
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
43
+ if (!orgUser) throw new UnauthorizedError();
44
+ const course = await learn.getCourse(req.orgId, orgUser.id, req.params.courseId);
45
+ if (!course) throw new NotFoundError("Course", req.params.courseId);
46
+ return course;
47
+ }
48
+ });
49
+ r.route({
50
+ method: "GET",
51
+ url: "/api/learn/courses/:courseId/modules",
52
+ preHandler: app.requireOrgSession,
53
+ schema: {
54
+ operationId: "listLearnModules",
55
+ tags: ["Learn"],
56
+ summary: "List an enrolled course's module/activity tree",
57
+ params: LearnCourseIdParam,
58
+ response: {
59
+ 200: LearnModules,
60
+ 404: ErrorBody
61
+ }
62
+ },
63
+ handler: async (req) => {
64
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
65
+ if (!orgUser) throw new UnauthorizedError();
66
+ const modules = await learn.listModules(req.orgId, orgUser.id, req.params.courseId);
67
+ if (!modules) throw new NotFoundError("Course", req.params.courseId);
68
+ return modules;
69
+ }
70
+ });
71
+ }
72
+ //#endregion
73
+ export { learnCoursesRoutes };
74
+
75
+ //# sourceMappingURL=courses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"courses.js","names":[],"sources":["../../../../src/http/routes/learn/courses.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Course,\n ErrorBody,\n LearnCourseIdParam,\n LearnCourses,\n LearnModules,\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 learnCoursesRoutes(\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/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listLearnCourses',\n tags: ['Learn'],\n summary: \"List the student's enrolled courses\",\n response: { 200: LearnCourses },\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.listCourses(req.orgId, orgUser.id);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/learn/courses/:courseId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getLearnCourse',\n tags: ['Learn'],\n summary: 'Get one enrolled course',\n params: LearnCourseIdParam,\n response: { 200: Course, 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 course = await learn.getCourse(req.orgId, orgUser.id, req.params.courseId);\n if (!course) {\n throw new NotFoundError('Course', req.params.courseId);\n }\n return course;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/learn/courses/:courseId/modules',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listLearnModules',\n tags: ['Learn'],\n summary: \"List an enrolled course's module/activity tree\",\n params: LearnCourseIdParam,\n response: { 200: LearnModules, 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 modules = await learn.listModules(req.orgId, orgUser.id, req.params.courseId);\n if (!modules) {\n throw new NotFoundError('Course', req.params.courseId);\n }\n return modules;\n },\n });\n}\n"],"mappings":";;;;;;;AAaA,eAAsB,mBACpB,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,aAAa;EAChC;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,YAAY,IAAI,OAAO,QAAQ,EAAE;EAChD;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;IAAQ,KAAK;GAAU;EAC1C;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,UAAU,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,QAAQ;GAC/E,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,UAAU,IAAI,OAAO,QAAQ;GAEvD,OAAO;EACT;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;IAAc,KAAK;GAAU;EAChD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;GAE9B,MAAM,UAAU,MAAM,MAAM,YAAY,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,QAAQ;GAClF,IAAI,CAAC,SACH,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/learn/downloads.d.ts
4
+ declare function learnDownloadsRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { learnDownloadsRoutes };
7
+ //# sourceMappingURL=downloads.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { UnauthorizedError } from "../../plugins/auth.js";
2
+ import { ErrorBody } from "../../schemas/shared.js";
3
+ import { DownloadAssetParams, DownloadIdParam } from "../../schemas/downloads.js";
4
+ import { LearnDownload, LearnDownloads } from "../../schemas/learn.js";
5
+ import "../../schemas/index.js";
6
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
7
+ import { z } from "zod";
8
+ //#region src/http/routes/learn/downloads.ts
9
+ async function learnDownloadsRoutes(app, container) {
10
+ const r = app.withTypeProvider();
11
+ const learn = container.reporting.learn;
12
+ r.route({
13
+ method: "GET",
14
+ url: "/api/learn/downloads",
15
+ preHandler: app.requireOrgSession,
16
+ schema: {
17
+ operationId: "listLearnDownloads",
18
+ tags: ["Learn"],
19
+ summary: "Downloads the student is actively entitled to",
20
+ response: { 200: LearnDownloads }
21
+ },
22
+ handler: async (req) => {
23
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
24
+ if (!orgUser) throw new UnauthorizedError();
25
+ return learn.listDownloads(req.orgId, orgUser.id);
26
+ }
27
+ });
28
+ r.route({
29
+ method: "GET",
30
+ url: "/api/learn/downloads/:downloadId",
31
+ preHandler: app.requireOrgSession,
32
+ schema: {
33
+ operationId: "getLearnDownload",
34
+ tags: ["Learn"],
35
+ summary: "One entitled download and its ordered assets",
36
+ params: DownloadIdParam,
37
+ response: {
38
+ 200: LearnDownload,
39
+ 404: ErrorBody
40
+ }
41
+ },
42
+ handler: async (req) => {
43
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
44
+ if (!orgUser) throw new UnauthorizedError();
45
+ const result = await learn.getDownload(req.orgId, orgUser.id, req.params.downloadId);
46
+ if (!result) throw new NotFoundError("Download", req.params.downloadId);
47
+ return result;
48
+ }
49
+ });
50
+ r.route({
51
+ method: "GET",
52
+ url: "/api/learn/downloads/:downloadId/assets/:assetId",
53
+ preHandler: app.requireOrgSession,
54
+ schema: {
55
+ operationId: "getLearnDownloadAsset",
56
+ tags: ["Learn"],
57
+ summary: "Redirect to a short-lived signed URL for an entitled asset",
58
+ params: DownloadAssetParams,
59
+ response: {
60
+ 302: z.void(),
61
+ 404: ErrorBody
62
+ }
63
+ },
64
+ handler: async (req, reply) => {
65
+ const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
66
+ if (!orgUser) throw new UnauthorizedError();
67
+ const signed = await learn.downloadAssetUrl(req.orgId, orgUser.id, req.params.downloadId, req.params.assetId);
68
+ if (!signed) throw new NotFoundError("Download asset", req.params.assetId);
69
+ return reply.header("cache-control", "no-store").header("referrer-policy", "no-referrer").redirect(signed.url, 302);
70
+ }
71
+ });
72
+ }
73
+ //#endregion
74
+ export { learnDownloadsRoutes };
75
+
76
+ //# sourceMappingURL=downloads.js.map