- 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
210 lines
6.2 KiB
JavaScript
210 lines
6.2 KiB
JavaScript
/**
|
||
* 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");
|
||
});
|