Files
DAO_DE_JING/server/index.js
T
dekun a8be586652 Fix PWA shortcut icon on Windows
Allow unauthenticated access to favicon and manifest, generate favicon.ico, and add multi-size manifest icons.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 17:33:55 +08:00

203 lines
4.9 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
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)
if (!fs.existsSync(distPath)) {
console.error(
'未找到构建产物 .vitepress/dist,请先运行: npm run build',
)
process.exit(1)
}
app.use(
express.static(distPath, {
index: false,
fallthrough: true,
}),
)
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')
})