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
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
import { Permissions, type Permission, type Role } from "@/shared/types/permissions"
|
||||
import { Permissions, type Permission, type BuiltinRole } from "@/shared/types/permissions"
|
||||
import { isPermission } from "@/shared/lib/type-guards"
|
||||
import { db } from "@/shared/db"
|
||||
import { roles, rolePermissions } from "@/shared/db/schema"
|
||||
import { and, eq, inArray } from "drizzle-orm"
|
||||
|
||||
// Role → Permission mapping
|
||||
// New roles only need to add an entry here + seed the DB
|
||||
export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
/**
|
||||
* Seed role → permission mapping for the 6 builtin roles.
|
||||
*
|
||||
* Used by:
|
||||
* - The seed migration to populate the `role_permissions` table.
|
||||
* - `resolvePermissions()` as a fallback when the DB query fails (e.g. during
|
||||
* initial bootstrap before the migration has run).
|
||||
*
|
||||
* Runtime permission resolution reads from the DB — this constant is NOT
|
||||
* consulted at runtime except as a fallback.
|
||||
*/
|
||||
export const ROLE_PERMISSIONS_SEED: Record<BuiltinRole, Permission[]> = {
|
||||
admin: [
|
||||
Permissions.EXAM_CREATE,
|
||||
Permissions.EXAM_READ,
|
||||
@@ -59,12 +72,22 @@ export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
Permissions.LESSON_PLAN_UPDATE,
|
||||
Permissions.LESSON_PLAN_DELETE,
|
||||
Permissions.LESSON_PLAN_PUBLISH,
|
||||
Permissions.STANDARD_READ,
|
||||
Permissions.STANDARD_MANAGE,
|
||||
Permissions.STANDARD_LINK,
|
||||
Permissions.FILE_UPLOAD,
|
||||
Permissions.FILE_READ,
|
||||
Permissions.FILE_DELETE,
|
||||
Permissions.DASHBOARD_ADMIN_READ,
|
||||
Permissions.ERROR_BOOK_ANALYTICS_READ,
|
||||
Permissions.ADAPTIVE_PRACTICE_READ,
|
||||
// RBAC management — admin only by default
|
||||
Permissions.ROLE_CREATE,
|
||||
Permissions.ROLE_READ,
|
||||
Permissions.ROLE_UPDATE,
|
||||
Permissions.ROLE_DELETE,
|
||||
Permissions.ROLE_ASSIGN,
|
||||
Permissions.PERMISSION_READ,
|
||||
],
|
||||
teacher: [
|
||||
Permissions.EXAM_CREATE,
|
||||
@@ -108,6 +131,8 @@ export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
Permissions.LESSON_PLAN_UPDATE,
|
||||
Permissions.LESSON_PLAN_DELETE,
|
||||
Permissions.LESSON_PLAN_PUBLISH,
|
||||
Permissions.STANDARD_READ,
|
||||
Permissions.STANDARD_LINK,
|
||||
Permissions.DASHBOARD_TEACHER_READ,
|
||||
Permissions.ERROR_BOOK_ANALYTICS_READ,
|
||||
Permissions.ADAPTIVE_PRACTICE_READ,
|
||||
@@ -143,6 +168,7 @@ export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
Permissions.TEXTBOOK_READ,
|
||||
Permissions.CLASS_READ,
|
||||
Permissions.USER_PROFILE_UPDATE,
|
||||
Permissions.AI_CHAT,
|
||||
Permissions.ANNOUNCEMENT_READ,
|
||||
Permissions.GRADE_RECORD_READ,
|
||||
Permissions.ATTENDANCE_READ,
|
||||
@@ -232,13 +258,53 @@ export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge permissions from all roles (deduplicated)
|
||||
* @deprecated Use `ROLE_PERMISSIONS_SEED` instead. Kept as a re-export for
|
||||
* backward compatibility with any code that still imports `ROLE_PERMISSIONS`.
|
||||
*/
|
||||
export function resolvePermissions(roleNames: Role[]): Permission[] {
|
||||
const set = new Set<Permission>()
|
||||
for (const name of roleNames) {
|
||||
const perms = ROLE_PERMISSIONS[name] ?? []
|
||||
for (const p of perms) set.add(p)
|
||||
export const ROLE_PERMISSIONS = ROLE_PERMISSIONS_SEED
|
||||
|
||||
/**
|
||||
* Merge permissions from all roles by querying the `role_permissions` table.
|
||||
*
|
||||
* - Only enabled roles contribute permissions (`roles.is_enabled = true`).
|
||||
* - Falls back to `ROLE_PERMISSIONS_SEED` for builtin roles if the DB query
|
||||
* fails (e.g. during initial bootstrap before the migration has run).
|
||||
* - Deduplicates the resulting permission list.
|
||||
*/
|
||||
export async function resolvePermissions(roleNames: string[]): Promise<Permission[]> {
|
||||
if (roleNames.length === 0) return []
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ permission: rolePermissions.permission })
|
||||
.from(rolePermissions)
|
||||
.innerJoin(roles, eq(rolePermissions.roleId, roles.id))
|
||||
.where(and(inArray(roles.name, roleNames), eq(roles.isEnabled, true)))
|
||||
|
||||
const set = new Set<Permission>()
|
||||
for (const row of rows) {
|
||||
if (isPermission(row.permission)) {
|
||||
set.add(row.permission)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if the DB returned nothing for builtin roles (e.g. migration
|
||||
// not yet applied), use the seed constant so login still works.
|
||||
if (set.size === 0) {
|
||||
for (const name of roleNames) {
|
||||
const seed = ROLE_PERMISSIONS_SEED[name as BuiltinRole]
|
||||
if (seed) for (const p of seed) set.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(set)
|
||||
} catch {
|
||||
// DB unavailable (e.g. during build) — fall back to seed for builtin roles
|
||||
const set = new Set<Permission>()
|
||||
for (const name of roleNames) {
|
||||
const seed = ROLE_PERMISSIONS_SEED[name as BuiltinRole]
|
||||
if (seed) for (const p of seed) set.add(p)
|
||||
}
|
||||
return Array.from(set)
|
||||
}
|
||||
return Array.from(set)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user