/** * server.js – Express-Backend für Encore. * * Funktionen: * - Proxy für die Ticketmaster Discovery API (API-Key bleibt serverseitig) * - Einfache Benutzerverwaltung (In-Memory, kein Persistenz-Layer) * - Einladungssystem zwischen registrierten Benutzern * * Sicherheitshinweise (OWASP): * - A02 Cryptographic Failures: Passwörter werden nicht gehasht (bewusste Vereinfachung) * - A07 Identification and Authentication Failures: Keine Tokens/Sessions, nur Header-Auth * - Der API-Key liegt serverseitig in .env und wird nie an den Client übermittelt */ import express from "express"; import cors from "cors"; import dotenv from "dotenv"; dotenv.config(); const app = express(); // Middleware: CORS erlaubt Anfragen vom Frontend (localhost), JSON-Body parsen app.use(cors()); app.use(express.json()); // ========================= // ROOT – Health-Check // ========================= app.get("/", (req, res) => { res.send("Encore API is running"); }); // ========================= // TICKETMASTER-PROXY // Versteckt den API-Key vor dem Client (A02: Sensitive Data Exposure vermeiden) // GET /api/events?city={city} // ========================= const TM_API_KEY = process.env.TM_API_KEY; app.get("/api/events", async (req, res) => { const city = req.query.city; if (!city) { return res.status(400).json({ message: "Stadt ist erforderlich" }); } try { const url = `https://app.ticketmaster.com/discovery/v2/events.json?apikey=${TM_API_KEY}&city=${encodeURIComponent(city)}`; const response = await fetch(url); const data = await response.json(); res.json(data); } catch (error) { res.status(500).json({ message: "Fehler beim Laden der Events" }); } }); // ========================= // BENUTZERVERWALTUNG // In-Memory-Speicher (Daten gehen bei Serverneustart verloren – bewusste Vereinfachung) // ========================= let users = []; /** * POST /api/user – Neuen Benutzer registrieren. * Vergibt ein Standard-Passwort "1234" (vereinfacht, kein Hashing). * Antwortet mit 201 Created bei Erfolg, 400 bei fehlendem/doppeltem Benutzernamen. */ app.post("/api/user", (req, res) => { const { username } = req.body; if (!username) { return res.status(400).json({ message: "Benutzername erforderlich" }); } if (users.find(u => u.username === username)) { return res.status(400).json({ message: "Benutzer existiert bereits" }); } const password = "1234"; users.push({ username, password }); res.status(201).json({ name: username, password }); }); /** * GET /api/user – Benutzer anmelden. * Credentials kommen als X-Username und X-Password Header. * Antwortet mit 200 OK bei Erfolg, 401 Unauthorized bei falschen Credentials. * * Hinweis: GET für eine Login-Operation ist unkonventionell (normalerweise POST). * Hier bewusst so gewählt, da kein Session/Token-Mechanismus implementiert ist. */ app.get("/api/user", (req, res) => { const username = req.header("X-Username"); const password = req.header("X-Password"); const user = users.find(u => u.username === username && u.password === password); if (!user) return res.sendStatus(401); res.sendStatus(200); }); // ========================= // EINLADUNGSSYSTEM // Verwaltet Event-Einladungen zwischen Benutzern // ========================= let invitations = []; let idCounter = 1; /** * Normalisiert Benutzernamen für Vergleiche. * Dadurch werden z.B. "Anna" und " anna " als derselbe Benutzer behandelt. */ function normalizeUsername(username) { return String(username || "").trim().toLowerCase(); } /** * Erstellt einen stabilen Event-Schlüssel für den Duplikat-Check. * Ticketmaster liefert normalerweise eine id. Falls diese fehlt, verwenden wir URL oder Namen. */ function getEventKey({ eventId, eventUrl, eventName }) { return String(eventId || eventUrl || eventName || "").trim().toLowerCase(); } /** * POST /api/invitation – Einladung senden. * Erwartet: Header X-Username (Sender), Body { toUser, eventId, eventName, eventUrl } * Antwortet mit 201 Created und dem erstellten Einladungsobjekt. */ app.post("/api/invitation", (req, res) => { if (!req.body) { return res.status(400).json({ message: "Request-Body fehlt" }); } const fromUserRaw = req.header("X-Username"); const { toUser: toUserRaw, eventId, eventName, eventUrl } = req.body; const fromUser = String(fromUserRaw || "").trim(); const toUser = String(toUserRaw || "").trim(); const fromUserKey = normalizeUsername(fromUser); const toUserKey = normalizeUsername(toUser); const eventKey = getEventKey({ eventId, eventUrl, eventName }); if (!fromUser || !toUser) { return res.status(400).json({ message: "Sender und Empfänger sind erforderlich" }); } if (!eventKey) { return res.status(400).json({ message: "Event fehlt. Einladung kann nicht gespeichert werden." }); } // Prüfen ob der Empfänger registriert ist. // Case-insensitive, damit "Anna" und "anna" nicht als verschiedene Nutzer behandelt werden. const recipient = users.find(u => normalizeUsername(u.username) === toUserKey); if (!recipient) { return res.status(404).json({ message: `Nutzer "${toUser}" ist nicht registriert.` }); } // Nicht sich selbst einladen if (fromUserKey === toUserKey) { return res.status(400).json({ message: "Du kannst dich nicht selbst einladen." }); } // Doppelte Einladung verhindern: // Derselbe Sender darf denselben Empfänger nicht mehrfach zum selben Event einladen. // Wichtig: Der Vergleich läuft über normalisierte Keys, nicht über direkte Strings. const duplicateInvitation = invitations.find(inv => inv.fromUserKey === fromUserKey && inv.toUserKey === toUserKey && inv.eventKey === eventKey ); if (duplicateInvitation) { return res.status(409).json({ message: `Du hast "${recipient.username}" bereits zu diesem Event eingeladen.` }); } const invitation = { id: idCounter++, fromUser, fromUserKey, toUser: recipient.username, toUserKey, eventId: eventId || null, eventKey, eventName, eventUrl: eventUrl || null, status: "pending" }; invitations.push(invitation); res.status(201).json(invitation); }); /** * GET /api/invitation – Alle Einladungen für einen Benutzer abrufen. * Erwartet: Header X-Username (Empfänger) * Antwortet mit 200 und Array der Einladungen. */ app.get("/api/invitation", (req, res) => { const user = req.header("X-Username"); if (!user) { return res.status(400).json({ message: "Benutzername fehlt" }); } const userKey = normalizeUsername(user); const userInvitations = invitations.filter(inv => inv.toUserKey === userKey); res.json(userInvitations); }); /** * PUT /api/invitation/:id – Einladungsstatus aktualisieren (annehmen oder ablehnen). * Erwartet: Body { status: "accepted" | "declined" } * HTTP-Verb PUT gemäss REST: Update einer bestehenden Ressource. * Antwortet mit 200 OK und dem aktualisierten Einladungsobjekt. */ app.put("/api/invitation/:id", (req, res) => { const inv = invitations.find(i => i.id == req.params.id); if (!inv) return res.status(404).json({ message: "Einladung nicht gefunden" }); const { status } = req.body; if (!["accepted", "declined"].includes(status)) { return res.status(400).json({ message: "Ungültiger Status. Erlaubt: accepted, declined" }); } inv.status = status; res.json(inv); }); /** * DELETE /api/invitation/:id – Einladung löschen. * HTTP-Verb DELETE gemäss REST: Entfernt die Ressource dauerhaft. * Antwortet mit 204 No Content bei Erfolg. */ app.delete("/api/invitation/:id", (req, res) => { const index = invitations.findIndex(i => i.id == req.params.id); if (index === -1) return res.status(404).json({ message: "Einladung nicht gefunden" }); invitations.splice(index, 1); res.sendStatus(204); // 204 No Content – Ressource erfolgreich gelöscht }); // ========================= // SERVER STARTEN // ========================= app.listen(3000, () => { console.log("Server läuft auf http://localhost:3000"); });