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.
This commit is contained in:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

View File

@@ -5,16 +5,21 @@ import { and, count, eq } from "drizzle-orm"
import { db } from "@/shared/db"
import {
classes,
classEnrollments,
classSubjectTeachers,
exams,
homeworkAnswers,
homeworkAssignmentQuestions,
homeworkAssignmentTargets,
homeworkAssignments,
homeworkSubmissions,
} from "@/shared/db/schema"
import {
getActiveStudentIdsByClassId,
getClassTeacherById as getClassTeacherIdFromClass,
getTeacherSubjectIdsByClass,
} from "@/modules/classes/data-access"
import {
getExamWithQuestionsForHomework as getExamWithQuestionsFromExams,
type ExamWithQuestionsForHomework,
} from "@/modules/exams/data-access"
import type { DataScope } from "@/shared/types/permissions"
// ---- Types ----
@@ -25,13 +30,7 @@ export type HomeworkExamQuestionData = {
order: number | null
}
export type HomeworkExamData = {
id: string
title: string
subjectId: string | null
structure: unknown
questions: HomeworkExamQuestionData[]
}
export type HomeworkExamData = ExamWithQuestionsForHomework
export type HomeworkSubmissionPermissionData = {
id: string
@@ -63,85 +62,38 @@ export type CreateHomeworkAssignmentData = {
}
// ---- Query helpers (for permission/validation in actions) ----
// These delegate to cross-module data-access interfaces to avoid direct DB queries.
export const getClassTeacherById = async (
classId: string
): Promise<{ id: string; teacherId: string } | null> => {
const [row] = await db
.select({ id: classes.id, teacherId: classes.teacherId })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return row ?? null
): Promise<{ id: string; teacherId: string | null } | null> => {
const teacherId = await getClassTeacherIdFromClass(classId)
if (teacherId === null) return null
return { id: classId, teacherId }
}
export const getExamWithQuestionsForHomework = async (
examId: string
): Promise<HomeworkExamData | null> => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
},
},
})
if (!exam) return null
return {
id: exam.id,
title: exam.title,
subjectId: exam.subjectId,
structure: exam.structure,
questions: exam.questions.map((q) => ({
questionId: q.questionId,
score: q.score ?? null,
order: q.order ?? null,
})),
}
return await getExamWithQuestionsFromExams(examId)
}
export const getTeacherAssignedSubjectIds = async (
classId: string,
teacherId: string
): Promise<string[]> => {
const rows = await db
.select({ subjectId: classSubjectTeachers.subjectId })
.from(classSubjectTeachers)
.where(
and(
eq(classSubjectTeachers.classId, classId),
eq(classSubjectTeachers.teacherId, teacherId)
)
)
return rows.map((r) => r.subjectId)
return await getTeacherSubjectIdsByClass(classId, teacherId)
}
export const getActiveClassStudentIdsForHomework = async (
classId: string,
dataScope: DataScope,
userId: string,
classTeacherId: string
_dataScope: DataScope,
_userId: string,
_classTeacherId: string | null
): Promise<string[]> => {
const classScope =
dataScope.type === "all"
? eq(classes.id, classId)
: classTeacherId === userId
? eq(classes.teacherId, userId)
: eq(classes.id, classId)
const rows = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.where(
and(
eq(classEnrollments.classId, classId),
eq(classEnrollments.status, "active"),
classScope
)
)
return rows.map((r) => r.studentId)
// Permission/scope filtering is handled by requirePermission in actions.ts.
// This function returns active students for the class via the classes data-access interface.
return await getActiveStudentIdsByClassId(classId)
}
export const getHomeworkSubmissionForPermission = async (
@@ -301,17 +253,19 @@ export const gradeHomeworkAnswers = async (
submissionId: string,
answers: Array<{ id: string; score: number; feedback: string | null }>
): Promise<void> => {
let totalScore = 0
for (const ans of answers) {
await db
.update(homeworkAnswers)
.set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() })
.where(eq(homeworkAnswers.id, ans.id))
totalScore += ans.score
}
await db.transaction(async (tx) => {
let totalScore = 0
for (const ans of answers) {
await tx
.update(homeworkAnswers)
.set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() })
.where(eq(homeworkAnswers.id, ans.id))
totalScore += ans.score
}
await db
.update(homeworkSubmissions)
.set({ score: totalScore, status: "graded", updatedAt: new Date() })
.where(eq(homeworkSubmissions.id, submissionId))
await tx
.update(homeworkSubmissions)
.set({ score: totalScore, status: "graded", updatedAt: new Date() })
.where(eq(homeworkSubmissions.id, submissionId))
})
}