包含以下核心内容: 1. 完整的Docker容器化部署方案,支持多架构amd64/arm64 2. 自研Web管理面板,包含登录认证、实例管理、权限控制 3. 微信实例容器镜像,内置中文字体与输入法修复 4. 飞牛OS应用打包适配 5. 完整的文档与运维指南
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
import { randomBytes } from 'node:crypto';
|
||
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
||
import { dirname } from 'node:path';
|
||
|
||
interface Session {
|
||
userId: string;
|
||
expires: number;
|
||
}
|
||
|
||
// 会话时长:可配置(天),默认 30 天。避免频繁重登(issue #95)。
|
||
const DAYS = Math.max(1, Number(process.env.WOC_SESSION_DAYS) || 30);
|
||
export const SESSION_TTL_MS = DAYS * 24 * 60 * 60 * 1000;
|
||
|
||
// 持久化到磁盘(与 accounts.json 同目录)。关键:面板重启 / 一键自更新 / 看门狗重建都会重启进程,
|
||
// 旧版会话只在内存里 → 每次都被踢下线要重输密码(这正是 #95 说的"记住密码没生效")。落盘后即可跨重启保持登录。
|
||
const FILE = `${dirname(process.env.PANEL_DATA || '/data/panel/accounts.json')}/sessions.json`;
|
||
const sessions = new Map<string, Session>();
|
||
|
||
function load() {
|
||
try {
|
||
if (!existsSync(FILE)) return;
|
||
const obj = JSON.parse(readFileSync(FILE, 'utf8')) as Record<string, Session>;
|
||
const now = Date.now();
|
||
for (const [t, s] of Object.entries(obj)) {
|
||
if (s && typeof s.userId === 'string' && typeof s.expires === 'number' && s.expires > now) sessions.set(t, s);
|
||
}
|
||
} catch {
|
||
/* 文件损坏则视作无会话(用户重登一次即可),不影响启动 */
|
||
}
|
||
}
|
||
|
||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||
function save() {
|
||
if (saveTimer) return; // 防抖:短时间多次变更合并一次写盘
|
||
saveTimer = setTimeout(() => {
|
||
saveTimer = null;
|
||
try {
|
||
mkdirSync(dirname(FILE), { recursive: true });
|
||
const now = Date.now();
|
||
const obj: Record<string, Session> = {};
|
||
for (const [t, s] of sessions) if (s.expires > now) obj[t] = s;
|
||
const tmp = `${FILE}.tmp`;
|
||
writeFileSync(tmp, JSON.stringify(obj), { mode: 0o600 });
|
||
renameSync(tmp, FILE);
|
||
} catch {
|
||
/* 写盘失败不致命:本进程内存里仍有会话 */
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
load();
|
||
|
||
export function createSession(userId: string) {
|
||
const token = randomBytes(32).toString('hex');
|
||
sessions.set(token, { userId, expires: Date.now() + SESSION_TTL_MS });
|
||
save();
|
||
return token;
|
||
}
|
||
|
||
export function getSession(token?: string) {
|
||
if (!token) return null;
|
||
const s = sessions.get(token);
|
||
if (!s) return null;
|
||
if (s.expires < Date.now()) {
|
||
sessions.delete(token);
|
||
save();
|
||
return null;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
export function destroySession(token?: string) {
|
||
if (token && sessions.delete(token)) save();
|
||
}
|
||
|
||
// 禁用/删除账号后,立即踢掉其所有在线会话
|
||
export function destroyUserSessions(userId: string) {
|
||
let changed = false;
|
||
for (const [token, s] of sessions) {
|
||
if (s.userId === userId) {
|
||
sessions.delete(token);
|
||
changed = true;
|
||
}
|
||
}
|
||
if (changed) save();
|
||
}
|