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>
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user