forked from bai/curriculum-project-hub
d159e372d2
删掉约 1770 行服务端模板拼接:renderDashboard / renderLoginPage
(databaseRoutes)、adminPanels、libraryBrowser、uiTheme,以及
libraryPage —— 后者迁移前已是无人引用的死代码。
新增两个端点承接原先在 page handler 里 inline 算的东西:
GET /database/config 免鉴权 bootstrap(org slug + dev 开关);
注册位置刻意早于 silo org 的提前返回,
org 未就绪时登录页仍要能渲染。
GET /database/api/stats 概览统计,要求 silo org OWNER/ADMIN ——
它聚合的是 org 级计数与审计流,不是
单节点权限视图。
静态托管收敛到 static.ts:一份 filelib-web 构建产物挂 /app 与
/database 两个前缀,资源路由只注册一次。并发症是路由顺序成了硬约束
—— 具体页面路由必须先于 SPA 通配注册,否则重演 /database/dashboard
盖住 SPA 的老 bug(ADR-0029)。
/database/library 改为 302 到 /database/dashboard/library。
BREAKING: 部署需先构建 filelib-web,否则 static.ts 的 existsSync
守卫会让 /app 与 /database 全部 404。
72 lines
3.3 KiB
TypeScript
72 lines
3.3 KiB
TypeScript
/**
|
|
* 前端静态托管:老师端 `/app/*` 与管理后台 `/database/*` 共用 `hub/filelib-web`
|
|
* 这**一份** SvelteKit 构建产物(adapter-static + fallback,见 filelib-web/svelte.config.js)。
|
|
*
|
|
* 标准前后端分离:服务端不渲染任何页面 —— 两个前缀下都只把同一个 index.html
|
|
* 原样送出,由 SvelteKit 客户端路由决定显示哪个视图;数据一律走 /database/api/*。
|
|
*
|
|
* 三类路由:
|
|
* /_filelib/* — 构建产物资源(JS/CSS/字体)。SvelteKit 的 appDir 被改名为
|
|
* `_filelib`,以避开 admin-web 在根上注册的 /_app/*
|
|
* (见 ../admin/static.ts)—— 同名会让 Fastify 启动即抛重复路由。
|
|
* /app, /app/* — 老师端 SPA 回退
|
|
* /database, /database/* — 管理后台 SPA 回退
|
|
*
|
|
* 具体路由(/database/api/*、/database/dev-login、/app/dev-login-teacher 等)由
|
|
* databaseRoutes.ts / teacherApp.ts 先注册;Fastify 按具体度匹配,通配不遮蔽它们。
|
|
*
|
|
* Override the UI directory with `CPH_FILELIB_UI_DIR` if needed.
|
|
*/
|
|
import { readFile } from "node:fs/promises";
|
|
import { existsSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
import fastifyStatic from "@fastify/static";
|
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
|
|
function resolveUiDir(): string {
|
|
const override = process.env["CPH_FILELIB_UI_DIR"];
|
|
if (override && override.trim() !== "") return resolvePath(override);
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
return resolvePath(join(here, "..", "..", "filelib-web", "build"));
|
|
}
|
|
|
|
/** 构建产物根目录下的顶层静态文件(SvelteKit 把 static/ 原样拷到这里)。 */
|
|
const TOP_LEVEL_FILES = ["favicon.svg", "favicon.ico", "robots.txt"] as const;
|
|
|
|
export async function registerDatabaseSpa(app: FastifyInstance): Promise<void> {
|
|
const uiDir = resolveUiDir();
|
|
if (!existsSync(join(uiDir, "index.html"))) {
|
|
app.log.warn(
|
|
{ uiDir },
|
|
"filelib-web/build not found; /app and /database SPA shells disabled. Run `npm run build --prefix filelib-web` to enable. JSON APIs remain fully functional.",
|
|
);
|
|
return;
|
|
}
|
|
const indexHtml = await readFile(join(uiDir, "index.html"), "utf8");
|
|
|
|
// 构建资源。@fastify/static 负责 MIME、ETag/Last-Modified 与目录穿越防护。
|
|
// 只注册一次 —— /app 与 /database 下的页面引用的都是这同一组绝对路径
|
|
// (filelib-web 的 paths.relative=false 保证了这点)。
|
|
await app.register(fastifyStatic, {
|
|
root: join(uiDir, "_filelib"),
|
|
prefix: "/_filelib/",
|
|
decorateReply: true,
|
|
});
|
|
|
|
for (const name of TOP_LEVEL_FILES) {
|
|
if (!existsSync(join(uiDir, name))) continue;
|
|
app.get(`/${name}`, async (_request, reply) => reply.sendFile(name, uiDir));
|
|
}
|
|
|
|
// SPA 回退:两个前缀,同一份 index.html。处理器不读请求、不查库 —— 所以
|
|
// 启动时读一次缓存在闭包里是安全的(每个请求发出的字节完全相同)。
|
|
const sendIndex = async (_request: FastifyRequest, reply: FastifyReply): Promise<FastifyReply> =>
|
|
reply.type("text/html; charset=utf-8").send(indexHtml);
|
|
|
|
app.get("/app", sendIndex);
|
|
app.get("/app/*", sendIndex);
|
|
app.get("/database", sendIndex);
|
|
app.get("/database/*", sendIndex);
|
|
}
|