服务端加固: - 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、学生账号清理、凭据门禁)。
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
import { z } from 'zod';
|
||
|
||
/** 通用路径参数::id */
|
||
export const idParamSchema = z.object({ id: z.string().min(1, '缺少 id') });
|
||
|
||
/** HH:mm 时间 */
|
||
export const timeSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, '时间格式应为 HH:mm');
|
||
|
||
/** ISO 日期时间字符串(接受 2026-03-01 或完整 ISO) */
|
||
export const dateTimeSchema = z
|
||
.string()
|
||
.min(1)
|
||
.refine((value) => !Number.isNaN(new Date(value).getTime()), '日期时间格式不正确');
|
||
|
||
/** 可选日期时间:允许 null / 空字符串,表示清空 */
|
||
export const optionalDateTimeSchema = z.union([dateTimeSchema, z.null(), z.literal('')]).optional();
|
||
|
||
/** 把可选日期时间统一转成 Date | null */
|
||
export function parseOptionalDate(value: string | null | undefined): Date | null {
|
||
if (!value) return null;
|
||
const date = new Date(value);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
|
||
/** 查询参数里的布尔值(true/1) */
|
||
export const booleanFlagSchema = z
|
||
.union([z.boolean(), z.string()])
|
||
.optional()
|
||
.transform((value) => value === true || value === 'true' || value === '1');
|
||
|
||
/** 关键字查询 */
|
||
export const keywordSchema = z.string().trim().max(64).optional();
|
||
|
||
/**
|
||
* 附件链接。
|
||
*
|
||
* 只允许 http(s) 绝对地址或站内相对路径:这一层是为**渲染端**兜底 ——
|
||
* 该字段最终会绑到前端的 `<a href>` / `el-link :href` 上,
|
||
* 若放行 `javascript:` / `data:text/html` 就会变成一条存储型 XSS 的注入点。
|
||
* 空串与 null 都表示"没有附件"。
|
||
*/
|
||
export const attachmentUrlSchema = z
|
||
.string()
|
||
.trim()
|
||
.max(500)
|
||
.refine(
|
||
(value) =>
|
||
value === '' || value.startsWith('/') || /^https?:\/\//i.test(value),
|
||
{ message: '附件链接需为 http(s):// 地址或站内相对路径' },
|
||
)
|
||
.nullish();
|
||
|
||
/** 周次:1..30 */
|
||
export const weekNumberSchema = z.coerce.number().int().min(1).max(30);
|