Files
happy-export/backend/routes/system.js
T
Drsakura 1ffcc1ea07 feat: add admin web updates and verified username changes
Add isolated update jobs with database backups, progress logs and application rollback. Remove default account hints and prevent default admin recreation after renaming.

Enforce customer and order search scopes, secure session cookies, fix quotation rounding and amount wording, and update backend dependencies. Add API and deployment regression coverage.
2026-10-22 17:24:19 +08:00

99 lines
3.6 KiB
JavaScript

/**
* 系统信息与版本更新检查。
* 挂载点:/api/system
*/
const express = require('express');
const updater = require('../lib/updater');
const branding = require('../lib/branding');
const auth = require('../lib/auth');
const webUpdate = require('../lib/webUpdate');
const fs = require('fs');
const path = require('path');
const router = express.Router();
function adminOnly(req, res, next) {
if (!req.user) return res.status(401).json({ error: '请先登录' });
if (req.user.role !== 'admin') return res.status(403).json({ error: '仅系统管理员可执行系统更新' });
next();
}
router.get('/update/status', adminOnly, async (req, res) => {
const status = await webUpdate.readStatus();
let log = '';
if (/^[a-f0-9]{16}$/.test(status.id || '')) {
const data = path.resolve(process.env.DATA_DIR || path.join(__dirname, '../data'));
try {
const file = path.join(data, 'system-update', status.id, 'update.log');
const fd = fs.openSync(file, 'r');
try {
const size = fs.fstatSync(fd).size;
const buffer = Buffer.alloc(Math.min(size, 12000));
fs.readSync(fd, buffer, 0, buffer.length, Math.max(0, size - buffer.length));
log = buffer.toString('utf8');
} finally { fs.closeSync(fd); }
} catch { /* 任务尚未开始写日志 */ }
}
res.setHeader('Cache-Control', 'no-store');
res.json({ ...status, log, capability: webUpdate.capability() });
});
router.post('/update', adminOnly, async (req, res) => {
try {
auth.verifyCurrentPassword(req, req.body?.current_password);
const status = await webUpdate.start(req);
auth.audit({ username: req.user.username, action: 'system_update', detail: `网页更新至 ${status.version}`, req });
res.status(202).json(status);
} catch (error) { res.status(error.statusCode || 500).json({ error: error.message }); }
});
// 简易缓存:GitHub 未认证请求每小时只有 60 次配额,连续点「检查更新」不该把额度刷完。
const CACHE_TTL_MS = 30 * 1000;
let cache = { key: '', at: 0, payload: null };
/**
* GET /api/system/info
* 当前版本与运行环境。轻量、不发外部请求,设置页打开时就能调。
*/
router.get('/info', auth.requireAuth, (req, res) => {
try {
res.json(updater.systemInfo());
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* GET /api/system/check-update?repo=owner/repo&force=1
* 对比远端仓库版本与本机版本。repo 省略时用默认仓库(Drsakura/happy-export)。
*/
router.get('/check-update', auth.requireAuth, async (req, res) => {
try {
const repo = updater.normalizeRepo(process.env.HAPPY_UPDATE_REPO) || updater.DEFAULT_REPO;
const force = String(req.query.force || '') === '1';
if (!force && cache.payload && cache.key === repo && Date.now() - cache.at < CACHE_TTL_MS) {
return res.json({ ...cache.payload, cached: true });
}
const result = await updater.checkUpdate(repo);
cache = { key: repo, at: Date.now(), payload: result };
res.json({ ...result, cached: false });
} catch (error) {
// checkUpdate 内部已兜底,走到这里说明是意料之外的问题
res.status(500).json({ ok: false, error: error.message });
}
});
/**
* GET /api/system/branding
* 品牌与组织名。公开接口 —— 登录页在拿到会话之前就得知道系统叫什么。
* 只回显示用的名字,不含任何业务数据。
*/
router.get('/branding', (req, res) => {
try {
res.json(branding.getBranding());
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;