import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; let DB_DIR = ''; let USERS_FILE = ''; let PAGES_DIR = ''; function initPaths(dataDir) { DB_DIR = dataDir; USERS_FILE = path.join(DB_DIR, 'users.json'); PAGES_DIR = path.join(DB_DIR, 'pages'); } function ensureDb() { fs.mkdirSync(DB_DIR, { recursive: true }); fs.mkdirSync(PAGES_DIR, { recursive: true }); if (!fs.existsSync(USERS_FILE)) { fs.writeFileSync(USERS_FILE, JSON.stringify({ users: [] }, null, 2), 'utf-8'); } } function readUsers() { ensureDb(); return JSON.parse(fs.readFileSync(USERS_FILE, 'utf-8')); } function writeUsers(data) { ensureDb(); fs.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), 'utf-8'); } function safeEqual(a, b) { const ba = Buffer.from(a); const bb = Buffer.from(b); return ba.length === bb.length && crypto.timingSafeEqual(ba, bb); } export function createWikiAuth(dataDir) { initPaths(dataDir); ensureDb(); const sessions = new Map(); const COOKIE_NAME = 'wiki_session'; function hashPassword(password, salt) { return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex'); } function register(username, password, displayName) { const db = readUsers(); if (db.users.find(u => u.username === username)) { return { ok: false, message: '用户名已存在' }; } const salt = crypto.randomBytes(16).toString('hex'); const hashed = hashPassword(password, salt); const user = { id: crypto.randomUUID(), username, displayName: displayName || username, salt, hashedPassword: hashed, createdAt: Date.now(), }; db.users.push(user); writeUsers(db); return { ok: true, user: { id: user.id, username: user.username, displayName: user.displayName } }; } function login(username, password) { const db = readUsers(); const user = db.users.find(u => u.username === username); if (!user) return { ok: false, message: '用户名或密码错误' }; const hashed = hashPassword(password, user.salt); if (!safeEqual(hashed, user.hashedPassword)) { return { ok: false, message: '用户名或密码错误' }; } const token = crypto.randomBytes(32).toString('base64url'); sessions.set(token, { userId: user.id, username: user.username, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 }); return { ok: true, token, user: { id: user.id, username: user.username, displayName: user.displayName } }; } function verify(token) { if (!token) return null; const session = sessions.get(token); if (!session || session.expiresAt < Date.now()) { sessions.delete(token); return null; } // extend session session.expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; return session; } function revoke(token) { sessions.delete(token); } function getUser(username) { const db = readUsers(); const user = db.users.find(u => u.username === username); if (!user) return null; return { id: user.id, username: user.username, displayName: user.displayName, createdAt: user.createdAt }; } function getAllUsers() { const db = readUsers(); return db.users.map(u => ({ id: u.id, username: u.username, displayName: u.displayName, createdAt: u.createdAt })); } // Page CRUD function listPages(username) { ensureDb(); const userPagesDir = path.join(PAGES_DIR, username); if (!fs.existsSync(userPagesDir)) return []; return fs.readdirSync(userPagesDir) .filter(f => f.endsWith('.json')) .map(f => { const data = JSON.parse(fs.readFileSync(path.join(userPagesDir, f), 'utf-8')); return { id: data.id, title: data.title, slug: data.slug, updatedAt: data.updatedAt, createdAt: data.createdAt, tags: data.tags || [], }; }) .sort((a, b) => b.updatedAt - a.updatedAt); } function getPage(username, slug) { ensureDb(); const filePath = path.join(PAGES_DIR, username, `${slug}.json`); if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } function savePage(username, slug, title, content, tags) { ensureDb(); const userPagesDir = path.join(PAGES_DIR, username); fs.mkdirSync(userPagesDir, { recursive: true }); const filePath = path.join(userPagesDir, `${slug}.json`); const existing = fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, 'utf-8')) : null; const page = { id: existing?.id || crypto.randomUUID(), slug, title: title || slug, content: content || '', tags: tags || [], username, createdAt: existing?.createdAt || Date.now(), updatedAt: Date.now(), }; fs.writeFileSync(filePath, JSON.stringify(page, null, 2), 'utf-8'); return page; } function deletePage(username, slug) { ensureDb(); const filePath = path.join(PAGES_DIR, username, `${slug}.json`); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); return true; } return false; } function searchPages(username, query) { const pages = listPages(username); const q = query.toLowerCase(); return pages.filter(p => p.title.toLowerCase().includes(q) || p.tags.some(t => t.toLowerCase().includes(q)) ); } return { COOKIE_NAME, register, login, verify, revoke, getUser, getAllUsers, listPages, getPage, savePage, deletePage, searchPages, }; }