ecd4f25700
Generate PNG icons from favicon.svg at build time so manifest icons resolve, replace legacy font tags with div wrappers, and tighten mobile layout so long text wraps instead of clipping. Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
5.5 KiB
JavaScript
223 lines
5.5 KiB
JavaScript
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
|
|
}
|
|
|
|
function normalizeRoutePath(urlPath) {
|
|
let route = decodeURIComponent(urlPath)
|
|
if (!route.startsWith('/')) route = `/${route}`
|
|
if (route.length > 1 && route.endsWith('/')) route = route.slice(0, -1)
|
|
return route === '' ? '/' : route
|
|
}
|
|
|
|
function resolveHtmlPath(urlPath) {
|
|
const route = normalizeRoutePath(urlPath)
|
|
const rel = route === '/' ? 'index' : route.replace(/^\//, '')
|
|
|
|
const candidates = [
|
|
path.join(distPath, `${rel}.html`),
|
|
path.join(distPath, rel, 'index.html'),
|
|
path.join(distPath, rel, 'README.html'),
|
|
]
|
|
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function isAssetRequest(urlPath) {
|
|
return /\.[a-zA-Z0-9]+$/.test(urlPath) && !urlPath.endsWith('.html')
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
const PUBLIC_PATHS = new Set([
|
|
'/favicon.ico',
|
|
'/favicon.png',
|
|
'/favicon.svg',
|
|
'/apple-touch-icon.png',
|
|
'/icon-192.png',
|
|
'/icon-512.png',
|
|
'/site.webmanifest',
|
|
'/vp-icons.css',
|
|
'/hashmap.json',
|
|
])
|
|
|
|
function isPublicPath(reqPath) {
|
|
if (PUBLIC_PATHS.has(reqPath)) return true
|
|
if (reqPath.startsWith('/assets/')) return true
|
|
if (reqPath === '/sw.js' || reqPath.endsWith('.webmanifest')) return true
|
|
if (/^\/workbox-[\w-]+\.js$/.test(reqPath)) return true
|
|
return false
|
|
}
|
|
|
|
function authGuard(req, res, next) {
|
|
if (req.path === '/login' || req.path.startsWith('/api/login')) {
|
|
return next()
|
|
}
|
|
|
|
if (isPublicPath(req.path)) {
|
|
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)
|
|
|
|
function sendDistFile(res, relativePath, contentType) {
|
|
const filePath = path.join(distPath, relativePath)
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).end('Not Found')
|
|
}
|
|
if (contentType) res.type(contentType)
|
|
return res.sendFile(filePath)
|
|
}
|
|
|
|
if (!fs.existsSync(distPath)) {
|
|
console.error(
|
|
'未找到构建产物 .vitepress/dist,请先运行: npm run build',
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
app.get('/site.webmanifest', (req, res) => {
|
|
sendDistFile(res, 'site.webmanifest', 'application/manifest+json')
|
|
})
|
|
|
|
app.use(
|
|
express.static(distPath, {
|
|
index: false,
|
|
fallthrough: true,
|
|
setHeaders(res, filePath) {
|
|
if (filePath.endsWith('.webmanifest')) {
|
|
res.setHeader('Content-Type', 'application/manifest+json')
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
|
|
app.use((req, res, next) => {
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
return next()
|
|
}
|
|
|
|
if (isAssetRequest(req.path)) {
|
|
return next()
|
|
}
|
|
|
|
const htmlPath = resolveHtmlPath(req.path)
|
|
if (htmlPath) {
|
|
return res.sendFile(htmlPath)
|
|
}
|
|
|
|
const notFound = path.join(distPath, '404.html')
|
|
if (fs.existsSync(notFound)) {
|
|
return res.status(404).sendFile(notFound)
|
|
}
|
|
|
|
return res.status(404).send('Not Found')
|
|
})
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`DAO DE JING 站点已启动: http://0.0.0.0:${PORT}`)
|
|
console.log('登录页: /login')
|
|
})
|