Files
ClassHelper/packages/server/src/modules/system/system.service.ts
T
LoveChengke 66be3a63b1 feat(authz): 重写权限模板分配与班级授权边界
拆分能力、分配范围和业务关系,固定管理员能力并收紧组织级写入口。
补齐内容、课表、设备会话和实时通道授权,兼容旧库分配及历史快照。
重写权限分配预览,按实际可操作范围展示维护入口,合并学生入口并独立学年升级页。
增加授权攻击矩阵及静态门禁,更新隔离回归与开发契约。

验证:授权矩阵 23 段通过,Node 24 下 Web 63/63,类型检查、lint 与文档严格构建通过。
2026-10-31 09:40:23 +08:00

94 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { assertAdmin } from '../../lib/authz/index.js';
import type { TokenPayload } from '../../lib/jwt.js';
import { createAutostartController } from './autostart.js';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {
CLIENT_ONLINE_WINDOW_MS,
type AutostartStatusDto,
type SystemDiagnosticsDto,
type SystemSettingsDto,
} from '@classhelper/shared';
import { env } from '../../config/env.js';
import { prisma } from '../../lib/db.js';
import { logger } from '../../lib/logger.js';
import { SETTING_KEYS, getBooleanSetting, setBooleanSetting } from '../../lib/settings.js';
import { APP_VERSION } from '../../lib/version.js';
import type { UpdateAutostartInput, UpdateSystemSettingsInput } from './system.schemas.js';
/**
* 服务端系统设置:开发者模式、开机自启、运行诊断。
*
* 为什么"开机自启"要做成接口而不是只在安装器里勾一下:教室服务器换机器、改过安装目录、
* 或者管理员当时没勾,事后都得能补 —— 安装器只负责**默认值**,运行期由这里改。
* 它改的是**操作系统**的东西(注册表 Run 项 / systemd 单元),所以:
* 1. 只在"装好的形态"里放行(开发机、免安装包、容器里返回 `supported:false`);
* 2. 失败原因是运维性质的(权限不足、单元文件不存在),一律带原文回给管理员,不吞成 500。
*/
function runCommand(file: string, args: string[]): { ok: boolean; output: string } {
const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true });
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
return { ok: result.status === 0, output: output || (result.error ? result.error.message : '') };
}
export async function getSystemSettings(): Promise<SystemSettingsDto> {
return { developerMode: await getBooleanSetting(SETTING_KEYS.developerMode, false) };
}
export async function updateSystemSettings(
user: TokenPayload,
input: UpdateSystemSettingsInput,
): Promise<SystemSettingsDto> {
assertAdmin(user);
if (input.developerMode !== undefined) {
await setBooleanSetting(SETTING_KEYS.developerMode, input.developerMode);
logger.info(`开发者模式已${input.developerMode ? '开启' : '关闭'}(操作人=${user.username})`);
}
return getSystemSettings();
}
const autostart = createAutostartController({
platform: process.platform,
root: path.resolve(env.serverRoot, '..'),
exists: fs.existsSync,
run: runCommand,
});
export async function getAutostartStatus(): Promise<AutostartStatusDto> {
return autostart.read();
}
export async function updateAutostart(
actor: TokenPayload,
input: UpdateAutostartInput,
): Promise<AutostartStatusDto> {
assertAdmin(actor);
return autostart.write(input.enabled);
}
export async function getDiagnostics(): Promise<SystemDiagnosticsDto> {
const now = Date.now();
// 延迟 import:registry 会 import 本模块,顶层互相引用在 ESM 下会拿到未初始化的绑定
const { apiModules } = await import('../registry.js');
const [clientReleaseCount, onlineClientCount, settings] = await Promise.all([
prisma.clientRelease.count(),
prisma.clientDevice.count({
where: { lastHeartbeatAt: { gt: new Date(now - CLIENT_ONLINE_WINDOW_MS) } },
}),
getSystemSettings(),
]);
return {
version: APP_VERSION,
nodeVersion: process.versions.node,
platform: `${process.platform}-${process.arch}`,
uptimeSeconds: Math.round(process.uptime()),
databaseProvider: env.databaseProvider,
serverRoot: env.serverRoot,
dataDir: path.resolve(env.serverRoot, '..', 'data'),
clientReleaseCount,
onlineClientCount,
moduleCount: apiModules.length,
developerMode: settings.developerMode,
};
}