forked from EduCraft/curriculum-project-hub
4e2699d0a5
registerStaticSpa was never mounted, so production only exposed org-admin APIs. Wire it after auth/API routes, fold admin:build into npm run build, and install admin-web deps during deploy so admin-web/build ships with the release for same-origin /admin/*.
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
/**
|
|
* Serves the org-admin SPA (built by SvelteKit via `admin-web/build/`) and
|
|
* the SPA index fallback for client-side routes under `/admin/*`.
|
|
*
|
|
* The SvelteKit project lives in `hub/admin-web/`. Run `npm run build` there
|
|
* to produce the static output in `admin-web/build/`. In development, use
|
|
* `npm run dev` in `admin-web/` which proxies `/api` and `/auth` to the Hub.
|
|
*
|
|
* Override the UI directory with `CPH_ADMIN_UI_DIR` env if needed.
|
|
*/
|
|
import { readFile } from "node:fs/promises";
|
|
import { existsSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, extname, join, resolve as resolvePath } from "node:path";
|
|
import type { FastifyInstance } from "fastify";
|
|
|
|
const MIME: Record<string, string> = {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".mjs": "text/javascript; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
".svg": "image/svg+xml",
|
|
".ico": "image/x-icon",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
".json": "application/json; charset=utf-8",
|
|
".txt": "text/plain; charset=utf-8",
|
|
};
|
|
|
|
function resolveUiDir(): string {
|
|
const override = process.env["CPH_ADMIN_UI_DIR"];
|
|
if (override && override.trim() !== "") return resolvePath(override);
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
return resolvePath(join(here, "..", "..", "admin-web", "build"));
|
|
}
|
|
|
|
export async function registerStaticSpa(app: FastifyInstance): Promise<void> {
|
|
const uiDir = resolveUiDir();
|
|
if (!existsSync(join(uiDir, "index.html"))) {
|
|
app.log.warn(
|
|
{ uiDir },
|
|
"admin-web/build not found; SPA shell disabled. Run `npm run build` in admin-web/ to enable. Org admin APIs remain fully functional.",
|
|
);
|
|
return;
|
|
}
|
|
const indexHtml = await readFile(join(uiDir, "index.html"), "utf8");
|
|
|
|
// SvelteKit static assets (_app/*, favicon, etc.)
|
|
app.get("/_app/*", async (request, reply) => {
|
|
const rel = (request.params as { "*": string })["*"];
|
|
const safe = rel.split("/").filter((p) => p !== ".." && p !== "").join("/");
|
|
try {
|
|
const buf = await readFile(join(uiDir, "_app", safe));
|
|
const mime = MIME[extname(safe)] ?? "application/octet-stream";
|
|
return reply.type(mime).send(buf);
|
|
} catch {
|
|
return reply.status(404).send({ error: { code: "not_found", message: "asset not found" } });
|
|
}
|
|
});
|
|
|
|
// Other top-level static assets (favicon.svg, robots.txt, etc.)
|
|
app.get("/favicon.svg", async (_request, reply) => {
|
|
try {
|
|
const buf = await readFile(join(uiDir, "favicon.svg"));
|
|
return reply.type("image/svg+xml").send(buf);
|
|
} catch {
|
|
return reply.status(404).send();
|
|
}
|
|
});
|
|
|
|
// SPA client-side route fallback. Auth registers `/admin/login` first so it
|
|
// takes precedence; everything else under /admin/* serves the index so
|
|
// SvelteKit's client-side router can resolve the view.
|
|
app.get("/admin", async (_request, reply) => {
|
|
return reply.type("text/html; charset=utf-8").send(indexHtml);
|
|
});
|
|
app.get("/admin/*", async (_request, reply) => {
|
|
return reply.type("text/html; charset=utf-8").send(indexHtml);
|
|
});
|
|
}
|