257 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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");
});