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:
1 parent
3880eee08e
commit
20fcca0388
28 files changed
+1649
-915
No files matched your search
@@ -33,6 +33,18 @@ const dayMarks = ref<Map<string, number>>(new Map());
|
||||
const detailVisible = ref(false);
|
||||
const current = ref<HomeworkDto | null>(null);
|
||||
|
||||
/**
|
||||
* 详情里的附件链接。
|
||||
*
|
||||
* 服务端已限制为 http(s) / 站内相对路径,这里在渲染端再兜一层:
|
||||
* 该值最终会进 `<a href>`,一旦放行 `javascript:` 就是一条存储型 XSS(老师写、管理员点)。
|
||||
*/
|
||||
const attachmentHref = computed(() => {
|
||||
const value = current.value?.attachmentUrl;
|
||||
if (!value) return '';
|
||||
return value.startsWith('/') || /^https?:\/\//i.test(value) ? value : '';
|
||||
});
|
||||
|
||||
const classStudents = ref<Array<{ id: string; name: string }>>([]);
|
||||
const completedMap = computed(() => {
|
||||
const total = classStudents.value.length;
|
||||
@@ -48,34 +60,57 @@ const submissionsSaving = ref(false);
|
||||
const submissionStudents = ref<HomeworkSubmissionsDto['students']>([]);
|
||||
/** 勾选 = 未交(保存后其余学生一律标记为已交) */
|
||||
const notSubmittedIds = ref<string[]>([]);
|
||||
/**
|
||||
* 当前这名单属于**哪一份作业**。
|
||||
*
|
||||
* 上面两个 ref 是页面级共享状态,而详情抽屉可以连续打开不同作业(loadSubmissions 是
|
||||
* 并发发出的),因此必须记住数据归属并给请求编号:否则慢响应会把 A 的名单显示成 B 的,
|
||||
* 老师点"保存"就把 A 的未交名单整份写到 B 上(服务端会对全班逐个 upsert,且没有二次确认)。
|
||||
*/
|
||||
const submissionsHomeworkId = ref('');
|
||||
let submissionsRequestId = 0;
|
||||
|
||||
const notSubmittedNames = computed(() =>
|
||||
submissionStudents.value.filter((item) => notSubmittedIds.value.includes(item.userId)),
|
||||
);
|
||||
|
||||
/** 打开详情时顺带读一次未交名单(教师/管理员都有权限) */
|
||||
async function loadSubmissions(homeworkId: string): Promise<void> {
|
||||
async function loadSubmissions(homeworkId: string): Promise<boolean> {
|
||||
const requestId = ++submissionsRequestId;
|
||||
submissionsLoading.value = true;
|
||||
try {
|
||||
const result = await homeworkApi.submissions(homeworkId);
|
||||
// 迟到的响应:用户已经切到别的作业了,直接丢弃
|
||||
if (requestId !== submissionsRequestId) return false;
|
||||
submissionStudents.value = result.students;
|
||||
notSubmittedIds.value = result.notSubmitted.map((item) => item.userId);
|
||||
submissionsHomeworkId.value = homeworkId;
|
||||
return true;
|
||||
} catch {
|
||||
if (requestId !== submissionsRequestId) return false;
|
||||
submissionStudents.value = [];
|
||||
notSubmittedIds.value = [];
|
||||
submissionsHomeworkId.value = homeworkId;
|
||||
return false;
|
||||
} finally {
|
||||
submissionsLoading.value = false;
|
||||
if (requestId === submissionsRequestId) submissionsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openSubmissions(): Promise<void> {
|
||||
if (!current.value) return;
|
||||
if (submissionStudents.value.length === 0) await loadSubmissions(current.value.id);
|
||||
// 每次都以"当前这份作业"为准重新拉一次:不能因为共享状态里还有数据就跳过 ——
|
||||
// 那些数据可能属于上一次打开的作业,保存时会写到错误的作业上。
|
||||
await loadSubmissions(current.value.id);
|
||||
submissionsVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveSubmissions(): Promise<void> {
|
||||
if (!current.value) return;
|
||||
if (submissionsHomeworkId.value !== current.value.id) {
|
||||
ElMessage.warning('未交名单尚未加载完成,请稍候再保存');
|
||||
return;
|
||||
}
|
||||
submissionsSaving.value = true;
|
||||
try {
|
||||
const result = await homeworkApi.saveSubmissions(current.value.id, notSubmittedIds.value);
|
||||
@@ -320,6 +355,8 @@ function onHomeworkEvent(): void {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadClasses();
|
||||
// 首屏也要拉一次"哪些天有作业",否则日期选择器上没有任何高亮,直到手动切一次班级才出现
|
||||
await loadDayMarks();
|
||||
await loadHomeworks();
|
||||
realtime.on(SOCKET_EVENTS.homeworkNew, onHomeworkEvent);
|
||||
realtime.on(SOCKET_EVENTS.homeworkUpdated, onHomeworkEvent);
|
||||
@@ -509,8 +546,8 @@ onUnmounted(() => {
|
||||
<p class="content-block">{{ current.content }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="current.attachmentUrl" class="mt-12">
|
||||
<el-link type="primary" :href="current.attachmentUrl" target="_blank">查看附件</el-link>
|
||||
<div v-if="attachmentHref" class="mt-12">
|
||||
<el-link type="primary" :href="attachmentHref" target="_blank">查看附件</el-link>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
|
||||
Reference in new issue
Block a user