"use server" import { revalidatePath } from "next/cache" import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import type { ActionState } from "@/shared/types/action-state" import { handleActionError } from "@/shared/lib/action-utils" import { createTeacherClass, deleteTeacherClass, updateTeacherClass, verifyTeacherOwnsClass, } from "./data-access" import { findGradeIdByHeadAndName, isGradeHead } from "@/modules/school/data-access" import { CreateTeacherClassSchema, UpdateTeacherClassSchema, DeleteTeacherClassSchema, } from "./schema" import { hasAdminScope } from "./actions-shared" export async function createTeacherClassAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.CLASS_CREATE) const t = await getTranslations("classes") const parsed = CreateTeacherClassSchema.safeParse({ name: formData.get("name"), grade: formData.get("grade"), schoolName: formData.get("schoolName"), schoolId: formData.get("schoolId"), gradeId: formData.get("gradeId"), homeroom: formData.get("homeroom"), room: formData.get("room"), }) if (!parsed.success) { return { success: false, message: t("actions.classNameGradeRequired") } } const { name, grade, schoolName, schoolId, gradeId, homeroom, room } = parsed.data // P1-1: 使用 dataScope 替代 ctx.roles.includes("admin") 硬编码 if (!hasAdminScope(ctx)) { const userId = ctx.userId const normalizedGradeId = typeof gradeId === "string" ? gradeId.trim() : "" const isOwner = normalizedGradeId ? await isGradeHead(normalizedGradeId, userId) : Boolean(await findGradeIdByHeadAndName(userId, grade)) if (!isOwner) { return { success: false, message: t("actions.onlyAdminsAndGradeHeads") } } } try { const id = await createTeacherClass({ schoolName: schoolName ?? null, schoolId: schoolId ?? null, name, grade, gradeId: gradeId ?? null, homeroom: homeroom ?? null, room: room ?? null, }) revalidatePath("/teacher/classes/my") revalidatePath("/teacher/classes/students") revalidatePath("/teacher/classes/schedule") return { success: true, message: t("actions.classCreated"), data: id } } catch (error) { return handleActionError(error) } } catch (e) { return handleActionError(e) } } export async function updateTeacherClassAction( classId: string, prevState: ActionState | null, formData: FormData ): Promise { try { const ctx = await requirePermission(Permissions.CLASS_UPDATE) const t = await getTranslations("classes") const parsed = UpdateTeacherClassSchema.safeParse({ classId, schoolName: formData.get("schoolName"), schoolId: formData.get("schoolId"), name: formData.get("name"), grade: formData.get("grade"), gradeId: formData.get("gradeId"), homeroom: formData.get("homeroom"), room: formData.get("room"), }) if (!parsed.success) { return { success: false, message: t("actions.missingClassId") } } const { classId: validatedClassId, schoolName, schoolId, name, grade, gradeId, homeroom, room } = parsed.data // P0-5: 越权校验——教师更新班级前校验归属(admin scope 跳过) if (!hasAdminScope(ctx)) { const owns = await verifyTeacherOwnsClass(validatedClassId, ctx.userId) if (!owns) { return { success: false, message: t("actions.notOwnClass") } } } try { await updateTeacherClass(validatedClassId, { schoolName: schoolName ?? undefined, schoolId: schoolId ?? undefined, name: name ?? undefined, grade: grade ?? undefined, gradeId: gradeId ?? undefined, homeroom: homeroom ?? undefined, room: room ?? undefined, }) revalidatePath("/teacher/classes/my") revalidatePath("/teacher/classes/students") revalidatePath("/teacher/classes/schedule") return { success: true, message: t("actions.classUpdated") } } catch (error) { return handleActionError(error) } } catch (e) { return handleActionError(e) } } export async function deleteTeacherClassAction(classId: string): Promise { try { const ctx = await requirePermission(Permissions.CLASS_DELETE) const t = await getTranslations("classes") const parsed = DeleteTeacherClassSchema.safeParse({ classId }) if (!parsed.success) { return { success: false, message: t("actions.missingClassId") } } // P0-5: 越权校验——教师删除班级前校验归属(admin scope 跳过) if (!hasAdminScope(ctx)) { const owns = await verifyTeacherOwnsClass(parsed.data.classId, ctx.userId) if (!owns) { return { success: false, message: t("actions.notOwnClass") } } } try { await deleteTeacherClass(parsed.data.classId) revalidatePath("/teacher/classes/my") revalidatePath("/teacher/classes/students") revalidatePath("/teacher/classes/schedule") return { success: true, message: t("actions.classDeleted") } } catch (error) { return handleActionError(error) } } catch (e) { return handleActionError(e) } }