Files
memind/wiki-auth.mjs
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

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,
};
}