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>
This commit is contained in:
106
server/index.js
Normal file
106
server/index.js
Normal file
@@ -0,0 +1,106 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user