2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
5.5 KiB
JavaScript
192 lines
5.5 KiB
JavaScript
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,
|
|
};
|
|
}
|