homework: - Add data-access-scans, data-access-student, data-access-utils, data-access-exam-cross - Add excellent-submissions, homework-take-confirm-dialog, homework-take-sidebar components classes: - Add class-delete-dialog, class-error-boundary, class-form-dialog, class-form-utils - Add class-list-table, class-list-toolbar, class-skeleton - Add schedule-create-dialog, schedule-delete-dialog, schedule-edit-dialog, schedule-utils - Add data-access-teacher and hooks directory course-plans: - Add course-plan-calendar, sortable-week-row, template-picker-dialog components - Add lib directory
168 lines
5.4 KiB
TypeScript
168 lines
5.4 KiB
TypeScript
"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<string> | null,
|
||
formData: FormData
|
||
): Promise<ActionState<string>> {
|
||
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<ActionState> {
|
||
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<ActionState> {
|
||
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)
|
||
}
|
||
}
|