Nandrx ae3692697b Add REST-compliant PUT/DELETE endpoints and invitation validation
- Replace POST /invitation/:id/accept|decline with PUT /invitation/:id
- Add DELETE /invitation/:id (full CRUD: GET, POST, PUT, DELETE)
- Validate invitation recipient exists before creating invitation (404)
- Block self-invitation (400)
- Propagate server error messages to inline card feedback
- Add Loeschen button to invitation cards (demonstrates DELETE in UI)
- Improve empty state with icon and descriptive text
2026-05-31 17:52:25 +02:00

210 lines
6.2 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) {
console.error("Ticketmaster-Fehler:", 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;
/**
* POST /api/invitation Einladung senden.
* Erwartet: Header X-Username (Sender), Body { toUser, eventId, eventName }
* 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 fromUser = req.header("X-Username");
const { toUser, eventId, eventName } = req.body;
if (!fromUser || !toUser) {
return res.status(400).json({ message: "Sender und Empfänger sind erforderlich" });
}
// Prüfen ob der Empfänger registriert ist
if (!users.find(u => u.username === toUser)) {
return res.status(404).json({ message: `Nutzer "${toUser}" ist nicht registriert.` });
}
// Nicht sich selbst einladen
if (fromUser === toUser) {
return res.status(400).json({ message: "Du kannst dich nicht selbst einladen." });
}
const invitation = {
id: idCounter++,
fromUser,
toUser,
eventId,
eventName,
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 userInvitations = invitations.filter(inv => inv.toUser === user);
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");
});