@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.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/app/container.d.ts +124 -0
- package/dist/app/container.js +312 -0
- package/dist/app/container.js.map +1 -0
- package/dist/app/db.d.ts +2 -0
- package/dist/app/db.js +2 -0
- package/dist/app/integrations.d.ts +7 -0
- package/dist/app/integrations.js +44 -0
- package/dist/app/integrations.js.map +1 -0
- package/dist/app/notifications.d.ts +7 -0
- package/dist/app/notifications.js +23 -0
- package/dist/app/notifications.js.map +1 -0
- package/dist/http/config.d.ts +25 -0
- package/dist/http/config.js +1 -0
- package/dist/http/fastify.d.ts +35 -0
- package/dist/http/plugins/auth.d.ts +11 -0
- package/dist/http/plugins/auth.js +47 -0
- package/dist/http/plugins/auth.js.map +1 -0
- package/dist/http/plugins/cors.d.ts +7 -0
- package/dist/http/plugins/cors.js +22 -0
- package/dist/http/plugins/cors.js.map +1 -0
- package/dist/http/plugins/error-handler.d.ts +6 -0
- package/dist/http/plugins/error-handler.js +64 -0
- package/dist/http/plugins/error-handler.js.map +1 -0
- package/dist/http/plugins/openapi.d.ts +7 -0
- package/dist/http/plugins/openapi.js +63 -0
- package/dist/http/plugins/openapi.js.map +1 -0
- package/dist/http/routes/activities.d.ts +7 -0
- package/dist/http/routes/activities.js +163 -0
- package/dist/http/routes/activities.js.map +1 -0
- package/dist/http/routes/assets.d.ts +7 -0
- package/dist/http/routes/assets.js +168 -0
- package/dist/http/routes/assets.js.map +1 -0
- package/dist/http/routes/automations.d.ts +7 -0
- package/dist/http/routes/automations.js +158 -0
- package/dist/http/routes/automations.js.map +1 -0
- package/dist/http/routes/courses.d.ts +7 -0
- package/dist/http/routes/courses.js +130 -0
- package/dist/http/routes/courses.js.map +1 -0
- package/dist/http/routes/dashboard.d.ts +7 -0
- package/dist/http/routes/dashboard.js +42 -0
- package/dist/http/routes/dashboard.js.map +1 -0
- package/dist/http/routes/discussion.d.ts +7 -0
- package/dist/http/routes/discussion.js +122 -0
- package/dist/http/routes/discussion.js.map +1 -0
- package/dist/http/routes/downloads.d.ts +7 -0
- package/dist/http/routes/downloads.js +208 -0
- package/dist/http/routes/downloads.js.map +1 -0
- package/dist/http/routes/entitlements.d.ts +7 -0
- package/dist/http/routes/entitlements.js +69 -0
- package/dist/http/routes/entitlements.js.map +1 -0
- package/dist/http/routes/identity.d.ts +6 -0
- package/dist/http/routes/identity.js +6 -0
- package/dist/http/routes/identity.js.map +1 -0
- package/dist/http/routes/integrations.d.ts +7 -0
- package/dist/http/routes/integrations.js +210 -0
- package/dist/http/routes/integrations.js.map +1 -0
- package/dist/http/routes/learn/assets.d.ts +8 -0
- package/dist/http/routes/learn/assets.js +32 -0
- package/dist/http/routes/learn/assets.js.map +1 -0
- package/dist/http/routes/learn/comments.d.ts +7 -0
- package/dist/http/routes/learn/comments.js +175 -0
- package/dist/http/routes/learn/comments.js.map +1 -0
- package/dist/http/routes/learn/courses.d.ts +7 -0
- package/dist/http/routes/learn/courses.js +75 -0
- package/dist/http/routes/learn/courses.js.map +1 -0
- package/dist/http/routes/learn/downloads.d.ts +7 -0
- package/dist/http/routes/learn/downloads.js +76 -0
- package/dist/http/routes/learn/downloads.js.map +1 -0
- package/dist/http/routes/learn/index.d.ts +7 -0
- package/dist/http/routes/learn/index.js +19 -0
- package/dist/http/routes/learn/index.js.map +1 -0
- package/dist/http/routes/learn/org.d.ts +7 -0
- package/dist/http/routes/learn/org.js +50 -0
- package/dist/http/routes/learn/org.js.map +1 -0
- package/dist/http/routes/learn/progress.d.ts +7 -0
- package/dist/http/routes/learn/progress.js +63 -0
- package/dist/http/routes/learn/progress.js.map +1 -0
- package/dist/http/routes/organizations.d.ts +7 -0
- package/dist/http/routes/organizations.js +219 -0
- package/dist/http/routes/organizations.js.map +1 -0
- package/dist/http/routes/progress.d.ts +6 -0
- package/dist/http/routes/progress.js +6 -0
- package/dist/http/routes/progress.js.map +1 -0
- package/dist/http/routes/students.d.ts +7 -0
- package/dist/http/routes/students.js +125 -0
- package/dist/http/routes/students.js.map +1 -0
- package/dist/http/routes.d.ts +7 -0
- package/dist/http/routes.js +37 -0
- package/dist/http/routes.js.map +1 -0
- package/dist/http/schemas/activities.d.ts +69 -0
- package/dist/http/schemas/activities.js +25 -0
- package/dist/http/schemas/activities.js.map +1 -0
- package/dist/http/schemas/assets.d.ts +142 -0
- package/dist/http/schemas/assets.js +24 -0
- package/dist/http/schemas/assets.js.map +1 -0
- package/dist/http/schemas/automations.d.ts +154 -0
- package/dist/http/schemas/automations.js +26 -0
- package/dist/http/schemas/automations.js.map +1 -0
- package/dist/http/schemas/content.d.ts +113 -0
- package/dist/http/schemas/content.js +29 -0
- package/dist/http/schemas/content.js.map +1 -0
- package/dist/http/schemas/dashboard.d.ts +26 -0
- package/dist/http/schemas/dashboard.js +18 -0
- package/dist/http/schemas/dashboard.js.map +1 -0
- package/dist/http/schemas/discussion.d.ts +340 -0
- package/dist/http/schemas/discussion.js +78 -0
- package/dist/http/schemas/discussion.js.map +1 -0
- package/dist/http/schemas/downloads.d.ts +110 -0
- package/dist/http/schemas/downloads.js +34 -0
- package/dist/http/schemas/downloads.js.map +1 -0
- package/dist/http/schemas/entitlements.d.ts +97 -0
- package/dist/http/schemas/entitlements.js +41 -0
- package/dist/http/schemas/entitlements.js.map +1 -0
- package/dist/http/schemas/index.d.ts +17 -0
- package/dist/http/schemas/index.js +17 -0
- package/dist/http/schemas/integrations.d.ts +93 -0
- package/dist/http/schemas/integrations.js +47 -0
- package/dist/http/schemas/integrations.js.map +1 -0
- package/dist/http/schemas/invites.d.ts +95 -0
- package/dist/http/schemas/invites.js +62 -0
- package/dist/http/schemas/invites.js.map +1 -0
- package/dist/http/schemas/learn.d.ts +142 -0
- package/dist/http/schemas/learn.js +58 -0
- package/dist/http/schemas/learn.js.map +1 -0
- package/dist/http/schemas/members.d.ts +88 -0
- package/dist/http/schemas/members.js +27 -0
- package/dist/http/schemas/members.js.map +1 -0
- package/dist/http/schemas/organizations.d.ts +22 -0
- package/dist/http/schemas/organizations.js +20 -0
- package/dist/http/schemas/organizations.js.map +1 -0
- package/dist/http/schemas/settings.d.ts +23 -0
- package/dist/http/schemas/settings.js +20 -0
- package/dist/http/schemas/settings.js.map +1 -0
- package/dist/http/schemas/shared.d.ts +57 -0
- package/dist/http/schemas/shared.js +54 -0
- package/dist/http/schemas/shared.js.map +1 -0
- package/dist/http/schemas/students.d.ts +64 -0
- package/dist/http/schemas/students.js +25 -0
- package/dist/http/schemas/students.js.map +1 -0
- package/dist/http/scope.d.ts +22 -0
- package/dist/http/scope.js +28 -0
- package/dist/http/scope.js.map +1 -0
- package/dist/http/server.d.ts +8 -0
- package/dist/http/server.js +33 -0
- package/dist/http/server.js.map +1 -0
- package/dist/http/web-bridge.d.ts +9 -0
- package/dist/http/web-bridge.js +27 -0
- package/dist/http/web-bridge.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { entitlementEvents } from "@headless-lms/core/entitlements";
|
|
2
|
+
//#region src/app/notifications.ts
|
|
3
|
+
function registerNotificationSubscribers(bus, mailer, lookups) {
|
|
4
|
+
bus.subscribe(entitlementEvents.entitlementCreated, async (event) => {
|
|
5
|
+
const { orgUserId, contentId } = event.data;
|
|
6
|
+
const [to, content] = await Promise.all([lookups.orgUserEmail(event.orgId, orgUserId), lookups.contentInfo(event.orgId, contentId)]);
|
|
7
|
+
if (!to || !content) throw new Error(`entitlement notification lookups failed: ${event.data.id}`);
|
|
8
|
+
await mailer.send(to, "accessGranted", {
|
|
9
|
+
contentTitle: content.title,
|
|
10
|
+
contentId: content.id
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
bus.subscribe(entitlementEvents.entitlementDeleted, async (event) => {
|
|
14
|
+
const { orgUserId, contentId } = event.data;
|
|
15
|
+
const [to, content] = await Promise.all([lookups.orgUserEmail(event.orgId, orgUserId), lookups.contentInfo(event.orgId, contentId)]);
|
|
16
|
+
if (!to || !content) throw new Error(`entitlement notification lookups failed: ${event.data.id}`);
|
|
17
|
+
await mailer.send(to, "accessRevoked", { contentTitle: content.title });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { registerNotificationSubscribers };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","names":[],"sources":["../../src/app/notifications.ts"],"sourcesContent":["// Email side-effects of domain events, subscribed on the EventBus. Events\n// reach the bus via the outbox relay at-least-once: a mailer failure throws\n// through publish, and the relay retries with backoff — no email is silently\n// dropped.\nimport type { EventBus } from '@headless-lms/core/shared/ports';\nimport type { Mailer, MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { entitlementEvents } from '@headless-lms/core/entitlements';\n\nexport function registerNotificationSubscribers(\n bus: EventBus,\n mailer: Pick<Mailer, 'send'>,\n lookups: MailerLookups,\n): void {\n bus.subscribe(entitlementEvents.entitlementCreated, async (event) => {\n const { orgUserId, contentId } = event.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessGranted', {\n contentTitle: content.title,\n contentId: content.id,\n });\n });\n\n bus.subscribe(entitlementEvents.entitlementDeleted, async (event) => {\n const { orgUserId, contentId } = event.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessRevoked', {\n contentTitle: content.title,\n });\n });\n}\n"],"mappings":";;AAQA,SAAgB,gCACd,KACA,QACA,SACM;CACN,IAAI,UAAU,kBAAkB,oBAAoB,OAAO,UAAU;EACnE,MAAM,EAAE,WAAW,cAAc,MAAM;EACvC,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,MAAM,OAAO,SAAS,GAC3C,QAAQ,YAAY,MAAM,OAAO,SAAS,CAC5C,CAAC;EACD,IAAI,CAAC,MAAM,CAAC,SACV,MAAM,IAAI,MAAM,4CAA4C,MAAM,KAAK,IAAI;EAE7E,MAAM,OAAO,KAAK,IAAI,iBAAiB;GACrC,cAAc,QAAQ;GACtB,WAAW,QAAQ;EACrB,CAAC;CACH,CAAC;CAED,IAAI,UAAU,kBAAkB,oBAAoB,OAAO,UAAU;EACnE,MAAM,EAAE,WAAW,cAAc,MAAM;EACvC,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,MAAM,OAAO,SAAS,GAC3C,QAAQ,YAAY,MAAM,OAAO,SAAS,CAC5C,CAAC;EACD,IAAI,CAAC,MAAM,CAAC,SACV,MAAM,IAAI,MAAM,4CAA4C,MAAM,KAAK,IAAI;EAE7E,MAAM,OAAO,KAAK,IAAI,iBAAiB,EACrC,cAAc,QAAQ,MACxB,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Config } from "../app/container.js";
|
|
2
|
+
//#region src/http/config.d.ts
|
|
3
|
+
interface ServerConfig {
|
|
4
|
+
/** TCP port to listen on. */
|
|
5
|
+
port: number;
|
|
6
|
+
/** Bind address. */
|
|
7
|
+
host: string;
|
|
8
|
+
/** The API's own origin, advertised as the OpenAPI server URL. */
|
|
9
|
+
publicUrl: string;
|
|
10
|
+
/** Browser app origins allowed by CORS. */
|
|
11
|
+
clientOrigins: string[];
|
|
12
|
+
/** Emit Fastify's per-request "incoming request"/"request completed" lines.
|
|
13
|
+
* Unset → on. Turn off for a quiet dev console; app-level logs are
|
|
14
|
+
* unaffected either way. */
|
|
15
|
+
requestLogging?: boolean;
|
|
16
|
+
/** Presigned URL lifetime for entitled download delivery, seconds. Separate
|
|
17
|
+
* from the storage adapter's general download expiry so raising that one
|
|
18
|
+
* can't silently extend paywalled link lifetimes. */
|
|
19
|
+
deliveryExpirySeconds: number;
|
|
20
|
+
/** Everything the composition container needs to wire adapters + services. */
|
|
21
|
+
container: Config;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ServerConfig };
|
|
25
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "fastify";
|
|
2
|
+
import { User } from "@headless-lms/core/types";
|
|
3
|
+
//#region src/http/fastify.d.ts
|
|
4
|
+
/** The session's account as the HTTP layer sees it. Re-exported from the
|
|
5
|
+
* package entry point so this module — and its augmentation — gets loaded. */
|
|
6
|
+
type AuthUser = User;
|
|
7
|
+
declare module 'fastify' {
|
|
8
|
+
interface FastifyInstance {
|
|
9
|
+
/** Session guard. Populates `authUser` (and `orgId` when the session has an
|
|
10
|
+
* active org), or throws `UnauthorizedError`. Use for the routes reached
|
|
11
|
+
* before the caller has an org — org creation, invite acceptance. */
|
|
12
|
+
requireSession(request: FastifyRequest): Promise<void>;
|
|
13
|
+
/** As `requireSession`, but also requires an active org on the session. */
|
|
14
|
+
requireOrgSession(request: FastifyRequest): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
interface FastifyRequest {
|
|
17
|
+
authUser: User;
|
|
18
|
+
/** Domain `organizations.id` for the session's active org — what every
|
|
19
|
+
* domain read keys off. Guaranteed set by `requireOrgSession`; absent under
|
|
20
|
+
* `requireSession` when the session has no active org. */
|
|
21
|
+
orgId: string;
|
|
22
|
+
/** The same org as better-auth knows it (`organizations.external_id`), for
|
|
23
|
+
* writes that go back through the auth provider. Equal to `orgId` today:
|
|
24
|
+
* the org-create hook gives better-auth the domain id and records it as
|
|
25
|
+
* the external id. */
|
|
26
|
+
authOrgId: string;
|
|
27
|
+
/** Domain `users.id` for the session's account — what `org_users.user_id`
|
|
28
|
+
* and every other domain row references. Not `authUser.id`: the two match
|
|
29
|
+
* only for a self-signup, never for a person an admin provisioned first. */
|
|
30
|
+
userId: string;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { AuthUser };
|
|
35
|
+
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Container } from "../../app/container.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/plugins/auth.d.ts
|
|
4
|
+
/** The request carries no session. The central error handler maps this to 401. */
|
|
5
|
+
declare class UnauthorizedError extends Error {
|
|
6
|
+
constructor(message?: string);
|
|
7
|
+
}
|
|
8
|
+
declare function registerAuth(app: FastifyInstance, appServer: Container): void;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { UnauthorizedError, registerAuth };
|
|
11
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { bridgeWebResponse, toWebRequest } from "../web-bridge.js";
|
|
2
|
+
//#region src/http/plugins/auth.ts
|
|
3
|
+
/** The request carries no session. The central error handler maps this to 401. */
|
|
4
|
+
var UnauthorizedError = class extends Error {
|
|
5
|
+
constructor(message = "no session on the request") {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "UnauthorizedError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function registerAuth(app, appServer) {
|
|
11
|
+
const { auth } = appServer;
|
|
12
|
+
app.route({
|
|
13
|
+
method: ["GET", "POST"],
|
|
14
|
+
url: "/api/auth/*",
|
|
15
|
+
async handler(request, reply) {
|
|
16
|
+
await bridgeWebResponse(await auth.handler(toWebRequest(request)), reply);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
app.decorate("requireSession", async (request) => {
|
|
20
|
+
const session = await appServer.auth.verify(request.headers);
|
|
21
|
+
if (!session) throw new UnauthorizedError();
|
|
22
|
+
request.authUser = session.user;
|
|
23
|
+
request.userId = session.user.id;
|
|
24
|
+
const authOrgId = session.session.activeOrganizationId;
|
|
25
|
+
if (authOrgId) {
|
|
26
|
+
request.authOrgId = authOrgId;
|
|
27
|
+
const org = await appServer.organizations.getByExternalId(authOrgId);
|
|
28
|
+
if (org) request.orgId = org.id;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
app.decorate("requireOrgSession", async (request) => {
|
|
32
|
+
const session = await appServer.auth.verify(request.headers);
|
|
33
|
+
if (!session) throw new UnauthorizedError();
|
|
34
|
+
const authOrgId = session.session.activeOrganizationId;
|
|
35
|
+
if (!authOrgId) throw new UnauthorizedError();
|
|
36
|
+
const org = await appServer.organizations.getByExternalId(authOrgId);
|
|
37
|
+
if (!org) throw new UnauthorizedError("session organization not found");
|
|
38
|
+
request.authUser = session.user;
|
|
39
|
+
request.userId = session.user.id;
|
|
40
|
+
request.orgId = org.id;
|
|
41
|
+
request.authOrgId = authOrgId;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { UnauthorizedError, registerAuth };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","names":[],"sources":["../../../src/http/plugins/auth.ts"],"sourcesContent":["// Everything better-auth touches on the HTTP surface:\n// - the /api/auth/* catch-all bridged to better-auth's Web handler\n// - the `requireSession` / `requireOrgSession` decorators routes guard with\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { Container } from '../../app/container.js';\nimport { bridgeWebResponse, toWebRequest } from '../web-bridge.js';\n\n/** The request carries no session. The central error handler maps this to 401. */\nexport class UnauthorizedError extends Error {\n constructor(message = 'no session on the request') {\n super(message);\n this.name = 'UnauthorizedError';\n }\n}\n\nexport function registerAuth(app: FastifyInstance, appServer: Container): void {\n const { auth } = appServer;\n\n // Mount better-auth: a catch-all that bridges Fastify <-> Web Request/Response.\n app.route({\n method: ['GET', 'POST'],\n url: '/api/auth/*',\n async handler(request, reply) {\n await bridgeWebResponse(await auth.handler(toWebRequest(request)), reply);\n },\n });\n\n // Session only, no active org required. For the routes a caller reaches\n // *before* they have an org: creating their first organization, and accepting\n // an invitation (which is what stamps the active org onto the session).\n app.decorate('requireSession', async (request: FastifyRequest) => {\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n request.authUser = session.user;\n request.userId = session.user.id;\n\n const authOrgId = session.session.activeOrganizationId;\n if (authOrgId) {\n request.authOrgId = authOrgId;\n const org = await appServer.organizations.getByExternalId(authOrgId);\n if (org) {\n request.orgId = org.id;\n }\n }\n });\n\n // Requires a valid session with an active org set — every org-scoped route.\n app.decorate('requireOrgSession', async (request: FastifyRequest) => {\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n const authOrgId = session.session.activeOrganizationId;\n if (!authOrgId) {\n throw new UnauthorizedError();\n }\n\n const org = await appServer.organizations.getByExternalId(authOrgId);\n if (!org) {\n throw new UnauthorizedError('session organization not found');\n }\n request.authUser = session.user;\n request.userId = session.user.id;\n request.orgId = org.id;\n // resolveScope requires the auth-provider id too — routes that write back\n // through better-auth key on it.\n request.authOrgId = authOrgId;\n });\n}\n"],"mappings":";;;AAQA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,UAAU,6BAA6B;EACjD,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,aAAa,KAAsB,WAA4B;CAC7E,MAAM,EAAE,SAAS;CAGjB,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,MAAM;EACtB,KAAK;EACL,MAAM,QAAQ,SAAS,OAAO;GAC5B,MAAM,kBAAkB,MAAM,KAAK,QAAQ,aAAa,OAAO,CAAC,GAAG,KAAK;EAC1E;CACF,CAAC;CAKD,IAAI,SAAS,kBAAkB,OAAO,YAA4B;EAChE,MAAM,UAAU,MAAM,UAAU,KAAK,OAAO,QAAQ,OAAO;EAC3D,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;EAE9B,QAAQ,WAAW,QAAQ;EAC3B,QAAQ,SAAS,QAAQ,KAAK;EAE9B,MAAM,YAAY,QAAQ,QAAQ;EAClC,IAAI,WAAW;GACb,QAAQ,YAAY;GACpB,MAAM,MAAM,MAAM,UAAU,cAAc,gBAAgB,SAAS;GACnE,IAAI,KACF,QAAQ,QAAQ,IAAI;EAExB;CACF,CAAC;CAGD,IAAI,SAAS,qBAAqB,OAAO,YAA4B;EACnE,MAAM,UAAU,MAAM,UAAU,KAAK,OAAO,QAAQ,OAAO;EAC3D,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;EAE9B,MAAM,YAAY,QAAQ,QAAQ;EAClC,IAAI,CAAC,WACH,MAAM,IAAI,kBAAkB;EAG9B,MAAM,MAAM,MAAM,UAAU,cAAc,gBAAgB,SAAS;EACnE,IAAI,CAAC,KACH,MAAM,IAAI,kBAAkB,gCAAgC;EAE9D,QAAQ,WAAW,QAAQ;EAC3B,QAAQ,SAAS,QAAQ,KAAK;EAC9B,QAAQ,QAAQ,IAAI;EAGpB,QAAQ,YAAY;CACtB,CAAC;AACH"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ServerConfig } from "../config.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/plugins/cors.d.ts
|
|
4
|
+
declare function registerCors(app: FastifyInstance, config: ServerConfig): void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { registerCors };
|
|
7
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import cors from "@fastify/cors";
|
|
2
|
+
//#region src/http/plugins/cors.ts
|
|
3
|
+
function registerCors(app, config) {
|
|
4
|
+
app.register(cors, {
|
|
5
|
+
origin: config.clientOrigins,
|
|
6
|
+
methods: [
|
|
7
|
+
"GET",
|
|
8
|
+
"POST",
|
|
9
|
+
"PUT",
|
|
10
|
+
"PATCH",
|
|
11
|
+
"DELETE",
|
|
12
|
+
"OPTIONS"
|
|
13
|
+
],
|
|
14
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
15
|
+
credentials: true,
|
|
16
|
+
maxAge: 86400
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { registerCors };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=cors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.js","names":[],"sources":["../../../src/http/plugins/cors.ts"],"sourcesContent":["// CORS for the browser apps (student web + admin). Credentials must be allowed\n// so the SPA/admin can carry the better-auth session cookie cross-origin.\nimport type { FastifyInstance } from 'fastify';\nimport cors from '@fastify/cors';\nimport type { ServerConfig } from '../config.js';\n\nexport function registerCors(app: FastifyInstance, config: ServerConfig): void {\n app.register(cors, {\n origin: config.clientOrigins,\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n allowedHeaders: ['Content-Type', 'Authorization'],\n credentials: true,\n maxAge: 86400,\n });\n}\n"],"mappings":";;AAMA,SAAgB,aAAa,KAAsB,QAA4B;CAC7E,IAAI,SAAS,MAAM;EACjB,QAAQ,OAAO;EACf,SAAS;GAAC;GAAO;GAAQ;GAAO;GAAS;GAAU;EAAS;EAC5D,gBAAgB,CAAC,gBAAgB,eAAe;EAChD,aAAa;EACb,QAAQ;CACV,CAAC;AACH"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
//#region src/http/plugins/error-handler.d.ts
|
|
3
|
+
declare function errorHandler(error: unknown, _request: FastifyRequest, reply: FastifyReply): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { errorHandler };
|
|
6
|
+
//# sourceMappingURL=error-handler.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { UnauthorizedError } from "./auth.js";
|
|
2
|
+
import { NoActiveOrgError } from "../scope.js";
|
|
3
|
+
import { OrganizationRuleError } from "@headless-lms/core/organizations";
|
|
4
|
+
import { ActionInvocationError, AlreadyConnectedError, InvalidConfigError, UnknownIntegrationError } from "@headless-lms/core/integrations";
|
|
5
|
+
import { InvalidTriggerError } from "@headless-lms/core/automations";
|
|
6
|
+
import { ConflictError, ForbiddenError, NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
|
+
//#region src/http/plugins/error-handler.ts
|
|
8
|
+
function errorHandler(error, _request, reply) {
|
|
9
|
+
if (error instanceof NotFoundError) return reply.status(404).send({
|
|
10
|
+
error: "not_found",
|
|
11
|
+
message: error.message
|
|
12
|
+
});
|
|
13
|
+
if (error instanceof ConflictError) return reply.status(409).send({
|
|
14
|
+
error: "conflict",
|
|
15
|
+
message: error.message
|
|
16
|
+
});
|
|
17
|
+
if (error instanceof ForbiddenError) return reply.status(403).send({
|
|
18
|
+
error: "forbidden",
|
|
19
|
+
message: error.message
|
|
20
|
+
});
|
|
21
|
+
if (error instanceof OrganizationRuleError) return reply.status(409).send({
|
|
22
|
+
error: "conflict",
|
|
23
|
+
message: error.message
|
|
24
|
+
});
|
|
25
|
+
if (error instanceof AlreadyConnectedError) return reply.status(409).send({
|
|
26
|
+
error: "already_connected",
|
|
27
|
+
message: error.message
|
|
28
|
+
});
|
|
29
|
+
if (error instanceof UnknownIntegrationError) return reply.status(400).send({
|
|
30
|
+
error: "unknown_integration",
|
|
31
|
+
message: error.message
|
|
32
|
+
});
|
|
33
|
+
if (error instanceof InvalidConfigError) return reply.status(400).send({
|
|
34
|
+
error: "invalid_config",
|
|
35
|
+
message: error.message
|
|
36
|
+
});
|
|
37
|
+
if (error instanceof ActionInvocationError) return reply.status(502).send({
|
|
38
|
+
error: "action_failed",
|
|
39
|
+
message: error.message
|
|
40
|
+
});
|
|
41
|
+
if (error instanceof InvalidTriggerError) return reply.status(400).send({
|
|
42
|
+
error: "invalid_trigger",
|
|
43
|
+
message: error.message
|
|
44
|
+
});
|
|
45
|
+
if (error instanceof NoActiveOrgError) return reply.status(403).send({
|
|
46
|
+
error: "forbidden",
|
|
47
|
+
message: error.message
|
|
48
|
+
});
|
|
49
|
+
if (error instanceof UnauthorizedError) return reply.status(401).send({
|
|
50
|
+
error: "unauthorized",
|
|
51
|
+
message: error.message
|
|
52
|
+
});
|
|
53
|
+
const err = error;
|
|
54
|
+
if (typeof err.statusCode === "number" && err.statusCode >= 400 && err.statusCode < 500) return reply.status(err.statusCode).send({
|
|
55
|
+
error: err.code ?? "bad_request",
|
|
56
|
+
message: err.message ?? "Bad request"
|
|
57
|
+
});
|
|
58
|
+
_request.log.error(error);
|
|
59
|
+
return reply.status(500).send({ error: "internal_error" });
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { errorHandler };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/http/plugins/error-handler.ts"],"sourcesContent":["import { ConflictError, ForbiddenError, NotFoundError } from '@headless-lms/core/shared/errors';\nimport { OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport { InvalidTriggerError } from '@headless-lms/core/automations';\nimport { NoActiveOrgError } from '../scope.js';\nimport { UnauthorizedError } from './auth.js';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\n\nexport function errorHandler(error: unknown, _request: FastifyRequest, reply: FastifyReply) {\n if (error instanceof NotFoundError) {\n return reply.status(404).send({ error: 'not_found', message: error.message });\n }\n if (error instanceof ConflictError) {\n return reply.status(409).send({ error: 'conflict', message: error.message });\n }\n if (error instanceof ForbiddenError) {\n return reply.status(403).send({ error: 'forbidden', message: error.message });\n }\n if (error instanceof OrganizationRuleError) {\n return reply.status(409).send({ error: 'conflict', message: error.message });\n }\n if (error instanceof AlreadyConnectedError) {\n return reply.status(409).send({ error: 'already_connected', message: error.message });\n }\n if (error instanceof UnknownIntegrationError) {\n return reply.status(400).send({ error: 'unknown_integration', message: error.message });\n }\n if (error instanceof InvalidConfigError) {\n return reply.status(400).send({ error: 'invalid_config', message: error.message });\n }\n if (error instanceof ActionInvocationError) {\n return reply.status(502).send({ error: 'action_failed', message: error.message });\n }\n if (error instanceof InvalidTriggerError) {\n return reply.status(400).send({ error: 'invalid_trigger', message: error.message });\n }\n if (error instanceof NoActiveOrgError) {\n return reply.status(403).send({ error: 'forbidden', message: error.message });\n }\n // No session at all — 401 so the client bounces to login, not a generic 403.\n if (error instanceof UnauthorizedError) {\n return reply.status(401).send({ error: 'unauthorized', message: error.message });\n }\n const err = error as { statusCode?: number; code?: string; message?: string };\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return reply\n .status(err.statusCode)\n .send({ error: err.code ?? 'bad_request', message: err.message ?? 'Bad request' });\n }\n _request.log.error(error);\n return reply.status(500).send({ error: 'internal_error' });\n}\n"],"mappings":";;;;;;;AAaA,SAAgB,aAAa,OAAgB,UAA0B,OAAqB;CAC1F,IAAI,iBAAiB,eACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAE9E,IAAI,iBAAiB,eACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAY,SAAS,MAAM;CAAQ,CAAC;CAE7E,IAAI,iBAAiB,gBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAE9E,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAY,SAAS,MAAM;CAAQ,CAAC;CAE7E,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAqB,SAAS,MAAM;CAAQ,CAAC;CAEtF,IAAI,iBAAiB,yBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAuB,SAAS,MAAM;CAAQ,CAAC;CAExF,IAAI,iBAAiB,oBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAkB,SAAS,MAAM;CAAQ,CAAC;CAEnF,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAiB,SAAS,MAAM;CAAQ,CAAC;CAElF,IAAI,iBAAiB,qBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAmB,SAAS,MAAM;CAAQ,CAAC;CAEpF,IAAI,iBAAiB,kBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAG9E,IAAI,iBAAiB,mBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAgB,SAAS,MAAM;CAAQ,CAAC;CAEjF,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,eAAe,YAAY,IAAI,cAAc,OAAO,IAAI,aAAa,KAClF,OAAO,MACJ,OAAO,IAAI,UAAU,CAAC,CACtB,KAAK;EAAE,OAAO,IAAI,QAAQ;EAAe,SAAS,IAAI,WAAW;CAAc,CAAC;CAErF,SAAS,IAAI,MAAM,KAAK;CACxB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAC3D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ServerConfig } from "../config.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/plugins/openapi.d.ts
|
|
4
|
+
declare function registerOpenApi(app: FastifyInstance, config: ServerConfig): void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { registerOpenApi };
|
|
7
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsonSchemaTransform } from "fastify-type-provider-zod";
|
|
2
|
+
import swagger from "@fastify/swagger";
|
|
3
|
+
import swaggerUi from "@fastify/swagger-ui";
|
|
4
|
+
//#region src/http/plugins/openapi.ts
|
|
5
|
+
function registerOpenApi(app, config) {
|
|
6
|
+
app.register(swagger, {
|
|
7
|
+
openapi: {
|
|
8
|
+
info: {
|
|
9
|
+
title: "Headless LMS API",
|
|
10
|
+
version: "0.0.0"
|
|
11
|
+
},
|
|
12
|
+
servers: [{ url: config.publicUrl }],
|
|
13
|
+
tags: [
|
|
14
|
+
{
|
|
15
|
+
name: "Organizations",
|
|
16
|
+
description: "Organizations and member management"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "Courses",
|
|
20
|
+
description: "Course authoring: courses, modules, activities"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Learn",
|
|
24
|
+
description: "Student-facing course delivery"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "Students",
|
|
28
|
+
description: "Students and their enrollments"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Entitlements",
|
|
32
|
+
description: "Access grants to content"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Progress",
|
|
36
|
+
description: "Completion tracking"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "Automations",
|
|
40
|
+
description: "Trigger/action workflows"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "Dashboard",
|
|
44
|
+
description: "Back-office overview"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "Assets",
|
|
48
|
+
description: "Media library and uploads"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "Integrations",
|
|
52
|
+
description: "Third-party integration connections"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
transform: jsonSchemaTransform
|
|
57
|
+
});
|
|
58
|
+
app.register(swaggerUi, { routePrefix: "/docs" });
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { registerOpenApi };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","names":[],"sources":["../../../src/http/plugins/openapi.ts"],"sourcesContent":["// OpenAPI documentation. @fastify/swagger reads the Zod route schemas (via the\n// transform) to build the spec the frontend SDK is generated from; it must be\n// registered before the routes so its onRoute hook captures them. UI + JSON are\n// served under /docs.\nimport type { FastifyInstance } from 'fastify';\nimport swagger from '@fastify/swagger';\nimport swaggerUi from '@fastify/swagger-ui';\nimport { jsonSchemaTransform } from 'fastify-type-provider-zod';\nimport type { ServerConfig } from '../config.js';\n\nexport function registerOpenApi(app: FastifyInstance, config: ServerConfig): void {\n app.register(swagger, {\n openapi: {\n info: { title: 'Headless LMS API', version: '0.0.0' },\n servers: [{ url: config.publicUrl }],\n tags: [\n { name: 'Organizations', description: 'Organizations and member management' },\n { name: 'Courses', description: 'Course authoring: courses, modules, activities' },\n { name: 'Learn', description: 'Student-facing course delivery' },\n { name: 'Students', description: 'Students and their enrollments' },\n { name: 'Entitlements', description: 'Access grants to content' },\n { name: 'Progress', description: 'Completion tracking' },\n { name: 'Automations', description: 'Trigger/action workflows' },\n { name: 'Dashboard', description: 'Back-office overview' },\n { name: 'Assets', description: 'Media library and uploads' },\n { name: 'Integrations', description: 'Third-party integration connections' },\n ],\n },\n transform: jsonSchemaTransform,\n });\n app.register(swaggerUi, { routePrefix: '/docs' });\n}\n"],"mappings":";;;;AAUA,SAAgB,gBAAgB,KAAsB,QAA4B;CAChF,IAAI,SAAS,SAAS;EACpB,SAAS;GACP,MAAM;IAAE,OAAO;IAAoB,SAAS;GAAQ;GACpD,SAAS,CAAC,EAAE,KAAK,OAAO,UAAU,CAAC;GACnC,MAAM;IACJ;KAAE,MAAM;KAAiB,aAAa;IAAsC;IAC5E;KAAE,MAAM;KAAW,aAAa;IAAiD;IACjF;KAAE,MAAM;KAAS,aAAa;IAAiC;IAC/D;KAAE,MAAM;KAAY,aAAa;IAAiC;IAClE;KAAE,MAAM;KAAgB,aAAa;IAA2B;IAChE;KAAE,MAAM;KAAY,aAAa;IAAsB;IACvD;KAAE,MAAM;KAAe,aAAa;IAA2B;IAC/D;KAAE,MAAM;KAAa,aAAa;IAAuB;IACzD;KAAE,MAAM;KAAU,aAAa;IAA4B;IAC3D;KAAE,MAAM;KAAgB,aAAa;IAAsC;GAC7E;EACF;EACA,WAAW;CACb,CAAC;CACD,IAAI,SAAS,WAAW,EAAE,aAAa,QAAQ,CAAC;AAClD"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Container } from "../../app/container.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/routes/activities.d.ts
|
|
4
|
+
declare function activitiesRoutes(app: FastifyInstance, container: Container): Promise<void>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { activitiesRoutes };
|
|
7
|
+
//# sourceMappingURL=activities.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolveScope } from "../scope.js";
|
|
2
|
+
import { ActivityPathParam, CourseIdPathParam, CreateModule, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule } from "../schemas/activities.js";
|
|
3
|
+
import "../schemas/index.js";
|
|
4
|
+
//#region src/http/routes/activities.ts
|
|
5
|
+
async function activitiesRoutes(app, container) {
|
|
6
|
+
const r = app.withTypeProvider();
|
|
7
|
+
const content = container.content;
|
|
8
|
+
const tags = ["Courses"];
|
|
9
|
+
r.route({
|
|
10
|
+
method: "GET",
|
|
11
|
+
url: "/api/courses/:courseId/modules",
|
|
12
|
+
preHandler: app.requireOrgSession,
|
|
13
|
+
schema: {
|
|
14
|
+
operationId: "listModules",
|
|
15
|
+
tags,
|
|
16
|
+
summary: "List a course's modules",
|
|
17
|
+
params: CourseIdPathParam,
|
|
18
|
+
response: { 200: ModuleList }
|
|
19
|
+
},
|
|
20
|
+
handler: async (req) => {
|
|
21
|
+
const scope = await resolveScope(container, req);
|
|
22
|
+
return content.listCourseModules(scope.orgId, req.params.courseId);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
r.route({
|
|
26
|
+
method: "POST",
|
|
27
|
+
url: "/api/courses/:courseId/modules",
|
|
28
|
+
preHandler: app.requireOrgSession,
|
|
29
|
+
schema: {
|
|
30
|
+
operationId: "createModule",
|
|
31
|
+
tags,
|
|
32
|
+
summary: "Add a module",
|
|
33
|
+
params: CourseIdPathParam,
|
|
34
|
+
body: CreateModule,
|
|
35
|
+
response: { 200: ModuleList }
|
|
36
|
+
},
|
|
37
|
+
handler: async (req) => {
|
|
38
|
+
const scope = await resolveScope(container, req);
|
|
39
|
+
return content.createModule(scope.orgId, req.params.courseId, req.body.title);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
r.route({
|
|
43
|
+
method: "POST",
|
|
44
|
+
url: "/api/courses/:courseId/modules/reorder",
|
|
45
|
+
preHandler: app.requireOrgSession,
|
|
46
|
+
schema: {
|
|
47
|
+
operationId: "reorderModules",
|
|
48
|
+
tags,
|
|
49
|
+
summary: "Reorder modules",
|
|
50
|
+
params: CourseIdPathParam,
|
|
51
|
+
body: ReorderInput,
|
|
52
|
+
response: { 200: ModuleList }
|
|
53
|
+
},
|
|
54
|
+
handler: async (req) => {
|
|
55
|
+
const scope = await resolveScope(container, req);
|
|
56
|
+
return content.reorderModules(scope.orgId, req.params.courseId, req.body.orderedIds);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
r.route({
|
|
60
|
+
method: "PATCH",
|
|
61
|
+
url: "/api/courses/:courseId/modules/:moduleId",
|
|
62
|
+
preHandler: app.requireOrgSession,
|
|
63
|
+
schema: {
|
|
64
|
+
operationId: "updateModule",
|
|
65
|
+
tags,
|
|
66
|
+
summary: "Rename a module",
|
|
67
|
+
params: ModulePathParam,
|
|
68
|
+
body: UpdateModule,
|
|
69
|
+
response: { 200: ModuleList }
|
|
70
|
+
},
|
|
71
|
+
handler: async (req) => {
|
|
72
|
+
const scope = await resolveScope(container, req);
|
|
73
|
+
return content.updateModule(scope.orgId, req.params.courseId, req.params.moduleId, req.body.title);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
r.route({
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
url: "/api/courses/:courseId/modules/:moduleId",
|
|
79
|
+
preHandler: app.requireOrgSession,
|
|
80
|
+
schema: {
|
|
81
|
+
operationId: "deleteModule",
|
|
82
|
+
tags,
|
|
83
|
+
summary: "Delete a module",
|
|
84
|
+
params: ModulePathParam,
|
|
85
|
+
response: { 200: ModuleList }
|
|
86
|
+
},
|
|
87
|
+
handler: async (req) => {
|
|
88
|
+
const scope = await resolveScope(container, req);
|
|
89
|
+
return content.deleteModule(scope.orgId, req.params.courseId, req.params.moduleId);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
r.route({
|
|
93
|
+
method: "POST",
|
|
94
|
+
url: "/api/courses/:courseId/modules/:moduleId/activities/reorder",
|
|
95
|
+
preHandler: app.requireOrgSession,
|
|
96
|
+
schema: {
|
|
97
|
+
operationId: "reorderActivities",
|
|
98
|
+
tags,
|
|
99
|
+
summary: "Reorder activities in a module",
|
|
100
|
+
params: ModulePathParam,
|
|
101
|
+
body: ReorderInput,
|
|
102
|
+
response: { 200: ModuleList }
|
|
103
|
+
},
|
|
104
|
+
handler: async (req) => {
|
|
105
|
+
const scope = await resolveScope(container, req);
|
|
106
|
+
return content.reorderActivities(scope.orgId, req.params.courseId, req.params.moduleId, req.body.orderedIds);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
r.route({
|
|
110
|
+
method: "POST",
|
|
111
|
+
url: "/api/courses/:courseId/modules/:moduleId/activities",
|
|
112
|
+
preHandler: app.requireOrgSession,
|
|
113
|
+
schema: {
|
|
114
|
+
operationId: "createActivity",
|
|
115
|
+
tags,
|
|
116
|
+
summary: "Add an activity",
|
|
117
|
+
params: ModulePathParam,
|
|
118
|
+
body: SaveActivity,
|
|
119
|
+
response: { 200: ModuleList }
|
|
120
|
+
},
|
|
121
|
+
handler: async (req) => {
|
|
122
|
+
const scope = await resolveScope(container, req);
|
|
123
|
+
return content.saveActivity(scope.orgId, req.params.courseId, req.params.moduleId, req.body);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
r.route({
|
|
127
|
+
method: "PATCH",
|
|
128
|
+
url: "/api/courses/:courseId/modules/:moduleId/activities/:activityId",
|
|
129
|
+
preHandler: app.requireOrgSession,
|
|
130
|
+
schema: {
|
|
131
|
+
operationId: "updateActivity",
|
|
132
|
+
tags,
|
|
133
|
+
summary: "Update an activity",
|
|
134
|
+
params: ActivityPathParam,
|
|
135
|
+
body: SaveActivity,
|
|
136
|
+
response: { 200: ModuleList }
|
|
137
|
+
},
|
|
138
|
+
handler: async (req) => {
|
|
139
|
+
const scope = await resolveScope(container, req);
|
|
140
|
+
return content.saveActivity(scope.orgId, req.params.courseId, req.params.moduleId, req.body, req.params.activityId);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
r.route({
|
|
144
|
+
method: "DELETE",
|
|
145
|
+
url: "/api/courses/:courseId/modules/:moduleId/activities/:activityId",
|
|
146
|
+
preHandler: app.requireOrgSession,
|
|
147
|
+
schema: {
|
|
148
|
+
operationId: "deleteActivity",
|
|
149
|
+
tags,
|
|
150
|
+
summary: "Delete an activity",
|
|
151
|
+
params: ActivityPathParam,
|
|
152
|
+
response: { 200: ModuleList }
|
|
153
|
+
},
|
|
154
|
+
handler: async (req) => {
|
|
155
|
+
const scope = await resolveScope(container, req);
|
|
156
|
+
return content.deleteActivity(scope.orgId, req.params.courseId, req.params.moduleId, req.params.activityId);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { activitiesRoutes };
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=activities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activities.js","names":[],"sources":["../../../src/http/routes/activities.ts"],"sourcesContent":["// HTTP routes for course modules + activities. Write operations return the\n// course's full, reordered module list (how the editor re-renders). Nested under\n// courses. All require a session with an active organization; handlers resolve\n// the session's active org to the domain org id and scope every call by it.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n ActivityPathParam,\n CourseIdPathParam,\n CreateModule,\n ModuleList,\n ModulePathParam,\n ReorderInput,\n SaveActivity,\n UpdateModule,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function activitiesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n // Modules/activities are part of the content aggregate; grouped under the Courses tag.\n const content = container.content;\n const tags = ['Courses'];\n\n r.route({\n method: 'GET',\n url: '/api/courses/:courseId/modules',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listModules',\n tags,\n summary: \"List a course's modules\",\n params: CourseIdPathParam,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listCourseModules(scope.orgId, req.params.courseId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses/:courseId/modules',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createModule',\n tags,\n summary: 'Add a module',\n params: CourseIdPathParam,\n body: CreateModule,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.createModule(scope.orgId, req.params.courseId, req.body.title);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses/:courseId/modules/reorder',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderModules',\n tags,\n summary: 'Reorder modules',\n params: CourseIdPathParam,\n body: ReorderInput,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderModules(scope.orgId, req.params.courseId, req.body.orderedIds);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:courseId/modules/:moduleId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateModule',\n tags,\n summary: 'Rename a module',\n params: ModulePathParam,\n body: UpdateModule,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateModule(\n scope.orgId,\n req.params.courseId,\n req.params.moduleId,\n req.body.title,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/courses/:courseId/modules/:moduleId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteModule',\n tags,\n summary: 'Delete a module',\n params: ModulePathParam,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.deleteModule(scope.orgId, req.params.courseId, req.params.moduleId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses/:courseId/modules/:moduleId/activities/reorder',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderActivities',\n tags,\n summary: 'Reorder activities in a module',\n params: ModulePathParam,\n body: ReorderInput,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderActivities(\n scope.orgId,\n req.params.courseId,\n req.params.moduleId,\n req.body.orderedIds,\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses/:courseId/modules/:moduleId/activities',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createActivity',\n tags,\n summary: 'Add an activity',\n params: ModulePathParam,\n body: SaveActivity,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.saveActivity(scope.orgId, req.params.courseId, req.params.moduleId, req.body);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:courseId/modules/:moduleId/activities/:activityId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateActivity',\n tags,\n summary: 'Update an activity',\n params: ActivityPathParam,\n body: SaveActivity,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.saveActivity(\n scope.orgId,\n req.params.courseId,\n req.params.moduleId,\n req.body,\n req.params.activityId,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/courses/:courseId/modules/:moduleId/activities/:activityId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteActivity',\n tags,\n summary: 'Delete an activity',\n params: ActivityPathParam,\n response: { 200: ModuleList },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.deleteActivity(\n scope.orgId,\n req.params.courseId,\n req.params.moduleId,\n req.params.activityId,\n );\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/assets.d.ts
|
|
4
|
+
declare function assetsRoutes(app: FastifyInstance, container: Container): Promise<void>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { assetsRoutes };
|
|
7
|
+
//# sourceMappingURL=assets.d.ts.map
|