Files
NextEdu/src/modules/classes/actions-teacher.ts
SpecialX dfffb61e94 feat(homework,classes,course-plans): add scans, student data, take confirm, error boundaries, dialogs, hooks, calendar
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
2026-07-03 10:25:35 +08:00

168 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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)
}
}