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。
107 lines
4.2 KiB
TypeScript
107 lines
4.2 KiB
TypeScript
/**
|
|
* 老师端后端支撑(标准前后端分离):
|
|
* GET /database/api/login-info — 登录页配置(org slug / dev 开关)
|
|
* GET /app/dev-login-teacher — DEV ONLY 一键登录(普通老师)
|
|
*
|
|
* 服务端不渲染任何页面。`/app/*` 的静态托管与 SPA 回退在 ../static.ts —— 那里
|
|
* 与管理后台 `/database/*` 共用同一份 filelib-web 构建产物,资源路由只注册一次。
|
|
*/
|
|
|
|
import type { FastifyInstance } from "fastify";
|
|
import type { PrismaClient } from "@prisma/client";
|
|
import { SESSION_COOKIE_NAME, signSession } from "../../admin/auth/session.js";
|
|
|
|
export interface TeacherAppConfig {
|
|
readonly prisma: PrismaClient;
|
|
readonly sessionSecret: string;
|
|
/** 飞书 OAuth 链接按 silo org slug 构造。 */
|
|
readonly siloOrganizationSlug: string;
|
|
/** DEV ONLY(双重门禁,见 database/plugin.ts):一键登录端点与按钮同进同退。 */
|
|
readonly allowDevLoginBypass: boolean;
|
|
}
|
|
|
|
export async function registerTeacherApp(
|
|
app: FastifyInstance,
|
|
config: TeacherAppConfig,
|
|
): Promise<void> {
|
|
// 登录页配置(公开;org slug 本就在 OAuth URL 中,不构成敏感信息)。
|
|
app.get("/database/api/login-info", async () => ({
|
|
orgSlug: config.siloOrganizationSlug,
|
|
devLoginEnabled: config.allowDevLoginBypass,
|
|
}));
|
|
|
|
if (!config.allowDevLoginBypass) return;
|
|
registerDevLogins(app, config);
|
|
}
|
|
|
|
/** DEV ONLY:普通老师一键登录端点(双重门禁见 plugin.ts)。
|
|
* 老师端不提供管理员登录 —— 管理员从 /database/admin 进。 */
|
|
function registerDevLogins(app: FastifyInstance, config: TeacherAppConfig): void {
|
|
app.get("/app/dev-login-teacher", async (_request, reply) => {
|
|
const prisma = config.prisma;
|
|
const organization = await prisma.organization.findFirst({
|
|
where: { status: "ACTIVE" },
|
|
select: { id: true },
|
|
});
|
|
if (organization === null) {
|
|
return reply.status(404).send({ error: { code: "no_org", message: "no active organization" } });
|
|
}
|
|
|
|
let membership = await prisma.organizationMembership.findFirst({
|
|
where: { organizationId: organization.id, role: "MEMBER", revokedAt: null },
|
|
select: { userId: true, organizationId: true },
|
|
});
|
|
if (membership === null) {
|
|
const teacher = await prisma.user.upsert({
|
|
where: { feishuOpenId: "ou_dev_teacher" },
|
|
update: {},
|
|
create: { feishuOpenId: "ou_dev_teacher", displayName: "测试老师" },
|
|
});
|
|
await prisma.organizationMembership.create({
|
|
data: { organizationId: organization.id, userId: teacher.id, role: "MEMBER" },
|
|
});
|
|
membership = { userId: teacher.id, organizationId: organization.id };
|
|
}
|
|
|
|
const connection = await prisma.organizationFeishuApplicationConnection.findFirst({
|
|
where: { organizationId: membership.organizationId, status: "ACTIVE" },
|
|
select: { id: true, organizationId: true },
|
|
});
|
|
if (connection === null) {
|
|
return reply.status(404).send({ error: { code: "no_connection", message: "no active Feishu connection for org" } });
|
|
}
|
|
let identity = await prisma.feishuUserIdentity.findFirst({
|
|
where: { userId: membership.userId, connectionId: connection.id },
|
|
select: { id: true, connectionId: true },
|
|
});
|
|
if (identity === null) {
|
|
identity = await prisma.feishuUserIdentity.create({
|
|
data: { connectionId: connection.id, userId: membership.userId, openId: "ou_dev_teacher" },
|
|
select: { id: true, connectionId: true },
|
|
});
|
|
}
|
|
|
|
setSessionCookie(reply, config.sessionSecret, membership.userId, identity.id, identity.connectionId, connection.organizationId);
|
|
reply.log.warn({ userId: membership.userId }, "DEV teacher-app login bypass (regular teacher) used");
|
|
return reply.redirect("/app");
|
|
});
|
|
}
|
|
|
|
function setSessionCookie(
|
|
reply: { setCookie: (name: string, value: string, opts: Record<string, unknown>) => void },
|
|
secret: string,
|
|
userId: string,
|
|
feishuIdentityId: string,
|
|
feishuConnectionId: string,
|
|
feishuOrganizationId: string,
|
|
): void {
|
|
const token = signSession({ userId, feishuIdentityId, feishuConnectionId, feishuOrganizationId }, secret);
|
|
reply.setCookie(SESSION_COOKIE_NAME, token, {
|
|
path: "/",
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: false,
|
|
maxAge: 7 * 24 * 60 * 60,
|
|
});
|
|
}
|