feat: WM2026 Tippspiel - Initial Backend + Frontend
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
import { staffbaseAuth as createStaffbaseAuth } from './middleware/staffbaseAuth';
|
||||
import { devAuth } from './middleware/devAuth';
|
||||
import { checkDbConnection } from './db/client';
|
||||
import { logger } from './services/logger';
|
||||
|
||||
import matchesRouter from './routes/matches';
|
||||
import tipsRouter from './routes/tips';
|
||||
import leaderboardRouter from './routes/leaderboard';
|
||||
import adminRouter from './routes/admin';
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT ?? '3001');
|
||||
|
||||
// ============================================================
|
||||
// Security Middleware
|
||||
// ============================================================
|
||||
app.use(
|
||||
helmet({
|
||||
// Staffbase lädt das Plugin in einem iFrame
|
||||
// X-Frame-Options muss daher SAMEORIGIN oder ALLOW-FROM erlauben
|
||||
frameguard: false,
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"], // für inline styles im Frontend
|
||||
imgSrc: ["'self'", 'data:', 'https://crests.football-data.org'],
|
||||
frameAncestors: ['https://app.staffbase.com', 'https://*.staffbase.com'],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// CORS – nur Staffbase Origins erlauben
|
||||
const allowedOrigins = (process.env.CORS_ORIGIN ?? 'https://app.staffbase.com')
|
||||
.split(',')
|
||||
.map((o) => o.trim());
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: (origin, callback) => {
|
||||
// Requests ohne Origin (z.B. direkte API-Calls) in Development erlauben
|
||||
if (!origin && process.env.NODE_ENV !== 'production') {
|
||||
return callback(null, true);
|
||||
}
|
||||
if (!origin || allowedOrigins.some((allowed) => origin.includes(allowed.replace('https://', '')))) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error(`CORS blocked: ${origin}`));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10kb' }));
|
||||
|
||||
// Rate limiting – API global
|
||||
app.use(
|
||||
'/api',
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 Minuten
|
||||
max: 200, // Max 200 Requests pro IP
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many requests', retryAfter: '15 minutes' },
|
||||
})
|
||||
);
|
||||
|
||||
// Tighter rate limit für Tipp-Abgabe
|
||||
app.use(
|
||||
'/api/tips',
|
||||
rateLimit({
|
||||
windowMs: 60 * 1000, // 1 Minute
|
||||
max: 20,
|
||||
message: { error: 'Too many tip submissions' },
|
||||
})
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// Health Check (ohne Auth – für Railway/Render/Uptime-Monitoring)
|
||||
// ============================================================
|
||||
app.get('/health', async (_req, res) => {
|
||||
const dbOk = await checkDbConnection();
|
||||
res.status(dbOk ? 200 : 503).json({
|
||||
status: dbOk ? 'ok' : 'degraded',
|
||||
db: dbOk ? 'connected' : 'unreachable',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version ?? '1.0.0',
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Protected API Routes (alle erfordern Staffbase JWT)
|
||||
// ============================================================
|
||||
// Auth: In Development ohne JWT über devAuth, sonst Staffbase JWT
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use('/api', (req, res, next) => {
|
||||
// Wenn JWT vorhanden → Staffbase Auth, sonst Dev-Mode
|
||||
if (req.query.jwt || req.headers.authorization?.startsWith('Bearer ')) {
|
||||
return createStaffbaseAuth()(req, res, next);
|
||||
}
|
||||
return devAuth(req, res, next);
|
||||
});
|
||||
} else {
|
||||
app.use('/api', createStaffbaseAuth());
|
||||
}
|
||||
app.use('/api/matches', matchesRouter);
|
||||
app.use('/api/tips', tipsRouter);
|
||||
app.use('/api/leaderboard', leaderboardRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
|
||||
// ============================================================
|
||||
// Frontend (React Build) – statisches Serving
|
||||
// In Production liefert das Backend auch das Frontend aus
|
||||
// ============================================================
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path') as typeof import('path');
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Error Handler
|
||||
// ============================================================
|
||||
app.use(
|
||||
(
|
||||
err: Error,
|
||||
_req: express.Request,
|
||||
res: express.Response,
|
||||
_next: express.NextFunction
|
||||
) => {
|
||||
logger.error('Unhandled error', {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
name: err.name,
|
||||
});
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// Start Server
|
||||
// ============================================================
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`🚀 WM 2026 Tippspiel Backend running on port ${PORT}`, {
|
||||
env: process.env.NODE_ENV,
|
||||
port: PORT,
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user