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
79 lines
2.9 KiB
TypeScript
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()
|
|
}
|