refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
Some checks failed
CI / build-deploy (push) Has been cancelled
Some checks failed
CI / build-deploy (push) Has been cancelled
- RBAC: 新增30个权限点、DataScope行级权限、requirePermission守卫,所有57+ Server Action接入权限校验 - UI拆分: exam-form(1623行→11文件)、textbook-reader(744行→7文件),均降至300行以内 - 测试: 新增5个单元测试文件(19用例),修复4个集成测试文件(38用例全部通过) - 架构文档: 新增架构影响地图(004/005)、标准功能清单(006)、差距审计报告(007) - 项目规则: 架构图优先规则,改码必同步图 - 安全: rehype-sanitize净化、AES加密API Key、权限路由守卫 - 无障碍: skip-link、aria-label、prefers-reduced-motion - 性能: next/font优化、next/image、代码分割
This commit is contained in:
@@ -1,52 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard";
|
||||
import { Permissions } from "@/shared/types/permissions";
|
||||
import { db } from "@/shared/db";
|
||||
import { chapters, knowledgePoints, questions, questionsToKnowledgePoints, textbooks, roles, users, usersToRoles } from "@/shared/db/schema";
|
||||
import { chapters, knowledgePoints, questions, questionsToKnowledgePoints, textbooks } from "@/shared/db/schema";
|
||||
import { CreateQuestionSchema } from "./schema";
|
||||
import type { CreateQuestionInput } from "./schema";
|
||||
import { ActionState } from "@/shared/types/action-state";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { getQuestions, type GetQuestionsParams } from "./data-access";
|
||||
import type { KnowledgePointOption } from "./types";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
const getSessionUserId = async (): Promise<string | null> => {
|
||||
const session = await auth();
|
||||
const userId = String(session?.user?.id ?? "").trim();
|
||||
return userId.length > 0 ? userId : null;
|
||||
};
|
||||
|
||||
async function ensureTeacher() {
|
||||
const userId = await getSessionUserId();
|
||||
if (!userId) {
|
||||
const [fallback] = await db
|
||||
.select({ id: users.id, role: roles.name })
|
||||
.from(users)
|
||||
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
||||
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
||||
.where(inArray(roles.name, ["teacher", "admin"]))
|
||||
.orderBy(asc(users.createdAt))
|
||||
.limit(1);
|
||||
if (!fallback) {
|
||||
throw new Error("Unauthorized: Only teachers can perform this action.");
|
||||
}
|
||||
return { id: fallback.id, role: fallback.role as "teacher" | "admin" };
|
||||
}
|
||||
const [row] = await db
|
||||
.select({ id: users.id, role: roles.name })
|
||||
.from(users)
|
||||
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
||||
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
||||
.where(and(eq(users.id, userId), inArray(roles.name, ["teacher", "admin"])))
|
||||
.limit(1);
|
||||
if (!row) {
|
||||
throw new Error("Unauthorized: Only teachers can perform this action.");
|
||||
}
|
||||
return { id: row.id, role: row.role as "teacher" | "admin" };
|
||||
}
|
||||
|
||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
|
||||
@@ -90,10 +56,10 @@ export async function createNestedQuestion(
|
||||
formData: FormData | CreateQuestionInput
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const user = await ensureTeacher();
|
||||
const ctx = await requirePermission(Permissions.QUESTION_CREATE);
|
||||
|
||||
let rawInput: unknown = formData;
|
||||
|
||||
|
||||
if (formData instanceof FormData) {
|
||||
const jsonString = formData.get("json");
|
||||
if (typeof jsonString === "string") {
|
||||
@@ -116,7 +82,7 @@ export async function createNestedQuestion(
|
||||
const input = validatedFields.data;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await insertQuestionWithRelations(tx, input, user.id, null);
|
||||
await insertQuestionWithRelations(tx, input, ctx.userId, null);
|
||||
});
|
||||
|
||||
revalidatePath("/teacher/questions");
|
||||
@@ -126,11 +92,14 @@ export async function createNestedQuestion(
|
||||
message: "Question created successfully",
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || "Database error occurred",
|
||||
message: e.message || "Database error occurred",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,8 +123,8 @@ export async function updateQuestionAction(
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const user = await ensureTeacher();
|
||||
const canEditAll = user.role === "admin";
|
||||
const ctx = await requirePermission(Permissions.QUESTION_UPDATE);
|
||||
const canEditAll = ctx.dataScope.type === "all";
|
||||
|
||||
const jsonString = formData.get("json");
|
||||
if (typeof jsonString !== "string") {
|
||||
@@ -182,7 +151,7 @@ export async function updateQuestionAction(
|
||||
content: input.content,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(canEditAll ? eq(questions.id, input.id) : and(eq(questions.id, input.id), eq(questions.authorId, user.id)));
|
||||
.where(canEditAll ? eq(questions.id, input.id) : and(eq(questions.id, input.id), eq(questions.authorId, ctx.userId)));
|
||||
|
||||
await tx
|
||||
.delete(questionsToKnowledgePoints)
|
||||
@@ -201,9 +170,12 @@ export async function updateQuestionAction(
|
||||
revalidatePath("/teacher/questions");
|
||||
|
||||
return { success: true, message: "Question updated successfully" };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { success: false, message: error.message };
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
return { success: false, message: "An unexpected error occurred" };
|
||||
}
|
||||
@@ -227,8 +199,8 @@ export async function deleteQuestionAction(
|
||||
formData: FormData
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const user = await ensureTeacher();
|
||||
const canDeleteAll = user.role === "admin";
|
||||
const ctx = await requirePermission(Permissions.QUESTION_DELETE);
|
||||
const canDeleteAll = ctx.dataScope.type === "all";
|
||||
|
||||
const questionId = formData.get("questionId");
|
||||
if (typeof questionId !== "string") {
|
||||
@@ -244,7 +216,7 @@ export async function deleteQuestionAction(
|
||||
throw new Error("Question not found");
|
||||
}
|
||||
|
||||
if (!canDeleteAll && q.authorId !== user.id) {
|
||||
if (!canDeleteAll && q.authorId !== ctx.userId) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
@@ -254,21 +226,32 @@ export async function deleteQuestionAction(
|
||||
revalidatePath("/teacher/questions");
|
||||
|
||||
return { success: true, message: "Question deleted successfully" };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { success: false, message: error.message };
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
return { success: false, message: "Failed to delete question" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQuestionsAction(params: GetQuestionsParams) {
|
||||
await ensureTeacher();
|
||||
return await getQuestions(params);
|
||||
try {
|
||||
await requirePermission(Permissions.QUESTION_READ);
|
||||
return await getQuestions(params);
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKnowledgePointOptionsAction(): Promise<KnowledgePointOption[]> {
|
||||
await ensureTeacher();
|
||||
try {
|
||||
await requirePermission(Permissions.QUESTION_READ);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
@@ -302,4 +285,10 @@ export async function getKnowledgePointOptionsAction(): Promise<KnowledgePointOp
|
||||
subject: row.subject ?? null,
|
||||
grade: row.grade ?? null,
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user