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

@@ -9,23 +9,21 @@ import {
classEnrollments,
classSchedule,
} from "@/shared/db/schema"
import {
insertClassScheduleItem,
updateClassScheduleItemById,
deleteClassScheduleItemById,
} from "@/modules/scheduling/data-access"
import type {
ClassScheduleItem,
CreateClassScheduleItemInput,
StudentScheduleItem,
UpdateClassScheduleItemInput,
} from "./types"
import {
getAccessibleClassIdsForTeacher,
getSessionTeacherId,
getTeacherIdForMutations,
} from "./data-access"
const isWeekday = (n: unknown): n is 1 | 2 | 3 | 4 | 5 | 6 | 7 =>
typeof n === "number" && n >= 1 && n <= 7 && Number.isInteger(n)
const toWeekday = (n: number): 1 | 2 | 3 | 4 | 5 | 6 | 7 =>
isWeekday(n) ? n : 1
export const getStudentSchedule = cache(async (studentId: string): Promise<StudentScheduleItem[]> => {
const id = studentId.trim()
if (!id) return []
@@ -51,7 +49,7 @@ export const getStudentSchedule = cache(async (studentId: string): Promise<Stude
id: r.id,
classId: r.classId,
className: r.className,
weekday: r.weekday as StudentScheduleItem["weekday"],
weekday: toWeekday(r.weekday),
startTime: r.startTime,
endTime: r.endTime,
course: r.course,
@@ -90,7 +88,7 @@ export const getClassSchedule = cache(
return rows.map((r) => ({
id: r.id,
classId: r.classId,
weekday: r.weekday as ClassScheduleItem["weekday"],
weekday: toWeekday(r.weekday),
startTime: r.startTime,
endTime: r.endTime,
course: r.course,
@@ -98,133 +96,3 @@ export const getClassSchedule = cache(
}))
}
)
const isTimeHHMM = (v: string) => /^\d{2}:\d{2}$/.test(v)
export async function createClassScheduleItem(data: CreateClassScheduleItemInput): Promise<string> {
const teacherId = await getTeacherIdForMutations()
const classId = data.classId.trim()
const course = data.course.trim()
const startTime = data.startTime.trim()
const endTime = data.endTime.trim()
const location = data.location?.trim() || null
const weekday = data.weekday
if (!classId) throw new Error("Class is required")
if (!course) throw new Error("Course is required")
if (!isTimeHHMM(startTime) || !isTimeHHMM(endTime)) throw new Error("Invalid time format")
if (startTime >= endTime) throw new Error("Start time must be earlier than end time")
if (weekday < 1 || weekday > 7) throw new Error("Invalid weekday")
const [owned] = await db
.select({ id: classes.id })
.from(classes)
.where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
.limit(1)
if (!owned) throw new Error("Class not found")
// Delegate DB write to scheduling module (unified write entry point)
return insertClassScheduleItem({
classId,
weekday,
startTime,
endTime,
course,
location,
})
}
export async function updateClassScheduleItem(scheduleId: string, data: UpdateClassScheduleItemInput): Promise<void> {
const teacherId = await getTeacherIdForMutations()
const id = scheduleId.trim()
if (!id) throw new Error("Missing schedule id")
const [existing] = await db
.select({
id: classSchedule.id,
classId: classSchedule.classId,
startTime: classSchedule.startTime,
endTime: classSchedule.endTime,
})
.from(classSchedule)
.innerJoin(classes, eq(classes.id, classSchedule.classId))
.where(and(eq(classSchedule.id, id), eq(classes.teacherId, teacherId)))
.limit(1)
if (!existing) throw new Error("Schedule item not found")
const update: Partial<typeof classSchedule.$inferSelect> = {}
if (typeof data.classId === "string") {
const nextClassId = data.classId.trim()
if (!nextClassId) throw new Error("Class is required")
const [ownedNext] = await db
.select({ id: classes.id })
.from(classes)
.where(and(eq(classes.id, nextClassId), eq(classes.teacherId, teacherId)))
.limit(1)
if (!ownedNext) throw new Error("Class not found")
update.classId = nextClassId
}
if (typeof data.weekday === "number") {
if (data.weekday < 1 || data.weekday > 7) throw new Error("Invalid weekday")
update.weekday = data.weekday
}
if (typeof data.course === "string") {
const course = data.course.trim()
if (!course) throw new Error("Course is required")
update.course = course
}
const nextStart = typeof data.startTime === "string" ? data.startTime.trim() : undefined
const nextEnd = typeof data.endTime === "string" ? data.endTime.trim() : undefined
if (nextStart !== undefined) {
if (!isTimeHHMM(nextStart)) throw new Error("Invalid time format")
update.startTime = nextStart
}
if (nextEnd !== undefined) {
if (!isTimeHHMM(nextEnd)) throw new Error("Invalid time format")
update.endTime = nextEnd
}
if (update.startTime !== undefined || update.endTime !== undefined) {
const mergedStart = update.startTime ?? existing.startTime
const mergedEnd = update.endTime ?? existing.endTime
if (typeof mergedStart === "string" && typeof mergedEnd === "string" && mergedStart >= mergedEnd) {
throw new Error("Start time must be earlier than end time")
}
}
if (data.location !== undefined) {
update.location = data.location?.trim() || null
}
if (Object.keys(update).length === 0) return
// Delegate DB write to scheduling module (unified write entry point)
await updateClassScheduleItemById(id, update)
}
export async function deleteClassScheduleItem(scheduleId: string): Promise<void> {
const teacherId = await getTeacherIdForMutations()
const id = scheduleId.trim()
if (!id) throw new Error("Missing schedule id")
const [owned] = await db
.select({ id: classSchedule.id })
.from(classSchedule)
.innerJoin(classes, eq(classes.id, classSchedule.classId))
.where(and(eq(classSchedule.id, id), eq(classes.teacherId, teacherId)))
.limit(1)
if (!owned) throw new Error("Schedule item not found")
// Delegate DB write to scheduling module (unified write entry point)
await deleteClassScheduleItemById(id)
}