Files
Visigine/server/index.js
Ihor_Zhekov e344f1b7e7 Initial commit: Visigine (Vite client + Express/SQLite backend)
Container-ready via docker/ compose (frontend nginx + backend Node). Compose adjusted for Coolify on the prod server: frontend uses expose:80 (no host binding — host 8080 is taken by the Coolify proxy; Traefik routes visigine.de), backend ALLOWED_ORIGINS=https://visigine.de. Secrets stay in server/.env (git-ignored); see server/.env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 10:15:06 +02:00

107 lines
3.7 KiB
JavaScript

import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import rateLimit from 'express-rate-limit'
import analyzeRoute from './routes/analyze.js'
import adminRoute from './routes/admin.js'
import adminMonitoringRoute from './routes/admin-monitoring.js'
import demoMonitoringRoute from './routes/demo-monitoring.js'
// Importing the db module here ensures SQLite is opened and the schema is
// applied at startup, before any route handler can hit it.
import './db/index.js'
const app = express()
const PORT = Number(process.env.PORT) || 3001
// Comma-separated whitelist; missing/empty means deny all browser origins.
// Legacy single-origin var is still honored for backwards compat.
const allowedOrigins = (
process.env.ALLOWED_ORIGINS ||
process.env.ALLOWED_ORIGIN ||
''
)
.split(',')
.map((s) => s.trim())
.filter(Boolean)
app.set('trust proxy', 1)
app.use(
cors({
origin(origin, cb) {
// No Origin header: same-origin, curl, server-to-server, health probes.
if (!origin) return cb(null, true)
if (allowedOrigins.includes(origin)) return cb(null, true)
cb(new Error('CORS_NOT_ALLOWED'))
},
methods: ['POST', 'GET'],
allowedHeaders: ['Content-Type', 'X-Admin-Token'],
exposedHeaders: ['X-Cache', 'X-Request-Id'],
})
)
app.use(express.json({ limit: '64kb' }))
const analyzeLimiter = rateLimit({
windowMs: 60 * 1000,
max: 20,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Zu viele Anfragen. Bitte einen Moment warten.' },
})
// Public demo endpoint: 5 / hour / IP. Reserved for iteration 4b landing-page
// teaser; backend works today, no UI exposes it yet.
const demoMonitoringLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Zu viele Anfragen. Bitte später erneut versuchen.' },
})
// Admin middleware: 401 unconditionally when ADMIN_TOKEN is empty/missing.
// This is the kill-switch for production deployments where admin is disabled.
function requireAdmin(req, res, next) {
const token = req.get('X-Admin-Token')
const expected = process.env.ADMIN_TOKEN
if (!expected || !token || token !== expected) {
return res.status(401).json({ error: 'Unauthorized' })
}
next()
}
app.get('/health', (_req, res) => res.json({ ok: true }))
app.use('/api/analyze', analyzeLimiter, analyzeRoute)
// Public demo for iteration 4b. Backend wired now, no UI yet.
app.use('/api/demo/monitoring', demoMonitoringLimiter, demoMonitoringRoute)
// Admin: token gate first, then the routers. No rate limiter is applied here
// because admin routes are gated by ADMIN_TOKEN and intended for the owner.
// The public /api/autofix/zip endpoint is intentionally NOT mounted — the
// public site only shows a teaser; downloadable files are paid-product only.
app.use('/api/admin/monitoring', requireAdmin, adminMonitoringRoute)
app.use('/api/admin', requireAdmin, adminRoute)
// Generic error handler — never leak stack traces.
app.use((err, _req, res, _next) => {
if (err?.message === 'CORS_NOT_ALLOWED') {
if (res.headersSent) return
return res.status(403).json({ error: 'Zugriff verweigert.' })
}
console.error('[express-error]', err?.message || err)
if (res.headersSent) return
res.status(500).json({ error: 'Ein interner Fehler ist aufgetreten.' })
})
// Bind 0.0.0.0 so containers / reverse proxies can reach the listener.
app.listen(PORT, '0.0.0.0', () => {
console.log(`[visigine-server] listening on 0.0.0.0:${PORT}`)
})
process.on('unhandledRejection', (reason) => {
console.error('[unhandledRejection]', reason)
})
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err?.message || err)
})