import express from 'express'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createWikiAuth } from './wiki-auth.mjs'; import { PUBLISH_ROOT_DIR } from './user-publish.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = Number(process.env.WIKI_PORT ?? 3000); const app = express(); app.set('trust proxy', 1); const auth = createWikiAuth(path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki-db')); const jsonBody = express.json({ limit: '1mb' }); function parseCookies(header = '') { return Object.fromEntries( header.split(';').map(p => p.trim()).filter(Boolean).map(p => { const idx = p.indexOf('='); return idx < 0 ? [p, ''] : [decodeURIComponent(p.slice(0, idx)), decodeURIComponent(p.slice(idx + 1))]; }), ); } function requireAuth(req, res, next) { const cookies = parseCookies(req.get('cookie')); const token = cookies[auth.COOKIE_NAME]; const session = auth.verify(token); if (!session) { return res.status(401).json({ message: '未登录' }); } req.user = session; next(); } // ============ Auth Routes ============ app.post('/api/auth/register', jsonBody, (req, res) => { const { username, password, displayName } = req.body || {}; if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }); } if (username.length < 2 || username.length > 20) { return res.status(400).json({ message: '用户名长度2-20个字符' }); } if (password.length < 4) { return res.status(400).json({ message: '密码至少4个字符' }); } const result = auth.register(username, password, displayName); if (!result.ok) { return res.status(409).json({ message: result.message }); } res.json({ ok: true, user: result.user }); }); app.post('/api/auth/login', jsonBody, (req, res) => { const { username, password } = req.body || {}; if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }); } const result = auth.login(username, password); if (!result.ok) { return res.status(401).json({ message: result.message }); } const isSecure = req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https'; res.set('Set-Cookie', `${auth.COOKIE_NAME}=${encodeURIComponent(result.token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800${isSecure ? '; Secure' : ''}`); res.json({ ok: true, user: result.user }); }); app.post('/api/auth/logout', (req, res) => { const cookies = parseCookies(req.get('cookie')); auth.revoke(cookies[auth.COOKIE_NAME]); const isSecure = req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https'; res.set('Set-Cookie', `${auth.COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${isSecure ? '; Secure' : ''}`); res.json({ ok: true }); }); app.get('/api/auth/me', (req, res) => { const cookies = parseCookies(req.get('cookie')); const session = auth.verify(cookies[auth.COOKIE_NAME]); if (!session) return res.json({ authenticated: false }); const user = auth.getUser(session.username); res.json({ authenticated: true, user }); }); // ============ Page Routes ============ app.get('/api/pages', requireAuth, (req, res) => { const { q } = req.query; if (q) { return res.json(auth.searchPages(req.user.username, q)); } res.json(auth.listPages(req.user.username)); }); app.get('/api/pages/:slug', requireAuth, (req, res) => { const page = auth.getPage(req.user.username, req.params.slug); if (!page) return res.status(404).json({ message: '页面不存在' }); res.json(page); }); app.post('/api/pages/:slug', requireAuth, jsonBody, (req, res) => { const { title, content, tags } = req.body || {}; const slug = req.params.slug; const page = auth.savePage(req.user.username, slug, title, content, tags); res.json(page); }); app.delete('/api/pages/:slug', requireAuth, (req, res) => { auth.deletePage(req.user.username, req.params.slug); res.json({ ok: true }); }); // ============ Static Files ============ app.use('/wiki', express.static(path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki'))); app.get('/wiki/*', (req, res) => { res.sendFile(path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki', 'index.html')); }); app.listen(PORT, '0.0.0.0', () => { console.log(`📚 Wiki系统运行中 @ http://0.0.0.0:${PORT}/wiki`); console.log(` 数据目录: ${path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki-db')}`); });