Files
minecraft_launcher/src/server/app.ts
claude-bot 7a963d0409 Refine installer step 2/4-1 and bind server to 127.0.0.1 by default
Step 2 (싱글/멀티): replace auto-advance with a card selection plus a
"다음" button so the user can review their choice before moving on.

Step 4-1 (모드 플랫폼): replace the "설치 / 건너뛰기" buttons with two
cards — "권장 플랫폼 설치" and "기본 마인크래프트로 설치". Selection
only records intent; the actual platform install fires in 4-2 along
with mods/resourcepacks (already handled by installer:client install).

Server: default HOST to 127.0.0.1 instead of 0.0.0.0 so the startup
log prints a Ctrl+클릭으로 바로 열 수 있는 URL. Set HOST=0.0.0.0
externally when public exposure is needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 19:51:17 +09:00

87 lines
2.6 KiB
TypeScript

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)}`)
})