feat: WM2026 Tippspiel - Initial Backend + Frontend

This commit is contained in:
Ronny Müller
2026-04-03 21:41:19 +02:00
commit 1c685b90a0
2507 changed files with 997210 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../services/logger';
/**
* Dev-Mode Authentifizierung
*
* Wenn NODE_ENV=development und kein Staffbase JWT vorhanden,
* wird automatisch ein Mock-User eingeloggt.
*
* Mehrere Test-User über Query-Parameter wählbar:
* ?devUser=1 → Ronny Mueller (du)
* ?devUser=2 → Test User 2
* ?devUser=3 → Test User 3
*
* Wird in Production komplett deaktiviert.
*/
const DEV_USERS = [
{
sub: 'dev-user-001',
name: 'Ronny Mueller',
given_name: 'Ronny',
family_name: 'Mueller',
locale: 'de_DE',
branch_id: 'gealan-main',
role: 'editor',
primary_email_address: 'ronny975@gmail.com',
},
{
sub: 'dev-user-002',
name: 'Max Mustermann',
given_name: 'Max',
family_name: 'Mustermann',
locale: 'de_DE',
branch_id: 'gealan-main',
role: 'viewer',
primary_email_address: 'max@gealan.de',
},
{
sub: 'dev-user-003',
name: 'Anna Schmidt',
given_name: 'Anna',
family_name: 'Schmidt',
locale: 'de_DE',
branch_id: 'gealan-werk2',
role: 'viewer',
primary_email_address: 'anna@gealan.de',
},
];
export const devAuth = (req: Request, res: Response, next: NextFunction): void => {
if (process.env.NODE_ENV === 'production') {
// In Production niemals Dev-Mode verwenden
res.status(401).json({ error: 'Unauthorized' });
return;
}
const userIndex = parseInt((req.query.devUser as string) ?? '1') - 1;
const mockUser = DEV_USERS[Math.min(Math.max(userIndex, 0), DEV_USERS.length - 1)];
req.staffbaseUser = mockUser;
req.sbSSO = mockUser;
logger.debug('Dev-Mode: Mock user injected', { user: mockUser.name });
next();
};
+123
View File
@@ -0,0 +1,123 @@
import { Request, Response, NextFunction } from 'express';
// @ts-ignore Staffbase SDK v1.x hat keine vollständigen TypeScript Typen
import staffbaseSDK from '@staffbase/staffbase-plugin-sdk';
// Im SDK v1.x ist `middleware` direkt die ssoMiddleWare-Funktion
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { middleware: ssoMiddleWare } = require('@staffbase/staffbase-plugin-sdk');
import { logger } from '../services/logger';
// ============================================================
// Staffbase SDK Typen (v1.x)
// Das SDK gibt req.sbSSO als SSOTokenData-Objekt zurück
// ============================================================
export interface SBSSOData {
sub: string; // userId
name?: string; // full name
given_name?: string; // first name
family_name?: string; // last name
locale?: string; // z.B. "de_DE"
branch_id?: string; // Branch/Abteilung
role?: string; // "editor" | "viewer" etc.
aud?: string; // Plugin-Audience
exp?: number;
iat?: number;
primary_email_address?: string;
username?: string;
}
// Express Request um sbSSO-Feld erweitern
declare global {
namespace Express {
interface Request {
sbSSO?: SBSSOData;
staffbaseUser?: SBSSOData; // Alias für komfortableren Zugriff
}
}
}
/**
* Staffbase SSO Middleware (SDK v1.x)
*
* Das SDK validiert den JWT (?jwt= Query-Parameter) automatisch
* und hängt die dekodierten User-Daten als req.sbSSO an.
*
* Wir wrappen das in unsere eigene Middleware, die:
* 1. Das SDK aufruft
* 2. req.staffbaseUser als bequemen Alias setzt
* 3. Bei fehlendem/ungültigem Token 401 zurückgibt
*/
export const staffbaseAuth = (pluginSecret?: string, audience?: string) => {
// SDK-Middleware initialisieren
// Secret = RSA Public Key aus STAFFBASE_PUBLIC_KEY env var
// Audience = Plugin-ID aus STAFFBASE_PLUGIN_ID env var (optional)
const sdkSecret = pluginSecret ?? process.env.STAFFBASE_PUBLIC_KEY ?? '';
const sdkAudience = audience ?? process.env.STAFFBASE_PLUGIN_ID ?? '';
const sdkMiddleware = ssoMiddleWare(sdkSecret, sdkAudience);
return (req: Request, res: Response, next: NextFunction): void => {
// Token muss vorhanden sein (Query-Param oder Authorization Header)
const hasToken =
req.query.jwt ||
req.headers.authorization?.startsWith('Bearer ');
if (!hasToken) {
res.status(401).json({
error: 'Unauthorized',
message: 'Kein JWT-Token gefunden. Bitte über Staffbase aufrufen.',
});
return;
}
// Authorization Header → in Query-Param umwandeln (SDK erwartet ?jwt=)
if (!req.query.jwt && req.headers.authorization) {
const token = req.headers.authorization.replace('Bearer ', '');
(req.query as Record<string, string>).jwt = token;
}
// SDK-Middleware aufrufen (setzt req.sbSSO bei Erfolg)
sdkMiddleware(req, res, () => {
if (!req.sbSSO || !req.sbSSO.sub) {
logger.warn('JWT validation failed or token missing', {
path: req.path,
ip: req.ip,
});
res.status(401).json({
error: 'Unauthorized',
message: 'Ungültiger oder abgelaufener JWT-Token.',
});
return;
}
// Alias setzen
req.staffbaseUser = req.sbSSO;
logger.debug('Staffbase JWT validated', {
userId: req.staffbaseUser.sub,
name: req.staffbaseUser.name,
role: req.staffbaseUser.role,
});
next();
});
};
};
/**
* Requires editor role for admin actions.
* Must be used after staffbaseAuth middleware.
*/
export const requireEditor = (
req: Request,
res: Response,
next: NextFunction
): void => {
if (!req.staffbaseUser || req.staffbaseUser.role !== 'editor') {
res.status(403).json({
error: 'Forbidden',
message: 'Editor-Rolle erforderlich.',
});
return;
}
next();
};