import { auth } from "@/auth" import type { Permission, DataScope, AuthContext } from "@/shared/types/permissions" import { db } from "@/shared/db" import { classes, classSubjectTeachers, grades, parentStudentRelations, } from "@/shared/db/schema" import { eq, or } from "drizzle-orm" export class PermissionDeniedError extends Error { constructor(permission: string) { super(`Permission denied: ${permission}`) this.name = "PermissionDeniedError" } } /** * Get the full authentication context for the current user. * Throws if not authenticated. */ export async function getAuthContext(): Promise { const session = await auth() const userId = session?.user?.id if (!userId) throw new PermissionDeniedError("auth_required") // Prefer session data (already resolved in JWT callback) const roleNames = (session.user.roles ?? []) as string[] const permissions = (session.user.permissions ?? []) as Permission[] // Resolve data scope from DB (not cached in JWT since it can change) const dataScope = await resolveDataScope(userId, roleNames) return { userId, roles: roleNames, permissions, dataScope } } /** * Assert the current user has the specified permission. * Returns AuthContext on success, throws PermissionDeniedError on failure. */ export async function requirePermission(permission: Permission): Promise { const ctx = await getAuthContext() if (!ctx.permissions.includes(permission)) { throw new PermissionDeniedError(permission) } return ctx } /** * Check permission without throwing. Useful for conditional logic. */ export async function checkPermission( permission: Permission ): Promise<{ allowed: boolean; ctx: AuthContext }> { const ctx = await getAuthContext() return { allowed: ctx.permissions.includes(permission), ctx } } /** * Resolve the data scope for a user based on their roles. * Queries the DB for resource ownership information. */ async function resolveDataScope(userId: string, roleNames: string[]): Promise { // Admin sees everything if (roleNames.includes("admin")) { return { type: "all" } } // Grade head / teaching head: can manage their grades if (roleNames.includes("grade_head") || roleNames.includes("teaching_head")) { const managedGrades = await db .select({ id: grades.id }) .from(grades) .where(or(eq(grades.gradeHeadId, userId), eq(grades.teachingHeadId, userId))) if (managedGrades.length > 0) { return { type: "grade_managed", gradeIds: managedGrades.map((g) => g.id) } } } // Teacher: can see their own classes if (roleNames.includes("teacher")) { // Classes where user is the homeroom teacher const homeroomClasses = await db .select({ id: classes.id }) .from(classes) .where(eq(classes.teacherId, userId)) // Classes where user is a subject teacher const subjectClasses = await db .selectDistinct({ classId: classSubjectTeachers.classId, subjectId: classSubjectTeachers.subjectId }) .from(classSubjectTeachers) .where(eq(classSubjectTeachers.teacherId, userId)) const classIds = [ ...new Set([ ...homeroomClasses.map((c) => c.id), ...subjectClasses.map((c) => c.classId), ]), ] const subjectIds = subjectClasses .map((c) => c.subjectId) .filter((s): s is string => s !== null) return { type: "class_taught", classIds, subjectIds: subjectIds.length > 0 ? subjectIds : undefined, } } // Student: can see data from their enrolled classes if (roleNames.includes("student")) { return { type: "class_members" } } // Parent: can see their children's data if (roleNames.includes("parent")) { const children = await db .select({ studentId: parentStudentRelations.studentId }) .from(parentStudentRelations) .where(eq(parentStudentRelations.parentId, userId)) return { type: "children", childrenIds: children.map((c) => c.studentId) } } // Fallback: only own data return { type: "owned", userId } } /** * Convenience: assert the user is authenticated (has any role). * Returns AuthContext on success. */ export async function requireAuth(): Promise { return getAuthContext() }