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