Files
NextEdu/src/shared/lib/auth-guard.ts
SpecialX 0e63c24ed9
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat(shared,tests): add error boundaries, lib utils, i18n messages, and integration tests
shared:

- Add class-filter, error-state, route-error, section-error-boundary, widget-boundary components

- Add ui/alert component

- Add constants directory

- Add breached-password, export-utils, permission-bitmap, rate-limit, resolve-action-error, route-permissions, route-resolver, type-guards lib

- Add i18n messages (en, zh-CN) for invitation-codes, parent, questions, rbac

tests:

- Add integration tests for elective

- Add tests/setup/empty-stub

scripts:

- Add update-md.cjs, tmp_append_en.ps1, tmp_merge_en.ps1 utilities
2026-07-03 10:26:38 +08:00

79 lines
2.9 KiB
TypeScript

import type { Permission, DataScope, AuthContext, Role } from "@/shared/types/permissions"
import { isPermission } from "@/shared/lib/type-guards"
import { getSession } from "@/shared/lib/session"
import { PermissionDeniedError } from "@/shared/lib/errors"
// Re-export for backward compatibility (other modules still import from here)
export { PermissionDeniedError } from "@/shared/lib/errors"
/**
* Resolve the data scope for a user based on their roles.
*
* Delegates to the RBAC module's configuration-driven resolver via dynamic
* import, so the shared layer never statically depends on modules/*.
* This is the same pattern used by shared/lib/session.ts to break the
* shared ↔ auth circular dependency.
*
* P1-5/P1-6 audit fix: removed direct DB queries against classes,
* classEnrollments, classSubjectTeachers, grades, parentStudentRelations
* tables. The resolver now calls module data-access functions and is
* driven by a configuration array (DATA_SCOPE_RULES) instead of hardcoded
* role-name checks.
*/
async function resolveDataScope(userId: string, roleNames: Role[]): Promise<DataScope> {
const { resolveDataScopeFromConfig } = await import("@/modules/rbac/lib/data-scope-resolver")
return resolveDataScopeFromConfig(userId, roleNames)
}
/**
* Get the full authentication context for the current user.
* Throws if not authenticated.
*/
export async function getAuthContext(): Promise<AuthContext> {
const session = await getSession()
const userId = session?.user?.id
if (!userId) throw new PermissionDeniedError("auth_required")
// Prefer session data (already resolved in JWT callback)
// Use type guards to safely coerce session values
const roleNames = (session.user.roles ?? []).filter(
(r): r is Role => typeof r === "string"
)
const permissions = (session.user.permissions ?? []).filter(isPermission)
// 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 }
}
/**
* Convenience: assert the user is authenticated (has any role).
* Returns AuthContext on success.
*/
export async function requireAuth(): Promise<AuthContext> {
return getAuthContext()
}