feat(server,web,shared,desktop): 上传超限正确报错 + 两个登录入口都限流 + 作业按天深链,文档与验证脚本同步

服务端加固:
- express.json 上限 2mb → 12mb:表格导入把文件以 base64 放进 JSON(约 4/3 倍),
  8MB 的声明上限原先到 1.5MB 就被拦掉;超限现在由 error 中间件映射成
  413 IMPORT_TOO_LARGE(中文提示"请拆分后分批导入"),不再落进 500 兜底分支抛英文原文;
- loginRateLimiter 同时挂 /auth/login 与 /auth/class-login(班级密码默认 123456,
  班级登录曾是唯一没有暴力破解防护的入口),两个路径共享同一计数器;
- CORS_ORIGIN=* 时启动告警(不拦截启动,避免挡住教室机 file:// 的合法部署);
- 删除已无用的 DEFAULT_STUDENT_PASSWORD;班级 termWeeks 上限 40 → 30,
  与 MAX_TERM_WEEK / weekNumberSchema 对齐(原先对外承诺了系统兑现不了的学期长度)。

Web 端:
- 作业页 ?date= 深链在同路由再推 query 时也生效(watch route.query.date);
- 表格导入对话框适配 8MB 上限与 413 提示。

冒烟与文档:
- 新增 scripts/lib/smoke-class.mjs:验证用临时班级,避免探针通知弹到用户自己的客户端与灵动岛;
- README 与 AGENTS.md 同步本轮全部行为变更(含 schedule source、学生账号清理、凭据门禁)。
This commit is contained in:
LoveChengke committed 2026-10-02 00:25:21 +08:00
1 parent 3880eee08e
commit 20fcca0388
28 files changed
+1649 -915

No files matched your search

+13 -3
View File
@@ -145,8 +145,15 @@ export function createApp(): Express {
app.use(securityHeaders());
app.use(compression());
app.use(cors({ origin: env.corsOrigins, credentials: true }));
app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: true }));
/**
* 请求体上限:表格导入是把文件以 **base64** 放进 JSON 的,base64 约为原文件的 4/3,
* 再算上 JSON 包装,8MB 的表格文件约需 11MB 请求体。
* 这里必须留够余量,否则「导入模板写的 8MB 上限」永远到不了(文件到 1.5MB 就被 body-parser 拦掉),
* 而且用户看到的是一句 500「服务器内部错误:request entity too large」——
* 与 `table-import.service.ts` 的 MAX_IMPORT_BYTES(8MB) 校验完全对不上。
*/
app.use(express.json({ limit: '12mb' }));
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
app.use(requestLogger());
// 探针(不参与限流)
@@ -173,7 +180,10 @@ export function createApp(): Express {
// 限流:先挂通用限流,再挂更严格的登录限流(后挂者决定响应头里的策略,便于观察暴力破解防护)
app.use(API_PREFIX, apiRateLimiter());
app.use(`${API_PREFIX}/auth/login`, loginRateLimiter());
// 登录限流必须覆盖**两个**登录入口:`/auth/login`(教师/管理员)与 `/auth/class-login`(学生端
// 班级账号)。后者的密码默认就是 123456,漏掉它等于形同虚设 —— 这里用数组形式挂载,
// 两个路径共享同一个计数器(同一 IP 对两种登录的失败次数合并计算)。
app.use([`${API_PREFIX}/auth/login`, `${API_PREFIX}/auth/class-login`], loginRateLimiter());
// 模块化装配:注册表里的每个模块挂载到 /api<basePath>
for (const module of apiModules) {