Files
SpecialX 49291fcc31 refactor: fix all P0/P1/P2 bugs and architecture issues
Bug fixes (from bugs/ directory):

- Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions

- Fix shared/lib <-> auth circular dependency via new session.ts module

- Fix divide-by-zero guard in grades data-access

- Fix audit export data truncation (paginated fetch for full datasets)

- Fix missing transactions in homework grading and elective lottery

- Fix missing revalidatePath in course-plans actions

- Fix frontend permission checks using requirePermission instead of requireAuth

- Fix dashboard role routing using session.user.roles

- Fix student auth pattern (migrate getDemoStudentUser to users module)

- Fix ActionState return type handling in components

Code quality fixes:

- Remove 60+ as type assertions (replace with type guards)

- Remove non-null assertions (use optional chaining or explicit checks)

- Convert dynamic imports to static imports (grades, diagnostic)

- Add React.cache() wrapping for read functions

- Parallelize independent queries with Promise.all

- Add explicit return types to 30+ arrow functions

- Replace any with unknown + type guards

- Fix import type for type-only imports

- Add Zod validation schemas for classes and diagnostic modules

- Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction)

- Add console.error to silent catch blocks

- Fix permission naming consistency (exam:proctor_read -> exam:proctor:read)

Architecture doc sync:

- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Update management-modules-audit.md for P0-7 cross-module fix

Moved deleted proctoring event route to deletes/ folder.
2026-06-19 05:13:34 +08:00

99 lines
2.8 KiB
TypeScript
Raw Permalink 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.
// Moved from src/app/api/proctoring/event/route.ts
// P0-6 fix: duplicate event reporting channel removed.
// The canonical path is the Server Action `recordProctoringEventAction`
// in src/modules/proctoring/actions.ts. This REST route was dead code
// (no client referenced /api/proctoring/event) and duplicated the
// Server Action's submission-ownership check + recordProctoringEvent call.
import { NextResponse } from "next/server"
import { z } from "zod"
import { requireAuth, PermissionDeniedError } from "@/shared/lib/auth-guard"
import { db } from "@/shared/db"
import { examSubmissions } from "@/shared/db/schema"
import { and, eq } from "drizzle-orm"
import { recordProctoringEvent } from "@/modules/proctoring/data-access"
import type { ProctoringEventType } from "@/modules/proctoring/types"
export const dynamic = "force-dynamic"
const EventSchema = z.object({
submissionId: z.string().min(1),
eventType: z.enum([
"tab_switch",
"window_blur",
"copy_attempt",
"paste_attempt",
"right_click",
"devtools_open",
"fullscreen_exit",
"idle_timeout",
]) as z.ZodType<ProctoringEventType>,
eventDetail: z.string().optional(),
})
export async function POST(req: Request) {
try {
const ctx = await requireAuth()
const body = await req.json().catch(() => null)
if (!body) {
return NextResponse.json(
{ success: false, message: "Invalid JSON body" },
{ status: 400 },
)
}
const parsed = EventSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{
success: false,
message: parsed.error.issues[0]?.message ?? "Invalid payload",
},
{ status: 400 },
)
}
// 安全校验submission 必须属于当前学生
const submission = await db.query.examSubmissions.findFirst({
where: and(
eq(examSubmissions.id, parsed.data.submissionId),
eq(examSubmissions.studentId, ctx.userId),
),
columns: {
id: true,
examId: true,
},
})
if (!submission) {
return NextResponse.json(
{ success: false, message: "Submission not found for current user" },
{ status: 404 },
)
}
await recordProctoringEvent({
submissionId: parsed.data.submissionId,
studentId: ctx.userId,
examId: submission.examId,
eventType: parsed.data.eventType,
eventDetail: parsed.data.eventDetail,
})
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof PermissionDeniedError) {
return NextResponse.json(
{ success: false, message: error.message },
{ status: 401 },
)
}
console.error("POST /api/proctoring/event error:", error)
return NextResponse.json(
{ success: false, message: "Failed to record proctoring event" },
{ status: 500 },
)
}
}