const path = require('path') const fs = require('fs') const dotenv = require('dotenv') const express = require('express') const cors = require('cors') const session = require('express-session') const FileStoreFactory = require('session-file-store') const crypto = require('crypto') function loadEnvFileIfExists(fileName) { const envPath = path.join(__dirname, '..', fileName) if (!fs.existsSync(envPath)) return false dotenv.config({ path: envPath }) return true } loadEnvFileIfExists('.env') const nodeEnv = String(process.env.NODE_ENV || 'development').trim().toLowerCase() if (nodeEnv === 'production') { loadEnvFileIfExists('.env.production') } else { const loadedLocal = loadEnvFileIfExists('.env.local') if (!loadedLocal) loadEnvFileIfExists('.env.production') } const { ensureData, getServerStats } = require('./src/db') const authRoutes = require('./src/routes/auth') const topicsRoutes = require('./src/routes/topics') const tierListsRoutes = require('./src/routes/tierlists') const commentsRoutes = require('./src/routes/comments') const usersRoutes = require('./src/routes/users') const adminRoutes = require('./src/routes/admin') const shareRoutes = require('./src/routes/share') const app = express() const PORT = process.env.PORT ? Number(process.env.PORT) : 5179 const SESSION_SECRET = process.env.SESSION_SECRET || 'dev-secret-change-me' const SESSION_COOKIE_SECURE = process.env.SESSION_COOKIE_SECURE === 'true' const SESSION_COOKIE_SAME_SITE = process.env.SESSION_COOKIE_SAME_SITE || 'lax' const TRUST_PROXY = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 1 const allowedOrigins = (process.env.CORS_ORIGINS || '') .split(',') .map((origin) => origin.trim()) .filter(Boolean) const SERVER_STATS_TOKEN = String(process.env.SERVER_STATS_TOKEN || '').trim() const FileStore = FileStoreFactory(session) function getBearerToken(req) { const authorization = String(req.headers.authorization || '') const match = authorization.match(/^Bearer\s+(.+)$/i) if (match) return match[1].trim() return String(req.query.token || '').trim() } function isEqualSecret(a, b) { const left = Buffer.from(String(a || '')) const right = Buffer.from(String(b || '')) return left.length === right.length && crypto.timingSafeEqual(left, right) } function requireServerStatsToken(req, res, next) { if (!SERVER_STATS_TOKEN) return res.status(503).json({ error: 'server_stats_token_not_configured' }) const token = getBearerToken(req) if (!token || !isEqualSecret(token, SERVER_STATS_TOKEN)) return res.status(401).json({ error: 'unauthorized' }) next() } ;['uploads/avatars', 'uploads/topics', 'uploads/custom', 'uploads/tierlists', '.sessions'].forEach((relativePath) => { fs.mkdirSync(path.join(__dirname, relativePath), { recursive: true }) }) app.set('trust proxy', TRUST_PROXY) app.use( cors({ origin: (origin, cb) => { if (!origin) return cb(null, true) if (allowedOrigins.includes(origin)) return cb(null, true) if (/^http:\/\/localhost:517\d$/.test(origin)) return cb(null, true) // 5170~5179 if (/^https?:\/\/127\.0\.0\.1:517\d$/.test(origin)) return cb(null, true) if (origin === 'http://localhost:5173') return cb(null, true) cb(new Error('not_allowed_by_cors')) }, credentials: true, }) ) app.use(express.json({ limit: '10mb' })) app.use(express.urlencoded({ extended: true })) app.use( session({ store: new FileStore({ path: path.join(__dirname, '.sessions'), ttl: 60 * 60 * 24 * 30, retries: 0, }), secret: SESSION_SECRET, proxy: TRUST_PROXY > 0, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: SESSION_COOKIE_SAME_SITE, secure: SESSION_COOKIE_SECURE, maxAge: 1000 * 60 * 60 * 24 * 30, }, }) ) app.use('/uploads', express.static(path.join(__dirname, 'uploads'))) app.get('/health', (req, res) => res.json({ ok: true })) app.use(async (req, res, next) => { try { await ensureData() next() } catch (e) { console.error('[backend] db init failed', e) res.status(500).json({ error: 'db_init_failed' }) } }) app.use('/api/auth', authRoutes) app.get('/api/server-stats', requireServerStatsToken, async (req, res) => { const stats = await getServerStats() res.json(stats) }) app.use('/api/topics', topicsRoutes) app.use('/api/tierlists', tierListsRoutes) app.use('/api/comments', commentsRoutes) app.use('/api/users', usersRoutes) app.use('/api/admin', adminRoutes) app.use('/share', shareRoutes) app.listen(PORT, () => { console.log(`[backend] listening on http://localhost:${PORT}`) })