Files
NextEdu/src/shared/lib/auth-guard.ts
SpecialX 3b6272c99d feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013
## P1 功能(20 项)
- 站内消息系统、家长仪表盘、学生考勤管理
- Excel 导入导出、用户批量导入、成绩导出
- 排课规则+自动排课+课表调整
- 成绩趋势+对比分析、密码安全策略、速率限制
- 数据变更日志、文件预览+存储策略、全文检索
- 依赖审计集成 CI、数据库定时备份、E2E 测试完善
- 通知偏好管理

## 基础设施修复
- src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求)
- .env: MySQL 端口从 13002 切换至 14013
- scripts/create-db.ts: 新增数据库初始化脚本

## 架构文档同步
- 004_architecture_impact_map.md 和 005_architecture_data.json
  完整记录所有新增表、模块、路由、权限、依赖关系
2026-06-17 13:44:37 +08:00

139 lines
4.2 KiB
TypeScript

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<AuthContext> {
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<AuthContext> {
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<DataScope> {
// 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<AuthContext> {
return getAuthContext()
}