feature/ui-redesign #8
@ -1,114 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* eventCard.js – Erstellt interaktive Event-Karten als DOM-Elemente.
|
||||||
|
* Jede Karte zeigt Titel, Datum, Veranstaltungsort sowie Speichern- und Einladen-Buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine vollständige Event-Karte als <article>-Element.
|
||||||
|
* @param {Object} event - Event-Objekt mit id, name, date, time, venue, category
|
||||||
|
* @returns {HTMLElement} Fertig aufgebautes Karten-Element
|
||||||
|
*/
|
||||||
export function createEventCard(event) {
|
export function createEventCard(event) {
|
||||||
|
|
||||||
const article = document.createElement("article");
|
const article = document.createElement("article");
|
||||||
article.className = "event-card";
|
article.className = "event-card";
|
||||||
|
|
||||||
|
// --- TITEL ---
|
||||||
const title = document.createElement("h3");
|
const title = document.createElement("h3");
|
||||||
title.className = "event-card__title";
|
title.className = "event-card__title";
|
||||||
title.textContent = event.name;
|
title.textContent = event.name;
|
||||||
|
|
||||||
|
// --- DATUM & UHRZEIT formatieren (Schweizer Format: dd.mm.yyyy) ---
|
||||||
const formattedDate = event.date
|
const formattedDate = event.date
|
||||||
? new Date(event.date).toLocaleDateString("de-CH", {
|
? new Date(event.date).toLocaleDateString("de-CH", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric"
|
year: "numeric"
|
||||||
})
|
})
|
||||||
: "Date not available";
|
: "Datum unbekannt";
|
||||||
|
|
||||||
const formattedTime = event.time
|
const formattedTime = event.time ? event.time.slice(0, 5) : "";
|
||||||
? event.time.slice(0, 5)
|
const dateTime = formattedTime ? `${formattedDate}, ${formattedTime}` : formattedDate;
|
||||||
: "";
|
|
||||||
|
|
||||||
const dateTime = formattedTime
|
|
||||||
? `${formattedDate}, ${formattedTime}`
|
|
||||||
: formattedDate;
|
|
||||||
|
|
||||||
const date = document.createElement("p");
|
const date = document.createElement("p");
|
||||||
date.className = "event-card__date";
|
date.className = "event-card__date";
|
||||||
date.textContent = dateTime;
|
date.textContent = dateTime;
|
||||||
|
|
||||||
|
// --- VERANSTALTUNGSORT ---
|
||||||
const venue = document.createElement("p");
|
const venue = document.createElement("p");
|
||||||
venue.className = "event-card__venue";
|
venue.className = "event-card__venue";
|
||||||
venue.textContent = event.venue;
|
venue.textContent = event.venue || "Ort unbekannt";
|
||||||
|
|
||||||
article.append(title, date, venue);
|
article.append(title, date, venue);
|
||||||
|
|
||||||
// =========================
|
// --- BUTTON-GRUPPE ---
|
||||||
// BUTTON CONTAINER
|
|
||||||
// =========================
|
|
||||||
const buttonContainer = document.createElement("div");
|
const buttonContainer = document.createElement("div");
|
||||||
buttonContainer.className = "d-flex gap-2 mt-2";
|
buttonContainer.className = "d-flex gap-2 mt-2 flex-wrap";
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// SAVE BUTTON
|
// SPEICHERN-BUTTON
|
||||||
|
// Speichert das Event im localStorage, verhindert Duplikate
|
||||||
// =========================
|
// =========================
|
||||||
const saveBtn = document.createElement("button");
|
const saveBtn = document.createElement("button");
|
||||||
saveBtn.textContent = "Save";
|
saveBtn.type = "button";
|
||||||
saveBtn.className = "btn btn-outline-primary btn-sm";
|
saveBtn.className = "btn btn-outline-primary btn-sm";
|
||||||
|
|
||||||
|
// Prüfen ob bereits gespeichert → Button-Text entsprechend setzen
|
||||||
|
const alreadySaved = isAlreadySaved(event.id);
|
||||||
|
saveBtn.textContent = alreadySaved ? "Gespeichert ✓" : "Speichern";
|
||||||
|
if (alreadySaved) saveBtn.disabled = true;
|
||||||
|
|
||||||
saveBtn.addEventListener("click", () => {
|
saveBtn.addEventListener("click", () => {
|
||||||
|
|
||||||
if (!window.currentUser) {
|
|
||||||
alert("Please login to save events");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||||||
|
|
||||||
// prevent duplicates
|
// Duplikat-Schutz
|
||||||
if (saved.find(e => e.id === event.id)) {
|
if (saved.find(e => e.id === event.id)) {
|
||||||
alert("Event already saved");
|
saveBtn.textContent = "Gespeichert ✓";
|
||||||
|
saveBtn.disabled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saved.push(event);
|
saved.push(event);
|
||||||
localStorage.setItem("savedEvents", JSON.stringify(saved));
|
localStorage.setItem("savedEvents", JSON.stringify(saved));
|
||||||
|
|
||||||
alert("Event saved!");
|
// Visuelles Feedback statt alert()
|
||||||
|
saveBtn.textContent = "Gespeichert ✓";
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.classList.replace("btn-outline-primary", "btn-success");
|
||||||
});
|
});
|
||||||
|
|
||||||
saveBtn.disabled = !window.currentUser;
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// INVITE BUTTON
|
// EINLADEN-BUTTON
|
||||||
|
// Zeigt ein Inline-Formular an (kein browser-prompt())
|
||||||
// =========================
|
// =========================
|
||||||
const inviteBtn = document.createElement("button");
|
const inviteBtn = document.createElement("button");
|
||||||
inviteBtn.textContent = "Invite";
|
inviteBtn.type = "button";
|
||||||
|
inviteBtn.textContent = "Einladen";
|
||||||
inviteBtn.className = "btn btn-primary btn-sm";
|
inviteBtn.className = "btn btn-primary btn-sm";
|
||||||
|
inviteBtn.disabled = false;
|
||||||
|
|
||||||
inviteBtn.addEventListener("click", async () => {
|
inviteBtn.addEventListener("click", () => {
|
||||||
|
|
||||||
if (!window.currentUser) {
|
if (!window.currentUser) {
|
||||||
alert("Please login to invite users");
|
showCardMessage(article, "Bitte einloggen um Einladungen zu senden.", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Formular ein-/ausblenden (Toggle)
|
||||||
const toUser = prompt("Enter username to invite:");
|
const existing = article.querySelector(".invite-form");
|
||||||
if (!toUser) return;
|
if (existing) {
|
||||||
|
existing.remove();
|
||||||
await fetch("http://localhost:3000/api/invitation", {
|
} else {
|
||||||
method: "POST",
|
article.appendChild(createInviteForm(event, article));
|
||||||
headers: {
|
}
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Username": window.currentUser
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
toUser,
|
|
||||||
eventId: event.id,
|
|
||||||
eventName: event.name
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
alert("Invitation sent!");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
inviteBtn.disabled = !window.currentUser;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// APPEND BUTTONS
|
|
||||||
// =========================
|
|
||||||
buttonContainer.append(saveBtn, inviteBtn);
|
buttonContainer.append(saveBtn, inviteBtn);
|
||||||
article.appendChild(buttonContainer);
|
article.appendChild(buttonContainer);
|
||||||
|
|
||||||
return article;
|
return article;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// HILFSFUNKTION – Einladungsformular
|
||||||
|
// Erstellt ein kleines Inline-Formular mit Benutzername-Eingabe
|
||||||
|
// =========================
|
||||||
|
function createInviteForm(event, card) {
|
||||||
|
const form = document.createElement("div");
|
||||||
|
form.className = "invite-form mt-2 d-flex gap-2 align-items-center flex-wrap";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.className = "form-control form-control-sm";
|
||||||
|
input.placeholder = "Benutzername eingeben";
|
||||||
|
input.setAttribute("aria-label", "Benutzername für Einladung");
|
||||||
|
|
||||||
|
const sendBtn = document.createElement("button");
|
||||||
|
sendBtn.type = "button";
|
||||||
|
sendBtn.textContent = "Senden";
|
||||||
|
sendBtn.className = "btn btn-primary btn-sm";
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement("button");
|
||||||
|
cancelBtn.type = "button";
|
||||||
|
cancelBtn.textContent = "Abbrechen";
|
||||||
|
cancelBtn.className = "btn btn-outline-secondary btn-sm";
|
||||||
|
|
||||||
|
cancelBtn.addEventListener("click", () => form.remove());
|
||||||
|
|
||||||
|
sendBtn.addEventListener("click", async () => {
|
||||||
|
const toUser = input.value.trim();
|
||||||
|
if (!toUser) {
|
||||||
|
input.classList.add("is-invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.textContent = "Sende...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("http://localhost:3000/api/invitation", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Username": window.currentUser
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
toUser,
|
||||||
|
eventId: event.id,
|
||||||
|
eventName: event.name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// Fehlermeldung vom Server auslesen (z.B. "Nutzer nicht registriert")
|
||||||
|
const errData = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.message || `Fehler ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formular entfernen und Erfolgsmeldung anzeigen
|
||||||
|
form.remove();
|
||||||
|
showCardMessage(card, `Einladung an "${toUser}" gesendet.`, "success");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.textContent = "Senden";
|
||||||
|
showCardMessage(card, err.message || "Fehler beim Senden der Einladung.", "danger");
|
||||||
|
console.error("Einladungs-Fehler:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.append(input, sendBtn, cancelBtn);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// HILFSFUNKTION – Status-Nachricht in Karte anzeigen
|
||||||
|
// Zeigt eine kurze Meldung innerhalb der Karte an und blendet sie nach 3s aus
|
||||||
|
// =========================
|
||||||
|
function showCardMessage(card, message, type) {
|
||||||
|
// Alte Nachrichten entfernen
|
||||||
|
card.querySelectorAll(".card-message").forEach(el => el.remove());
|
||||||
|
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.className = `card-message text-${type} mt-1 mb-0`;
|
||||||
|
msg.textContent = message;
|
||||||
|
card.appendChild(msg);
|
||||||
|
|
||||||
|
setTimeout(() => msg.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// HILFSFUNKTION – Prüft ob Event bereits gespeichert ist
|
||||||
|
// =========================
|
||||||
|
function isAlreadySaved(eventId) {
|
||||||
|
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||||||
|
return saved.some(e => e.id === eventId);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* eventList.js – Rendert eine Liste von Event-Karten in einen Container.
|
||||||
|
* Delegiert die Erstellung einzelner Karten an createEventCard().
|
||||||
|
*/
|
||||||
|
|
||||||
import { createEventCard } from "./eventCard.js";
|
import { createEventCard } from "./eventCard.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leert den Container und fügt für jedes Event eine Karte ein.
|
||||||
|
* Zeigt eine Meldung an, wenn keine Events gefunden wurden.
|
||||||
|
* @param {Array} events - Array von Event-Objekten
|
||||||
|
* @param {HTMLElement} container - Ziel-DOM-Element
|
||||||
|
*/
|
||||||
export function renderEventList(events, container) {
|
export function renderEventList(events, container) {
|
||||||
|
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
container.innerHTML = "No events found.";
|
const state = document.createElement("div");
|
||||||
|
state.className = "empty-state";
|
||||||
|
|
||||||
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "empty-state__icon";
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
icon.textContent = "🎭";
|
||||||
|
|
||||||
|
const title = document.createElement("p");
|
||||||
|
title.className = "empty-state__text";
|
||||||
|
title.textContent = "Keine Events gefunden";
|
||||||
|
|
||||||
|
const sub = document.createElement("p");
|
||||||
|
sub.className = "empty-state__sub";
|
||||||
|
sub.textContent = "Versuche eine andere Stadt oder erweitere den Datumsbereich.";
|
||||||
|
|
||||||
|
state.append(icon, title, sub);
|
||||||
|
container.appendChild(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,5 +40,4 @@ export function renderEventList(events, container) {
|
|||||||
const card = createEventCard(event);
|
const card = createEventCard(event);
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
252
server/server.js
252
server/server.js
@ -1,167 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* 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 express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Middleware
|
// Middleware: CORS erlaubt Anfragen vom Frontend (localhost), JSON-Body parsen
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// ROOT
|
// ROOT – Health-Check
|
||||||
// =========================
|
// =========================
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.send("Encore API is running");
|
res.send("Encore API is running");
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// TICKETMASTER EVENTS (PROXY)
|
// 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;
|
const TM_API_KEY = process.env.TM_API_KEY;
|
||||||
|
|
||||||
app.get("/api/events", async (req, res) => {
|
app.get("/api/events", async (req, res) => {
|
||||||
|
|
||||||
const city = req.query.city;
|
const city = req.query.city;
|
||||||
|
|
||||||
if (!city) {
|
if (!city) {
|
||||||
return res.status(400).json({ message: "City is required" });
|
return res.status(400).json({ message: "Stadt ist erforderlich" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `https://app.ticketmaster.com/discovery/v2/events.json?apikey=${TM_API_KEY}&city=${city}`;
|
const url = `https://app.ticketmaster.com/discovery/v2/events.json?apikey=${TM_API_KEY}&city=${encodeURIComponent(city)}`;
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
res.json(data);
|
res.json(data);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Event fetch error:", error);
|
console.error("Ticketmaster-Fehler:", error);
|
||||||
res.status(500).json({ message: "Error fetching events" });
|
res.status(500).json({ message: "Fehler beim Laden der Events" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// INVITATIONS SYSTEM
|
// BENUTZERVERWALTUNG
|
||||||
|
// In-Memory-Speicher (Daten gehen bei Serverneustart verloren – bewusste Vereinfachung)
|
||||||
// =========================
|
// =========================
|
||||||
let invitations = [];
|
|
||||||
let idCounter = 1;
|
|
||||||
|
|
||||||
// SEND INVITATION
|
|
||||||
app.post("/api/invitation", (req, res) => {
|
|
||||||
|
|
||||||
if (!req.body) {
|
|
||||||
return res.status(400).json({ message: "Missing request body" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromUser = req.header("X-Username");
|
|
||||||
const { toUser, eventId, eventName } = req.body;
|
|
||||||
|
|
||||||
if (!fromUser || !toUser) {
|
|
||||||
return res.status(400).json({ message: "Missing users" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitation = {
|
|
||||||
id: idCounter++,
|
|
||||||
fromUser,
|
|
||||||
toUser,
|
|
||||||
eventId,
|
|
||||||
eventName,
|
|
||||||
status: "pending"
|
|
||||||
};
|
|
||||||
|
|
||||||
invitations.push(invitation);
|
|
||||||
|
|
||||||
res.json(invitation);
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET INVITATIONS FOR USER
|
|
||||||
app.get("/api/invitation", (req, res) => {
|
|
||||||
|
|
||||||
const user = req.header("X-Username");
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(400).json({ message: "Missing username" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInvitations = invitations.filter(
|
|
||||||
inv => inv.toUser === user
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(userInvitations);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ACCEPT INVITATION
|
|
||||||
app.post("/api/invitation/:id/accept", (req, res) => {
|
|
||||||
|
|
||||||
const inv = invitations.find(i => i.id == req.params.id);
|
|
||||||
|
|
||||||
if (!inv) {
|
|
||||||
return res.status(404).json({ message: "Invitation not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
inv.status = "accepted";
|
|
||||||
|
|
||||||
res.json(inv);
|
|
||||||
});
|
|
||||||
|
|
||||||
// DECLINE INVITATION
|
|
||||||
app.post("/api/invitation/:id/decline", (req, res) => {
|
|
||||||
|
|
||||||
const inv = invitations.find(i => i.id == req.params.id);
|
|
||||||
|
|
||||||
if (!inv) {
|
|
||||||
return res.status(404).json({ message: "Invitation not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
inv.status = "declined";
|
|
||||||
|
|
||||||
res.json(inv);
|
|
||||||
});
|
|
||||||
|
|
||||||
let users = [];
|
let users = [];
|
||||||
|
|
||||||
// CREATE USER
|
/**
|
||||||
|
* 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) => {
|
app.post("/api/user", (req, res) => {
|
||||||
|
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
return res.status(400).json({ message: "Username required" });
|
return res.status(400).json({ message: "Benutzername erforderlich" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (users.find(u => u.username === username)) {
|
if (users.find(u => u.username === username)) {
|
||||||
return res.status(400).json({ message: "User exists" });
|
return res.status(400).json({ message: "Benutzer existiert bereits" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const password = "1234";
|
const password = "1234";
|
||||||
|
users.push({ username, password });
|
||||||
|
|
||||||
const user = { username, password };
|
res.status(201).json({ name: username, password });
|
||||||
users.push(user);
|
|
||||||
|
|
||||||
res.json({ name: username, password });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// LOGIN
|
/**
|
||||||
|
* 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) => {
|
app.get("/api/user", (req, res) => {
|
||||||
|
|
||||||
const username = req.header("X-Username");
|
const username = req.header("X-Username");
|
||||||
const password = req.header("X-Password");
|
const password = req.header("X-Password");
|
||||||
|
|
||||||
const user = users.find(
|
const user = users.find(u => u.username === username && u.password === password);
|
||||||
u => u.username === username && u.password === password
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) return res.sendStatus(401);
|
if (!user) return res.sendStatus(401);
|
||||||
|
|
||||||
res.sendStatus(201);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// START SERVER
|
// 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, () => {
|
app.listen(3000, () => {
|
||||||
console.log("Server running on http://localhost:3000");
|
console.log("Server läuft auf http://localhost:3000");
|
||||||
});
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user