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
@@ -676,12 +676,13 @@ async function main() {
|
||||
`共 ${studentListAfter.payload?.data?.length ?? 0} 人`,
|
||||
);
|
||||
|
||||
const resetResult = await api(`/students/${addedStudentId}/reset-password`, {
|
||||
// 学生没有密码相关接口(个人学生账号已清理,重置密码入口已下线):这里改为断言接口确实不存在
|
||||
const resetGone = await api(`/students/${addedStudentId}/reset-password`, {
|
||||
method: 'POST',
|
||||
token: adminToken,
|
||||
body: {},
|
||||
});
|
||||
record('重置学生密码', resetResult.status === 200, `status=${resetResult.status}`);
|
||||
record('学生重置密码接口已下线(404)', resetGone.status === 404, `status=${resetGone.status}`);
|
||||
|
||||
const renamed = await api(`/students/${addedStudentId}`, {
|
||||
method: 'PATCH',
|
||||
@@ -1597,11 +1598,8 @@ async function main() {
|
||||
|
||||
// 8.4 学生名单导入(仅管理员)+ 数据清理
|
||||
const importedUsername = `imp${Date.now().toString(36).slice(-6)}`;
|
||||
const studentsCsv = [
|
||||
'用户名,姓名,初始密码',
|
||||
`${importedUsername},导入测试生,imp123456`,
|
||||
'bad user,非法用户名,imp123456',
|
||||
].join(NL);
|
||||
// 学生导入模板已无"初始密码"列(学生没有账号属性)
|
||||
const studentsCsv = ['用户名,姓名', `${importedUsername},导入测试生`, 'bad user,非法用户名'].join(NL);
|
||||
const studentsCommit = await api('/imports/table/commit', {
|
||||
method: 'POST',
|
||||
token: adminToken,
|
||||
@@ -1610,7 +1608,7 @@ async function main() {
|
||||
classId,
|
||||
fileName: 'students.csv',
|
||||
contentBase64: Buffer.from(`\ufeff${studentsCsv}${NL}`, 'utf8').toString('base64'),
|
||||
mapping: { username: '用户名', name: '姓名', password: '初始密码' },
|
||||
mapping: { username: '用户名', name: '姓名' },
|
||||
mode: 'append',
|
||||
},
|
||||
});
|
||||
@@ -1740,7 +1738,7 @@ async function main() {
|
||||
classId,
|
||||
fileName: 'students.csv',
|
||||
contentBase64: Buffer.from(`\ufeff${studentsCsv}${NL}`, 'utf8').toString('base64'),
|
||||
mapping: { username: '用户名', name: '姓名', password: '初始密码' },
|
||||
mapping: { username: '用户名', name: '姓名' },
|
||||
mode: 'append',
|
||||
},
|
||||
});
|
||||
@@ -2228,6 +2226,68 @@ async function main() {
|
||||
`班主任账号=${deleteHeadTeacher.status}(${deleteHeadTeacher.payload?.message ?? ''}) 冒烟账号=${deleteSmokeTeacher.status}`,
|
||||
);
|
||||
|
||||
// 级联删除护栏:`Homework.createdBy` / `Notification.createdBy` 都是 ON DELETE CASCADE,
|
||||
// 因此"已不在任何班级、名下无课程,但发布过内容"的账号也必须被拒 ——
|
||||
// 否则"班主任转给别人 → 解除协作 → 删除账号"会静默清掉他在各班发过的作业与通知。
|
||||
const cascadeTeacherUsername = `smoke_cas_${Date.now().toString().slice(-8)}`;
|
||||
const cascadeTeacher = await api('/teachers', {
|
||||
method: 'POST',
|
||||
token: adminToken,
|
||||
body: { username: cascadeTeacherUsername, name: '冒烟级联教师', password: 'cascade123456' },
|
||||
});
|
||||
const cascadeTeacherId = cascadeTeacher.payload?.data?.id;
|
||||
const cascadeTeacherLogin = await api('/auth/login', {
|
||||
method: 'POST',
|
||||
body: { username: cascadeTeacherUsername, password: 'cascade123456' },
|
||||
});
|
||||
const cascadeTeacherToken = cascadeTeacherLogin.payload?.data?.token;
|
||||
|
||||
// 1) 先让他成为该班科任(这样才有发布权限),发一条通知
|
||||
await api(`/classes/${classId}/teachers`, {
|
||||
method: 'POST',
|
||||
token: adminToken,
|
||||
body: { teacherId: cascadeTeacherId },
|
||||
});
|
||||
const cascadeNotification = await api('/notifications', {
|
||||
method: 'POST',
|
||||
token: cascadeTeacherToken,
|
||||
body: { classId, title: '级联删除回归', content: '用于验证删除教师不会连带删除已发布内容' },
|
||||
});
|
||||
const cascadeNotificationId = cascadeNotification.payload?.data?.id;
|
||||
// 2) 解除班级分配:此时他"不在任何班级、名下无课程",但发布过内容
|
||||
await api(`/classes/${classId}/teachers/${cascadeTeacherId}`, {
|
||||
method: 'DELETE',
|
||||
token: adminToken,
|
||||
});
|
||||
// 3) 删除账号应当被拒(409),且那条通知必须还在
|
||||
const cascadeDelete = await api(`/teachers/${cascadeTeacherId}`, {
|
||||
method: 'DELETE',
|
||||
token: adminToken,
|
||||
});
|
||||
const afterCascadeList = await api(`/notifications?classId=${classId}`, { token: adminToken });
|
||||
const notificationAlive = (afterCascadeList.payload?.data ?? []).some(
|
||||
(item) => item.id === cascadeNotificationId,
|
||||
);
|
||||
record(
|
||||
'删除教师护栏覆盖"发布过作业/通知"的账号(不再级联删除内容)',
|
||||
cascadeDelete.status === 409 && notificationAlive,
|
||||
`删除=${cascadeDelete.status}(${cascadeDelete.payload?.message ?? ''}) 通知仍在=${notificationAlive}`,
|
||||
);
|
||||
|
||||
// 清理:内容删掉后账号即可删除
|
||||
if (cascadeNotificationId) {
|
||||
await api(`/notifications/${cascadeNotificationId}`, { method: 'DELETE', token: adminToken });
|
||||
}
|
||||
const cascadeDeleteAfter = await api(`/teachers/${cascadeTeacherId}`, {
|
||||
method: 'DELETE',
|
||||
token: adminToken,
|
||||
});
|
||||
record(
|
||||
'清理:内容删除后该教师账号可删除',
|
||||
cascadeDeleteAfter.status === 200,
|
||||
`status=${cascadeDeleteAfter.status}`,
|
||||
);
|
||||
|
||||
// 创建班级仅管理员 + 设置/更改班主任
|
||||
const teacherCreateClass = await api('/classes', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in new issue
Block a user