Add VitePress site with local login and deployment docs

Enable static site build on port 12100 with Express session auth, auto sidebar, and DEPLOY.md for Ubuntu/NPS setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 16:12:35 +08:00
parent 2a1dc95253
commit 68d816b3f3
102 changed files with 4419 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"username": "admin",
"password": "change-me",
"sessionSecret": "please-change-this-session-secret"
}
+144
View File
@@ -0,0 +1,144 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import cookieParser from 'cookie-parser'
import express from 'express'
import session from 'express-session'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const distPath = path.join(root, '.vitepress', 'dist')
const authExamplePath = path.join(__dirname, 'auth.config.example.json')
const authConfigPath = path.join(__dirname, 'auth.config.json')
const PORT = 12100
function loadAuthConfig() {
if (!fs.existsSync(authConfigPath)) {
fs.copyFileSync(authExamplePath, authConfigPath)
console.warn(
'[auth] 已生成 server/auth.config.json,请修改默认用户名和密码后重启服务。',
)
}
const raw = fs.readFileSync(authConfigPath, 'utf8')
const config = JSON.parse(raw)
if (!config.username || !config.password) {
throw new Error('server/auth.config.json 缺少 username 或 password')
}
if (!config.sessionSecret || config.sessionSecret === 'please-change-this-session-secret') {
console.warn('[auth] 建议在 server/auth.config.json 中设置独立的 sessionSecret')
}
return config
}
const authConfig = loadAuthConfig()
const app = express()
app.set('trust proxy', 1)
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(
session({
name: 'dao_de_jing.sid',
secret: authConfig.sessionSecret || 'dao-de-jing-default-secret',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'lax',
},
}),
)
function isAuthenticated(req) {
return Boolean(req.session?.user)
}
function authGuard(req, res, next) {
if (req.path === '/login' || req.path.startsWith('/api/login')) {
return next()
}
if (isAuthenticated(req)) {
return next()
}
if (req.path.startsWith('/api/')) {
return res.status(401).json({ message: '未登录' })
}
const redirect = encodeURIComponent(req.originalUrl || '/')
return res.redirect(`/login?redirect=${redirect}`)
}
app.get('/login', (req, res) => {
if (isAuthenticated(req)) {
const redirect = req.query.redirect || '/'
return res.redirect(String(redirect))
}
return res.sendFile(path.join(__dirname, 'public', 'login.html'))
})
app.post('/api/login', (req, res) => {
const { username, password } = req.body || {}
if (username === authConfig.username && password === authConfig.password) {
req.session.user = { username }
return res.json({ ok: true })
}
return res.status(401).json({ message: '用户名或密码错误' })
})
app.post('/api/logout', authGuard, (req, res) => {
req.session.destroy(() => {
res.clearCookie('dao_de_jing.sid')
res.json({ ok: true })
})
})
app.get('/api/me', authGuard, (req, res) => {
res.json({ user: req.session.user })
})
app.use(authGuard)
if (!fs.existsSync(distPath)) {
console.error(
'未找到构建产物 .vitepress/dist,请先运行: npm run build',
)
process.exit(1)
}
app.use(express.static(distPath, { index: false }))
app.get('*', (req, res, next) => {
if (req.path.includes('.')) {
return next()
}
const rel = req.path.replace(/^\//, '').replace(/\/$/, '') || 'index'
const candidates = [
path.join(distPath, `${rel}.html`),
path.join(distPath, rel, 'index.html'),
path.join(distPath, '404.html'),
]
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return res.sendFile(candidate)
}
}
return next()
})
app.listen(PORT, '0.0.0.0', () => {
console.log(`DAO DE JING 站点已启动: http://0.0.0.0:${PORT}`)
console.log('登录页: /login')
})
+132
View File
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 · DAO DE JING</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: #e8e8e8;
}
.card {
width: min(92vw, 380px);
padding: 2rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(8px);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
}
h1 {
margin: 0 0 0.25rem;
font-size: 1.35rem;
font-weight: 600;
}
p.sub {
margin: 0 0 1.5rem;
color: #a8b2c1;
font-size: 0.9rem;
}
label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.85rem;
color: #c5cdd8;
}
input {
width: 100%;
margin-bottom: 1rem;
padding: 0.65rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
background: rgba(0, 0, 0, 0.25);
color: #fff;
font-size: 1rem;
}
input:focus {
outline: none;
border-color: #5b8def;
}
button {
width: 100%;
padding: 0.7rem;
border: none;
border-radius: 8px;
background: #3b82f6;
color: #fff;
font-size: 1rem;
cursor: pointer;
}
button:hover { background: #2563eb; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
.error {
display: none;
margin-bottom: 1rem;
padding: 0.6rem 0.75rem;
border-radius: 8px;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35);
color: #fca5a5;
font-size: 0.85rem;
}
.error.show { display: block; }
</style>
</head>
<body>
<div class="card">
<h1>DAO DE JING</h1>
<p class="sub">请登录后访问站点</p>
<div id="error" class="error"></div>
<form id="loginForm">
<label for="username">用户名</label>
<input id="username" name="username" autocomplete="username" required />
<label for="password">密码</label>
<input id="password" name="password" type="password" autocomplete="current-password" required />
<button type="submit" id="submitBtn">登录</button>
</form>
</div>
<script>
const form = document.getElementById('loginForm')
const errorEl = document.getElementById('error')
const submitBtn = document.getElementById('submitBtn')
const params = new URLSearchParams(window.location.search)
const redirect = params.get('redirect') || '/'
form.addEventListener('submit', async (event) => {
event.preventDefault()
errorEl.classList.remove('show')
submitBtn.disabled = true
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: form.username.value.trim(),
password: form.password.value,
}),
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(data.message || '登录失败')
}
window.location.href = redirect
} catch (error) {
errorEl.textContent = error.message || '登录失败'
errorEl.classList.add('show')
} finally {
submitBtn.disabled = false
}
})
</script>
</body>
</html>