feat(textbooks): 教材模块审计重构 — 跨模块解耦 + 权限 + i18n + 错误边界 + 纯函数抽取
P0 修复: - 解耦跨模块 UI 依赖:knowledge-point-dialogs 不再直接 import questions, 改为 renderQuestionCreator render prop 由页面注入 - 接入 usePermission Hook 替换 canEdit 硬编码 - 全模块 i18n 改造:新增 en/zh-CN 翻译文件,替换所有硬编码文案 - Server Action 资源归属校验:新增 verifyChapterBelongsToTextbook/ verifyKnowledgePointBelongsToTextbook,在 reorder/update/delete/create 中校验 P1 改进: - 补齐 Error Boundary:4 个 error.tsx + TextbookSectionErrorBoundary 区块包裹 - 抽取纯函数到 utils.ts/graph-layout.ts/constants.ts 并补单测(26 用例全通过) - 消除重复组件:删除 knowledge-point-panel/create-knowledge-point-dialog - 修复类型断言:chapter.children! → 守卫式访问 - 图谱 a11y:添加 role/aria-label/aria-pressed - 统一删除确认:confirm() → AlertDialog - 数据范围过滤:getTextbooksWithScope 支持学生端按年级过滤 P2 预留: - TextbookAnalytics 埋点接口 + Provider + Hook 同步 005 架构数据 JSON:补充 getTextbooksWithScope/verify*/ChapterTreeNode 等
This commit is contained in:
@@ -14,7 +14,9 @@ import {
|
||||
updateKnowledgePoint,
|
||||
updateTextbook,
|
||||
deleteTextbook,
|
||||
reorderChapters
|
||||
reorderChapters,
|
||||
verifyChapterBelongsToTextbook,
|
||||
verifyKnowledgePointBelongsToTextbook,
|
||||
} from "./data-access";
|
||||
import {
|
||||
CreateTextbookSchema,
|
||||
@@ -38,6 +40,11 @@ export async function reorderChaptersAction(
|
||||
): Promise<ActionState> {
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_UPDATE);
|
||||
// P0-4 资源归属校验:防止越权操作其他教材的章节
|
||||
const belongs = await verifyChapterBelongsToTextbook(chapterId, textbookId)
|
||||
if (!belongs) {
|
||||
return { success: false, message: "Chapter does not belong to this textbook" };
|
||||
}
|
||||
await reorderChapters(chapterId, newIndex, parentId);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Chapters reordered successfully" };
|
||||
@@ -203,6 +210,11 @@ export async function updateChapterContentAction(
|
||||
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_UPDATE);
|
||||
// P0-4 资源归属校验
|
||||
const belongs = await verifyChapterBelongsToTextbook(chapterId, textbookId)
|
||||
if (!belongs) {
|
||||
return { success: false, message: "Chapter does not belong to this textbook" };
|
||||
}
|
||||
await updateChapterContent(parsed.data);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Content updated successfully" };
|
||||
@@ -220,6 +232,11 @@ export async function deleteChapterAction(
|
||||
): Promise<ActionState> {
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_DELETE);
|
||||
// P0-4 资源归属校验
|
||||
const belongs = await verifyChapterBelongsToTextbook(chapterId, textbookId)
|
||||
if (!belongs) {
|
||||
return { success: false, message: "Chapter does not belong to this textbook" };
|
||||
}
|
||||
await deleteChapter(chapterId);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Chapter deleted successfully" };
|
||||
@@ -254,6 +271,11 @@ export async function createKnowledgePointAction(
|
||||
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_CREATE);
|
||||
// P0-4 资源归属校验:确保 chapter 属于该 textbook,防止跨教材越权创建知识点
|
||||
const chapterBelongs = await verifyChapterBelongsToTextbook(chapterId, textbookId);
|
||||
if (!chapterBelongs) {
|
||||
return { success: false, message: "Chapter does not belong to this textbook" };
|
||||
}
|
||||
await createKnowledgePoint(parsed.data);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Knowledge point created successfully" };
|
||||
@@ -271,6 +293,11 @@ export async function deleteKnowledgePointAction(
|
||||
): Promise<ActionState> {
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_DELETE);
|
||||
// P0-4 资源归属校验
|
||||
const belongs = await verifyKnowledgePointBelongsToTextbook(kpId, textbookId)
|
||||
if (!belongs) {
|
||||
return { success: false, message: "Knowledge point does not belong to this textbook" };
|
||||
}
|
||||
await deleteKnowledgePoint(kpId);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Knowledge point deleted successfully" };
|
||||
@@ -305,6 +332,11 @@ export async function updateKnowledgePointAction(
|
||||
|
||||
try {
|
||||
await requirePermission(Permissions.TEXTBOOK_UPDATE);
|
||||
// P0-4 资源归属校验
|
||||
const belongs = await verifyKnowledgePointBelongsToTextbook(kpId, textbookId)
|
||||
if (!belongs) {
|
||||
return { success: false, message: "Knowledge point does not belong to this textbook" };
|
||||
}
|
||||
await updateKnowledgePoint(parsed.data);
|
||||
revalidatePath(`/teacher/textbooks/${textbookId}`);
|
||||
return { success: true, message: "Knowledge point updated successfully" };
|
||||
|
||||
Reference in New Issue
Block a user