feat: WM2026 Tippspiel - Initial Backend + Frontend
This commit is contained in:
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
Reference in New Issue
Block a user