Files
tmaker.sori.studio/backend/index.js
2026-08-12 17:48:34 +09:00

140 lines
4.5 KiB
JavaScript

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