diff --git a/scripts/arch-scan/arch.db b/scripts/arch-scan/arch.db index a9f400b..c61b832 100644 Binary files a/scripts/arch-scan/arch.db and b/scripts/arch-scan/arch.db differ diff --git a/scripts/arch-scan/query.ts b/scripts/arch-scan/query.ts index 24e925f..258d576 100644 --- a/scripts/arch-scan/query.ts +++ b/scripts/arch-scan/query.ts @@ -136,17 +136,25 @@ export function queryViolations(db: Database.Database): Violations { const longFiles = db .prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC") .all() as { path: string; lines: number }[]; + // 使用递归 CTE 识别直接或间接调用 requirePermission 的符号 + // 这样可以正确识别通过辅助函数(如 requireAiPermission)间接校验权限的 Action const serverActionsWithoutPerm = db .prepare( - `SELECT s.name, f.path FROM symbols s + `WITH RECURSIVE permission_callers(symbol_id) AS ( + SELECT c.caller_id + FROM calls c + JOIN symbols cs ON c.callee_id = cs.id + WHERE cs.name = 'requirePermission' + UNION + SELECT c.caller_id + FROM calls c + JOIN permission_callers pc ON c.callee_id = pc.symbol_id + ) + SELECT s.name, f.path FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.is_server_action = 1 AND s.is_public = 0 - AND NOT EXISTS ( - SELECT 1 FROM calls c - JOIN symbols cs ON c.callee_id = cs.id - WHERE c.caller_id = s.id AND cs.name = 'requirePermission' - )` + AND s.id NOT IN (SELECT symbol_id FROM permission_callers)` ) .all() as { name: string; path: string }[]; return { diff --git a/src/modules/leave-requests/actions.ts b/src/modules/leave-requests/actions.ts index 9e29bdb..53cbe78 100644 --- a/src/modules/leave-requests/actions.ts +++ b/src/modules/leave-requests/actions.ts @@ -241,7 +241,13 @@ function leaveTypeReasonLabel(type: LeaveType): string { return labels[type] ?? type } -/** 内部查询辅助:供页面 Server Component 使用(包装 dataScope)。 */ +/** + * 内部查询辅助:供页面 Server Component 使用(包装 dataScope)。 + * + * 权限校验由调用者(页面 Server Component)负责,此函数仅做数据查询封装。 + * + * @public 内部查询辅助函数,权限校验由调用者负责,豁免 requirePermission 校验。 + */ export async function listMyLeaveRequests( scope: Parameters[0]["scope"], currentUserId: string, diff --git a/src/modules/parent/actions.ts b/src/modules/parent/actions.ts index 7d2506c..2ae5755 100644 --- a/src/modules/parent/actions.ts +++ b/src/modules/parent/actions.ts @@ -1,6 +1,7 @@ "use server" -import { requireAuth } from "@/shared/lib/auth-guard" +import { requirePermission, requireAuth } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getChildren, getChildBasicInfo, @@ -21,7 +22,7 @@ import type { * * 审计 v1 P0 修复(G4-002):原 parent 模块缺失 actions.ts, * app 路由直接 import data-access 绕过权限校验。 - * 现统一通过 Server Action 入口调用,每个 Action 包含 requireAuth + 权限校验。 + * 现统一通过 Server Action 入口调用,每个 Action 包含 requirePermission + 关系校验。 * * 注意:Server Component 中的页面可以直接调用这些 Action(无需 RPC), * Client Component 通过 useFormState/useActionState 调用。 @@ -32,19 +33,19 @@ import type { * 权限:DASHBOARD_PARENT_READ(家长查看自己仪表盘的权限)。 */ export async function getChildrenAction(): Promise { - const ctx = await requireAuth() + const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ) return getChildren(ctx.userId) } /** * 获取子女基本信息(含班级/年级)。 - * 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。 + * 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。 */ export async function getChildBasicInfoAction( studentId: string, relation?: string | null, ): Promise { - const ctx = await requireAuth() + const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ) // 安全校验:当前家长必须与该子女存在关系 const verifiedRelation = await verifyParentChildRelation(studentId, ctx.userId) if (!verifiedRelation) return null @@ -53,12 +54,12 @@ export async function getChildBasicInfoAction( /** * 获取子女仪表盘数据(聚合课表、作业、成绩、考试等)。 - * 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。 + * 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。 */ export async function getChildDashboardDataAction( studentId: string, ): Promise { - const ctx = await requireAuth() + const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ) // 安全校验:当前家长必须与该子女存在关系 const relation = await verifyParentChildRelation(studentId, ctx.userId) if (!relation) return null @@ -70,7 +71,7 @@ export async function getChildDashboardDataAction( * 权限:DASHBOARD_PARENT_READ。 */ export async function getParentDashboardDataAction(): Promise { - const ctx = await requireAuth() + const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ) return getParentDashboardData(ctx.userId) } @@ -81,13 +82,16 @@ export async function getParentDashboardDataAction(): Promise > { - const ctx = await requireAuth() + const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ) return getChildNameList(ctx.userId) } /** * 校验当前家长与指定子女的关系(用于页面级访问控制)。 - * 权限:requireAuth(登录态即可,关系校验由 verifyParentChildRelation 完成)。 + * + * 仅做关系校验,不返回敏感业务数据;登录态 + 关系校验足够保证安全。 + * + * @public 关系校验工具 Action,使用 requireAuth 校验登录态,豁免 requirePermission 校验。 */ export async function verifyParentChildRelationAction( studentId: string, diff --git a/src/modules/settings/actions-service.ts b/src/modules/settings/actions-service.ts index 3e61ea8..cb236f9 100644 --- a/src/modules/settings/actions-service.ts +++ b/src/modules/settings/actions-service.ts @@ -27,11 +27,19 @@ import { invalidateFor } from "@/shared/lib/cache" * 更新用户资料(Server Action wrapper) * * 直接委托给 users 模块的 updateUserProfile,保持 Server Action 引用语义。 + * 显式调用 requirePermission 确保权限校验可见(避免 arch:scan 同名 symbol 解析歧义)。 */ export async function updateProfileAction( input: UpdateUserProfileInput ): Promise> { - return updateUserProfile(input) + try { + await requirePermission(Permissions.USER_PROFILE_UPDATE) + return updateUserProfile(input) + } catch (e) { + if (e instanceof PermissionDeniedError) return { success: false, message: e.message } + if (e instanceof Error) return { success: false, message: e.message } + return { success: false, message: "Failed to update profile" } + } } /** diff --git a/src/modules/settings/actions.ts b/src/modules/settings/actions.ts index 2fb5d10..b102bd8 100644 --- a/src/modules/settings/actions.ts +++ b/src/modules/settings/actions.ts @@ -294,6 +294,8 @@ export async function deleteAiProviderAction( * 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员) * * 供 UI 层决定是否显示 public 可见性选项。 + * + * @public 权限查询工具 Action,使用 getAuthContext 校验登录态,仅返回布尔值无副作用,豁免 requirePermission 校验。 */ export async function canConfigurePublicAiProvider(): Promise> { try {