import express from 'express' import session from 'express-session' import path from 'node:path' import { manifestRootPath, manifestDirPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths' import { indexRouter } from './routes/index' import { opRouter } from './routes/op' const PORT = Number(process.env.PORT ?? 3000) // 터미널에서 Ctrl+클릭으로 바로 열 수 있도록 기본값은 127.0.0.1. // 외부 노출이 필요할 때만 HOST=0.0.0.0 환경변수로 덮어씀. const HOST = process.env.HOST ?? '127.0.0.1' const app = express() app.set('view engine', 'ejs') app.set('views', viewsDirPath) app.set('trust proxy', 1) app.use(express.urlencoded({ extended: true })) app.use(express.json()) app.use(session({ secret: process.env.SESSION_SECRET ?? 'music-quiz-installer-dev-secret', resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', maxAge: 1000 * 60 * 60 * 8 } })) // account.json은 외부 노출 절대 금지. app.use((req, res, next) => { if (/^\/account\.json/i.test(req.path)) { res.status(404).send('Not Found') return } next() }) app.use('/static', express.static(publicDirPath)) // 외부 노출이 필요한 정적 자원만 화이트리스트로 라우팅한다. app.get('/manifest.json', (_req, res) => { res.sendFile(manifestRootPath) }) // 설치기에서 개별 음악퀴즈 JSON을 가져갈 수 있도록 파일 단위로만 허용. // 디렉토리 리스팅, 다른 확장자, 경로 탈출은 차단. app.get('/manifest/:fileName', (req, res) => { const fileName = req.params.fileName if (!/^[a-zA-Z0-9_\-]+\.json$/.test(fileName)) { res.status(404).send('Not Found') return } res.sendFile(path.join(manifestDirPath, fileName), (err) => { if (!err || res.headersSent) return res.status(404).send('Not Found') }) }) // 그 외 /manifest/ 하위 모든 요청 차단 (디렉토리 인덱스 포함). app.use((req, res, next) => { if (/^\/manifest\//i.test(req.path)) { res.status(404).send('Not Found') return } next() }) app.use('/file', express.static(fileDirPath, { fallthrough: true, index: false })) app.use('/', indexRouter) app.use('/', opRouter) app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { console.error(err) const message = err instanceof Error ? err.message : '알 수 없는 오류' res.status(500).send(`서버 오류: ${message}`) }) app.listen(PORT, HOST, () => { console.log(`[server] http://${HOST}:${PORT}`) console.log(`[server] views: ${path.relative(process.cwd(), viewsDirPath)}`) })