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

+18 -2
View File
@@ -46,8 +46,15 @@ function run(args, logName) {
});
});
}
const suite = process.argv.includes('--review-api')
? 'review-api'
: process.argv.includes('--clients-api')
? 'clients-api'
: null;
const seedFile = path.join(root, 'packages/server/prisma', `.verification-seed-${process.pid}.ts`),
testFile = path.join(root, 'packages/server/scripts', `.verification-e2e-${process.pid}.mjs`);
testFile = suite
? path.join(root, 'scripts', `.verification-${suite}-${process.pid}.mjs`)
: path.join(root, 'packages/server/scripts', `.verification-e2e-${process.pid}.mjs`);
const prefix = `temporary${process.pid}_`;
function namespace(code) {
for (const name of ['admin', 'teacher1', 'teacher2'])
@@ -101,7 +108,12 @@ try {
if (!ready) throw new Error('临时服务未就绪');
fs.writeFileSync(
testFile,
namespace(fs.readFileSync(path.join(root, 'packages/server/scripts/verify-e2e.mjs'), 'utf8')),
namespace(
fs.readFileSync(
path.join(root, suite ? `scripts/verify-${suite}.mjs` : 'packages/server/scripts/verify-e2e.mjs'),
'utf8',
),
),
{ flag: 'wx' },
);
if (process.argv.includes('--web')) {
@@ -110,6 +122,10 @@ try {
env.UI_SMOKE_ADMIN_PASS = 'admin123';
await run(['scripts/ui-smoke/run.mjs'], 'web.log');
} else await run([testFile], 'e2e.log');
if (server.exitCode !== null || server.signalCode !== null)
throw new Error(`临时服务在验收收尾前退出:code=${server.exitCode} signal=${server.signalCode}`);
const readyAtEnd = await fetch(`${env.VERIFY_BASE_URL}/readyz`, { signal: AbortSignal.timeout(3000) });
if (!readyAtEnd.ok) throw new Error(`临时服务收尾检查失败:HTTP ${readyAtEnd.status}`);
} finally {
if (server && server.exitCode === null) {
server.kill();