feat(authz): 重写权限模板分配与班级授权边界

拆分能力、分配范围和业务关系,固定管理员能力并收紧组织级写入口。
补齐内容、课表、设备会话和实时通道授权,兼容旧库分配及历史快照。
重写权限分配预览,按实际可操作范围展示维护入口,合并学生入口并独立学年升级页。
增加授权攻击矩阵及静态门禁,更新隔离回归与开发契约。

验证:授权矩阵 23 段通过,Node 24 下 Web 63/63,类型检查、lint 与文档严格构建通过。
This commit is contained in:
LoveChengke committed 2026-10-31 09:40:23 +08:00
1 parent 682818dd4a
commit 66be3a63b1
115 files changed
+4463 -2747

No files matched your search

@@ -1,3 +1,4 @@
import { assertAdmin, assertHeadOnly } from '../../lib/authz/index.js';
import {
buildAutoTermWeeks,
validateTermWeeks,
@@ -38,9 +39,15 @@ function scopeOf(classId?: string | null): string {
/** 读取某个范围的周次配置 */
export async function getTermWeeks(user: TokenPayload, classId?: string | null): Promise<TermWeeksDto> {
const scope = scopeOf(classId);
if (scope) await assertClassAccess(user, scope);
const scope = user.role === 'CLASS_DEVICE' ? (user.classId ?? user.sub) : scopeOf(classId);
if (user.role === 'CLASS_DEVICE' && classId && classId !== scope)
throw ApiError.forbidden('不能读取其他班级周次');
if (!scope) await assertPermission(user, 'term:read');
if (scope) await assertClassAccess(user, scope, 'term:read');
return readTermWeeks(scope);
}
async function readTermWeeks(scope: string): Promise<TermWeeksDto> {
const [weeks] = await Promise.all([
prisma.termWeek.findMany({ where: { classId: scope }, orderBy: { weekNumber: 'asc' } }),
scope
@@ -75,8 +82,9 @@ export async function updateTermWeeks(
user: TokenPayload,
input: UpdateTermWeeksInput,
): Promise<TermWeeksDto> {
await assertPermission(user, 'term:write');
const scope = scopeOf(input.classId);
if (scope) await assertHeadOnly(user, 'class:settings', scope);
else assertAdmin(user);
const maximum = input.maxTermWeeks ?? (await getMaxTermWeeks());
if (input.maxTermWeeks !== undefined && scope) throw ApiError.badRequest('总周数只能在全校默认范围修改');
@@ -132,7 +140,7 @@ export async function updateTermWeeks(
logger.info(`学期周次已更新:范围=${scope || '全校默认'} 周数=${input.weeks.length} 由 ${user.name} 执行`);
if (input.maxTermWeeks !== undefined) await setSetting(SETTING_KEYS.termMaxWeeks, String(maximum));
await publishTermChange(scope);
return getTermWeeks(user, scope || null);
return readTermWeeks(scope);
}
/**
@@ -142,8 +150,9 @@ export async function updateTermWeeks(
* 比从空白开始一周周填要快得多。
*/
export async function autoTermWeeks(user: TokenPayload, input: AutoTermWeeksInput): Promise<TermWeeksDto> {
await assertPermission(user, 'term:write');
const scope = scopeOf(input.classId);
if (scope) await assertHeadOnly(user, 'class:settings', scope);
else assertAdmin(user);
const context = await loadTermContext(scope || null);
const termStartDate = input.termStartDate || context.termStartDate || env.termStartDate;
const maxWeek = input.maxWeek ?? context.maxWeek;