Umsetzung Aufgabenverwaltung

This commit is contained in:
2026-03-02 15:33:35 +01:00
parent c922ef6668
commit abf85f66e4
43 changed files with 6617 additions and 2103 deletions

View File

@@ -11,11 +11,12 @@ public class AufgabenGruppe {
private String von;
private UUID userId;
private boolean privateGruppe;
private List<Toy> toys;
private List<Aufgabe> aufgaben;
private List<Strafe> strafen;
private List<Sperre> sperren;
private String bild;
private long subscriberCount;
private boolean subscribed;
public UUID getGruppenId() { return gruppenId; }
public void setGruppenId(UUID gruppenId) { this.gruppenId = gruppenId; }
@@ -35,9 +36,6 @@ public class AufgabenGruppe {
public boolean isPrivateGruppe() { return privateGruppe; }
public void setPrivateGruppe(boolean privateGruppe) { this.privateGruppe = privateGruppe; }
public List<Toy> getToys() { return toys; }
public void setToys(List<Toy> toys) { this.toys = toys; }
public List<Aufgabe> getAufgaben() { return aufgaben; }
public void setAufgaben(List<Aufgabe> aufgaben) { this.aufgaben = aufgaben; }
@@ -49,4 +47,10 @@ public class AufgabenGruppe {
public String getBild() { return bild; }
public void setBild(String bild) { this.bild = bild; }
public long getSubscriberCount() { return subscriberCount; }
public void setSubscriberCount(long subscriberCount) { this.subscriberCount = subscriberCount; }
public boolean isSubscribed() { return subscribed; }
public void setSubscribed(boolean subscribed) { this.subscribed = subscribed; }
}

View File

@@ -0,0 +1,23 @@
package de.oaa.xxx.aufgaben;
import java.util.List;
public class AufgabenGruppePage {
private List<AufgabenGruppe> content;
private int currentPage;
private int totalPages;
private long totalElements;
public List<AufgabenGruppe> getContent() { return content; }
public void setContent(List<AufgabenGruppe> content) { this.content = content; }
public int getCurrentPage() { return currentPage; }
public void setCurrentPage(int currentPage) { this.currentPage = currentPage; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
public long getTotalElements() { return totalElements; }
public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
}

View File

@@ -57,10 +57,10 @@ public class DefaultFiller {
void chastityFemale() {
AufgabenGruppeEntity keuschWiebl = createAufgGruppe("Keuschhaltung weiblich", "Enthält verschiedene Aufgaben für Keuschhaltung von weiblichen Spielpartnern", getClass().getClassLoader().getResourceAsStream("femaleCB.png"));
ToyEntity kg = createToy("KG weiblich", "Ein Voll-Keuschheitsgürtel für die Frau", keuschWiebl);
ToyEntity kgVaginal = createToy("KG weiblich, Vaginaldildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginaldildos", keuschWiebl);
ToyEntity kgAnal = createToy("KG weiblich, Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Analdildos", keuschWiebl);
ToyEntity kgDouble = createToy("KG weiblich, Vaginal- u. Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginal- und Analdildos", keuschWiebl);
ToyEntity kg = createToy("KG weiblich", "Ein Voll-Keuschheitsgürtel für die Frau");
ToyEntity kgVaginal = createToy("KG weiblich, Vaginaldildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginaldildos");
ToyEntity kgAnal = createToy("KG weiblich, Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Analdildos");
ToyEntity kgDouble = createToy("KG weiblich, Vaginal- u. Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginal- und Analdildos");
createSperre("Voll-KG", "{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien", 10, 30, Arrays.asList(kg), Arrays.asList(VAGINA), keuschWiebl);
createSperre("Voll-KG + Vaginaldildo", "{PASSIV} trägt fortan einen Voll-KG mit Vaginaldildo, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien", 10, 30, Arrays.asList(kgVaginal), Arrays.asList(VAGINA), keuschWiebl);
@@ -70,9 +70,9 @@ public class DefaultFiller {
void chastityMale() {
AufgabenGruppeEntity keuschMaennl = createAufgGruppe("Keuschhaltung männlich", "Enthält verschiedene Aufgaben für Keuschhaltung von männlichen Spielpartnern", getClass().getClassLoader().getResourceAsStream("maleCB.png"));
ToyEntity kaefig = createToy("Peniskäfig", "Ein gewöhnlicher Peniskäfig", keuschMaennl);
ToyEntity kgMaennl = createToy("KG männlich", "Ein Voll-Keuschheitsgürtel für den Mann", keuschMaennl);
ToyEntity knMaennlAnal = createToy("KG männlich, Analdildo", "Ein Voll-Keuschheitsgürtel für den Mann inkl. eines Analdildos oder -plugs", keuschMaennl);
ToyEntity kaefig = createToy("Peniskäfig", "Ein gewöhnlicher Peniskäfig");
ToyEntity kgMaennl = createToy("KG männlich", "Ein Voll-Keuschheitsgürtel für den Mann");
ToyEntity knMaennlAnal = createToy("KG männlich, Analdildo", "Ein Voll-Keuschheitsgürtel für den Mann inkl. eines Analdildos oder -plugs");
createSperre("Peniskäfig", "{PASSIV} trägt fortan einen Peniskäfig, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von seinem Peniskäfig zu befreien", 10, 30, Arrays.asList(kaefig), Arrays.asList(PENIS), keuschMaennl);
createSperre("Voll-KG", "{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von seinem KG zu befreien", 10, 30, Arrays.asList(kgMaennl), Arrays.asList(PENIS), keuschMaennl);
@@ -81,10 +81,10 @@ public class DefaultFiller {
void plugs() {
AufgabenGruppeEntity gruppe = createAufgGruppe("Plugs", "Enthält verschiedene Aufgaben für das Tragen von Buttplugs über einen gewissen Zeitraum.", getClass().getClassLoader().getResourceAsStream("plugs.png"));
ToyEntity plugKlein = createToy("Plug klein", "Ein kleiner Buttplug", gruppe);
ToyEntity plugMittel = createToy("Plug mittel", "Ein mittelgroßer Buttplug", gruppe);
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug", gruppe);
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst", gruppe);
ToyEntity plugKlein = createToy("Plug klein", "Ein kleiner Buttplug");
ToyEntity plugMittel = createToy("Plug mittel", "Ein mittelgroßer Buttplug");
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug");
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst");
createSperre("Plug klein", "{AKTIV} führt {PASSIV} einen kleinen Buttplug in anal ein, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien", 10, 30, Arrays.asList(plugKlein), Arrays.asList(ANUS), gruppe);
createSperre("Plug mittel", "{AKTIV} führt {PASSIV} einen mittelgroßen Buttplug anal ein, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien", 10, 30, Arrays.asList(plugMittel), Arrays.asList(ANUS), gruppe);
@@ -95,10 +95,10 @@ public class DefaultFiller {
void knebel() {
AufgabenGruppeEntity gruppe = createAufgGruppe("Knebel", "Enthält verschiedene Aufgaben für das Tragen von Knebeln über einen gewissen Zeitraum.", getClass().getClassLoader().getResourceAsStream("knebel.png"));
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel", gruppe);
ToyEntity penisKnebel = createToy("Penisknebel", "Ein Penisknebel", gruppe);
ToyEntity aufblKnebel = createToy("Aufblasbarer Knebel", "Ein aufblasbarer Knebel", gruppe);
ToyEntity isolationsmaske = createToy("Isolationsmaske", "Eine Isolationsmaske", gruppe);
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel");
ToyEntity penisKnebel = createToy("Penisknebel", "Ein Penisknebel");
ToyEntity aufblKnebel = createToy("Aufblasbarer Knebel", "Ein aufblasbarer Knebel");
ToyEntity isolationsmaske = createToy("Isolationsmaske", "Eine Isolationsmaske");
createSperre("Ballknebel", "{AKTIV}, lege {PASSIV} einen Ballknebel an, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.", 10, 30, Arrays.asList(ballKnebel), Arrays.asList(MUND), gruppe);
createSperre("Penisknebel", "{AKTIV}, lege {PASSIV} einen Dildoknebel an, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.", 10, 30, Arrays.asList(penisKnebel), Arrays.asList(MUND), gruppe);
@@ -109,22 +109,22 @@ public class DefaultFiller {
void stafen() {
AufgabenGruppeEntity strafen = createAufgGruppe("Strafen", "Enthält verschiedene Bestrafungen", getClass().getClassLoader().getResourceAsStream("peitsche.png"));
ToyEntity gerte = createToy("Gerte", "Eine gewöhnliche Gerte", strafen);
ToyEntity paddel = createToy("Paddel", "Eine gewöhnliches Paddel", strafen);
ToyEntity peitsche = createToy("Peitsche", "Eine gewöhnliche Peitsche", strafen);
ToyEntity penisKnebel = createToy("Doppel-Penisknebel", "Ein Doppel-Penisknebel", strafen);
ToyEntity handfesseln = createToy("Handfesseln", "Fesseln zum Binden der Hände, z.B. Handschellen", strafen);
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug", strafen);
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst", strafen);
ToyEntity plugPump = createToy("Pump-Plug", "Ein aufblasbarer Plug", strafen);
ToyEntity nippelklemmen = createToy("Nippelklemmen", "Nippelklemmen", strafen);
ToyEntity augenbinde = createToy("Augenbinde", "Eine Augenbinde", strafen);
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel", strafen);
ToyEntity strapon = createToy("Strapon", "Ein Umschnalldildo", strafen);
ToyEntity kgMann = createToy("KG Mann", "Ein Voll-KG oder Peniskäfig für den Mann", strafen);
ToyEntity kgFrau = createToy("KG Frau", "Ein Voll-KG die Frau", strafen);
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo", strafen);
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo", strafen);
ToyEntity gerte = createToy("Gerte", "Eine gewöhnliche Gerte");
ToyEntity paddel = createToy("Paddel", "Eine gewöhnliches Paddel");
ToyEntity peitsche = createToy("Peitsche", "Eine gewöhnliche Peitsche");
ToyEntity penisKnebel = createToy("Doppel-Penisknebel", "Ein Doppel-Penisknebel");
ToyEntity handfesseln = createToy("Handfesseln", "Fesseln zum Binden der Hände, z.B. Handschellen");
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug");
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst");
ToyEntity plugPump = createToy("Pump-Plug", "Ein aufblasbarer Plug");
ToyEntity nippelklemmen = createToy("Nippelklemmen", "Nippelklemmen");
ToyEntity augenbinde = createToy("Augenbinde", "Eine Augenbinde");
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel");
ToyEntity strapon = createToy("Strapon", "Ein Umschnalldildo");
ToyEntity kgMann = createToy("KG Mann", "Ein Voll-KG oder Peniskäfig für den Mann");
ToyEntity kgFrau = createToy("KG Frau", "Ein Voll-KG die Frau");
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo");
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo");
createStrafe("5 Schläge mit flachen Hand", "{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit der flachen Hand auf das Gesäß.",
1, null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), strafen);
@@ -213,9 +213,9 @@ public class DefaultFiller {
void aufgaben() {
AufgabenGruppeEntity aufgaben = createAufgGruppe("Aufgaben", "Enthält verschiedene Sex-Aufgaben.", getClass().getClassLoader().getResourceAsStream("sex.png"));
ToyEntity vibrator = createToy("Vibrator", "Ein herkömmlicher Vibrator.", aufgaben);
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo", aufgaben);
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo", aufgaben);
ToyEntity vibrator = createToy("Vibrator", "Ein herkömmlicher Vibrator.");
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo");
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo");
createAufgabe("Hintern präsentieren", "{AKTIV}, zeig {PASSIV} deinen Hintern, gib dir selber dabei ein oder zwei Klappse auf den Po",
1, null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), aufgaben);
@@ -367,12 +367,11 @@ public class DefaultFiller {
return entity;
}
private ToyEntity createToy(String name, String beschreibung, AufgabenGruppeEntity gruppe) {
private ToyEntity createToy(String name, String beschreibung) {
ToyEntity toy = new ToyEntity();
toy.setToyId(UUID.randomUUID());
toy.setName(name);
toy.setBeschreibung(beschreibung);
toy.setAufgabenGruppe(gruppe);
toyRepository.save(toy);
return toy;
}

View File

@@ -3,7 +3,8 @@ package de.oaa.xxx.aufgaben;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.Image;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -11,19 +12,47 @@ import java.io.IOException;
public class ImageScaler {
public byte[] scale(byte[] origByte) {
try (ByteArrayInputStream bais = new ByteArrayInputStream(origByte)) {
private static final int MAX_SIZE = 128;
public byte[] scale(byte[] origBytes) {
try (ByteArrayInputStream bais = new ByteArrayInputStream(origBytes)) {
BufferedImage orig = ImageIO.read(bais);
BufferedImage scaled = (BufferedImage) orig.getScaledInstance(128, 128, Image.SCALE_DEFAULT);
if (orig == null) {
return origBytes;
}
int origWidth = orig.getWidth();
int origHeight = orig.getHeight();
// Bereits klein genug unverändern zurückgeben
if (origWidth <= MAX_SIZE && origHeight <= MAX_SIZE) {
return origBytes;
}
// Seitenverhältnis beibehalten: längste Seite auf MAX_SIZE
int newWidth, newHeight;
if (origWidth >= origHeight) {
newWidth = MAX_SIZE;
newHeight = Math.max(1, Math.round((float) MAX_SIZE * origHeight / origWidth));
} else {
newHeight = MAX_SIZE;
newWidth = Math.max(1, Math.round((float) MAX_SIZE * origWidth / origHeight));
}
BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.drawImage(orig, 0, 0, newWidth, newHeight, null);
g.dispose();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(scaled, "png", baos);
return baos.toByteArray();
} catch (IOException exception) {
LoggerFactory.getLogger(getClass()).error("Fehler beim Skalieren des Bildes", exception);
}
} catch (IOException exception) {
LoggerFactory.getLogger(getClass()).error("Fehler beim Skalieren des Bildes", exception);
} catch (IOException e) {
LoggerFactory.getLogger(ImageScaler.class).error("Fehler beim Skalieren des Bildes", e);
return origBytes;
}
return new byte[0];
}
}

View File

@@ -7,7 +7,8 @@ public class Toy {
private UUID toyId;
private String name;
private String beschreibung;
private UUID gruppeId;
private UUID userId;
private String bild;
public UUID getToyId() { return toyId; }
public void setToyId(UUID toyId) { this.toyId = toyId; }
@@ -18,6 +19,9 @@ public class Toy {
public String getBeschreibung() { return beschreibung; }
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
public UUID getGruppeId() { return gruppeId; }
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public String getBild() { return bild; }
public void setBild(String bild) { this.bild = bild; }
}

View File

@@ -0,0 +1,15 @@
package de.oaa.xxx.aufgaben;
import java.util.List;
public class ToyList {
private List<Toy> systemToys;
private List<Toy> userToys;
public List<Toy> getSystemToys() { return systemToys; }
public void setSystemToys(List<Toy> systemToys) { this.systemToys = systemToys; }
public List<Toy> getUserToys() { return userToys; }
public void setUserToys(List<Toy> userToys) { this.userToys = userToys; }
}

View File

@@ -0,0 +1,23 @@
package de.oaa.xxx.aufgaben;
import java.util.List;
public class ToyPage {
private List<Toy> content;
private int currentPage;
private int totalPages;
private long totalElements;
public List<Toy> getContent() { return content; }
public void setContent(List<Toy> content) { this.content = content; }
public int getCurrentPage() { return currentPage; }
public void setCurrentPage(int currentPage) { this.currentPage = currentPage; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
public long getTotalElements() { return totalElements; }
public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
}

View File

@@ -0,0 +1,149 @@
package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.AufgabenGruppe;
import de.oaa.xxx.aufgaben.AufgabenGruppePage;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.GruppenAboEntity;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/abo")
@Transactional
public class AboController {
private static final int DEFAULT_PAGE_SIZE = 5;
private static final int DISCOVER_PAGE_SIZE = 10;
private final GruppenAboRepository aboRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final UserRepository userRepository;
public AboController(GruppenAboRepository aboRepository,
AufgabenGruppeRepository gruppeRepository,
UserRepository userRepository) {
this.aboRepository = aboRepository;
this.gruppeRepository = gruppeRepository;
this.userRepository = userRepository;
}
// ── Abonnierte Gruppen laden ──
@GetMapping("/list")
public ResponseEntity<AufgabenGruppePage> listSubscribed(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
List<AufgabenGruppe> dtos = aboRepository.findByUserId(user.getUserId()).stream()
.map(GruppenAboEntity::getAufgabenGruppe)
.filter(g -> !g.isPrivateGruppe()) // ignoriere inzwischen wieder private Gruppen
.map(g -> enrich(g, user.getUserId(), true))
.sorted(Comparator.comparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
.toList();
return ResponseEntity.ok(manualPage(dtos, page, size));
}
// ── Entdecken ──
@GetMapping("/discover")
public ResponseEntity<AufgabenGruppePage> discover(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DISCOVER_PAGE_SIZE) int size,
Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
List<AufgabenGruppe> dtos = gruppeRepository
.findPublicFromOthers(user.getUserId(), namePattern).stream()
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
.toList();
return ResponseEntity.ok(manualPage(dtos, page, size));
}
// ── Abonnieren ──
@PostMapping("/{gruppenId}")
public ResponseEntity<Void> subscribe(@PathVariable UUID gruppenId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity gruppe = gruppeRepository.findById(gruppenId).orElse(null);
if (gruppe == null || gruppe.isPrivateGruppe() || user.getUserId().equals(gruppe.getUserId())) {
return ResponseEntity.badRequest().build();
}
if (aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), gruppe)) {
return ResponseEntity.ok().build();
}
GruppenAboEntity abo = new GruppenAboEntity();
abo.setAboId(UUID.randomUUID());
abo.setUserId(user.getUserId());
abo.setAufgabenGruppe(gruppe);
aboRepository.save(abo);
return ResponseEntity.status(201).build();
}
// ── Abonnement kündigen ──
@DeleteMapping("/{gruppenId}")
public ResponseEntity<Void> unsubscribe(@PathVariable UUID gruppenId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity gruppe = gruppeRepository.findById(gruppenId).orElse(null);
if (gruppe == null) return ResponseEntity.noContent().build();
aboRepository.deleteByUserIdAndAufgabenGruppe(user.getUserId(), gruppe);
return ResponseEntity.accepted().build();
}
// ── Hilfsmethoden ──
private AufgabenGruppe enrich(AufgabenGruppeEntity entity, UUID userId, boolean subscribed) {
AufgabenGruppe g = entity.toAufgabenGruppe();
g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
g.setSubscribed(subscribed);
return g;
}
private AufgabenGruppePage manualPage(List<AufgabenGruppe> all, int page, int size) {
int total = all.size();
int start = page * size;
List<AufgabenGruppe> content = start >= total ? List.of() : all.subList(start, Math.min(start + size, total));
AufgabenGruppePage result = new AufgabenGruppePage();
result.setContent(content);
result.setCurrentPage(page);
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
result.setTotalElements(total);
return result;
}
private UserEntity resolveUser(Principal principal) {
return userRepository.findByEmail(principal.getName()).orElse(null);
}
}

View File

@@ -1,10 +1,13 @@
package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.Aufgabe;
import de.oaa.xxx.aufgaben.Toy;
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -13,11 +16,14 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@@ -29,10 +35,14 @@ public class AufgabeController {
private final AufgabeRepository aufgabeRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public AufgabeController(AufgabeRepository aufgabeRepository, AufgabenGruppeRepository gruppeRepository) {
public AufgabeController(AufgabeRepository aufgabeRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.aufgabeRepository = aufgabeRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{aufgabeId}")
@@ -48,16 +58,39 @@ public class AufgabeController {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(aufgabe.getGruppeId()).orElse(null);
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity);
if (gruppeEntity.getAufgaben().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(aufgabe.getBenoetigteToys());
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity, toys);
aufgabeRepository.save(entity);
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getAufgabeId()).toUri()
).build();
}
@PutMapping("/{aufgabeId}")
public ResponseEntity<Void> update(@PathVariable UUID aufgabeId, @RequestBody Aufgabe aufgabe) {
if (aufgabe.getKurzText() == null || aufgabe.getText() == null || aufgabe.getLevel() == null) {
return ResponseEntity.badRequest().build();
}
AufgabeEntity entity = aufgabeRepository.findById(aufgabeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(aufgabe.getKurzText());
entity.setText(aufgabe.getText());
entity.setLevel(aufgabe.getLevel());
entity.setSekundenVon(aufgabe.getSekundenVon());
entity.setSekundenBis(aufgabe.getSekundenBis());
entity.setBenoetigtAktiv(aufgabe.getBenoetigtAktiv());
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(aufgabe.getBenoetigteToys()));
aufgabeRepository.save(entity);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Aufgabe aufgabe) {
try {
@@ -68,4 +101,14 @@ public class AufgabeController {
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -2,11 +2,25 @@ package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.AufgabenGruppe;
import de.oaa.xxx.aufgaben.AufgabenGruppeList;
import de.oaa.xxx.aufgaben.AufgabenGruppePage;
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.SperreEntity;
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.transaction.annotation.Transactional;
@@ -14,12 +28,21 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@RestController
@@ -28,13 +51,57 @@ import java.util.UUID;
public class AufgabenGruppeController {
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeController.class);
private static final int DEFAULT_PAGE_SIZE = 5;
private final AufgabenGruppeRepository gruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final UserRepository userRepository;
private final GruppenAboRepository aboRepository;
private final ToyRepository toyRepository;
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository) {
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
UserRepository userRepository,
GruppenAboRepository aboRepository,
ToyRepository toyRepository) {
this.gruppeRepository = gruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.userRepository = userRepository;
this.aboRepository = aboRepository;
this.toyRepository = toyRepository;
}
// ── Paginierte Listen ──
@GetMapping("/list/user")
public ResponseEntity<AufgabenGruppePage> listUser(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserId(
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toGruppePage(result, true));
}
@GetMapping("/list/system")
public ResponseEntity<AufgabenGruppePage> listSystem(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserIdIsNull(
PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toGruppePage(result));
}
// ── Bestehende Endpunkte ──
@GetMapping("/all")
public ResponseEntity<AufgabenGruppeList> getAll(@RequestParam(required = false) String search) {
UUID userId = (UUID) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
@@ -60,15 +127,185 @@ public class AufgabenGruppeController {
.orElse(ResponseEntity.noContent().build());
}
// ── Anlegen ──
@PostMapping
public ResponseEntity<Void> create(@RequestBody AufgabenGruppe gruppe) {
public ResponseEntity<Void> create(@RequestBody AufgabenGruppe gruppe, Principal principal) {
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) {
return ResponseEntity.status(409).build();
}
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
entity.setUserId(user.getUserId());
entity.setPrivateGruppe(true);
gruppeRepository.save(entity);
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
).build();
}
// ── Bearbeiten ──
@PutMapping("/{gruppeId}")
public ResponseEntity<Void> update(@PathVariable UUID gruppeId,
@RequestBody AufgabenGruppe gruppe,
Principal principal) {
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
entity.setName(gruppe.getName().trim());
entity.setBeschreibung(gruppe.getBeschreibung());
entity.setVon(gruppe.getVon());
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
if (gruppe.getBild() != null) {
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
}
gruppeRepository.save(entity);
return ResponseEntity.ok().build();
}
// ── Kopieren (Systemgruppe → eigene) ──
@PostMapping("/copy/{gruppeId}")
public ResponseEntity<Void> copy(@PathVariable UUID gruppeId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) {
return ResponseEntity.status(409).build();
}
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
if (source.isPrivateGruppe()) return ResponseEntity.status(403).build();
if (user.getUserId().equals(source.getUserId())) return ResponseEntity.status(403).build();
// Build toy mapping: source toyId → toy entity the copy will reference
Set<ToyEntity> allSourceToys = new HashSet<>();
source.getAufgaben().forEach(a -> { if (a.getBenoetigteToys() != null) allSourceToys.addAll(a.getBenoetigteToys()); });
source.getStrafen().forEach(s -> { if (s.getBenoetigteToys() != null) allSourceToys.addAll(s.getBenoetigteToys()); });
source.getSperren().forEach(sp -> { if (sp.getBenoetigteToys() != null) allSourceToys.addAll(sp.getBenoetigteToys()); });
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
for (ToyEntity sourceToy : allSourceToys) {
if (sourceToy.getUserId() == null) {
// System toy reference directly
toyMapping.put(sourceToy.getToyId(), sourceToy);
} else {
// User toy find existing toy with same name in user's collection, or create a copy
ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), user.getUserId())
.orElseGet(() -> {
ToyEntity tc = new ToyEntity();
tc.setToyId(UUID.randomUUID());
tc.setName(sourceToy.getName());
tc.setBeschreibung(sourceToy.getBeschreibung());
tc.setBild(sourceToy.getBild());
tc.setUserId(user.getUserId());
return toyRepository.save(tc);
});
toyMapping.put(sourceToy.getToyId(), mapped);
}
}
AufgabenGruppeEntity copy = new AufgabenGruppeEntity();
copy.setGruppenId(UUID.randomUUID());
copy.setName(source.getName());
copy.setBeschreibung(source.getBeschreibung());
copy.setVon(source.getVon());
copy.setBild(source.getBild());
copy.setUserId(user.getUserId());
copy.setPrivateGruppe(true);
gruppeRepository.save(copy);
for (AufgabeEntity a : source.getAufgaben()) {
AufgabeEntity ac = new AufgabeEntity();
ac.setAufgabeId(UUID.randomUUID());
ac.setAufgabenGruppe(copy);
ac.setKurzText(a.getKurzText());
ac.setText(a.getText());
ac.setLevel(a.getLevel());
ac.setSekundenVon(a.getSekundenVon());
ac.setSekundenBis(a.getSekundenBis());
ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null);
ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null);
ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping));
aufgabeRepository.save(ac);
}
for (StrafeEntity s : source.getStrafen()) {
StrafeEntity sc = new StrafeEntity();
sc.setStrafeId(UUID.randomUUID());
sc.setAufgabenGruppe(copy);
sc.setKurzText(s.getKurzText());
sc.setText(s.getText());
sc.setLevel(s.getLevel());
sc.setSekundenVon(s.getSekundenVon());
sc.setSekundenBis(s.getSekundenBis());
sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null);
sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null);
sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping));
strafeRepository.save(sc);
}
for (SperreEntity sp : source.getSperren()) {
SperreEntity spc = new SperreEntity();
spc.setSperreId(UUID.randomUUID());
spc.setAufgabenGruppe(copy);
spc.setKurzText(sp.getKurzText());
spc.setText(sp.getText());
spc.setReleaseText(sp.getReleaseText());
spc.setMinutenVon(sp.getMinutenVon());
spc.setMinutenBis(sp.getMinutenBis());
spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null);
spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping));
sperreRepository.save(spc);
}
return ResponseEntity.status(201).build();
}
private List<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
if (source == null || source.isEmpty()) return new ArrayList<>();
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
}
// ── Löschen ──
@DeleteMapping("/{gruppeId}")
public ResponseEntity<Void> deleteById(@PathVariable UUID gruppeId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
if (entity == null) return ResponseEntity.noContent().build();
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
try {
aboRepository.deleteByAufgabenGruppe(entity);
aufgabeRepository.deleteAll(entity.getAufgaben());
strafeRepository.deleteAll(entity.getStrafen());
sperreRepository.deleteAll(entity.getSperren());
gruppeRepository.delete(entity);
return ResponseEntity.accepted().build();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
try {
@@ -79,4 +316,27 @@ public class AufgabenGruppeController {
return ResponseEntity.internalServerError().build();
}
}
// ── Hilfsmethoden ──
private UserEntity resolveUser(Principal principal) {
return userRepository.findByEmail(principal.getName()).orElse(null);
}
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page) {
return toGruppePage(page, false);
}
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page, boolean withSubscriberCount) {
AufgabenGruppePage result = new AufgabenGruppePage();
result.setContent(page.getContent().stream().map(entity -> {
AufgabenGruppe g = entity.toAufgabenGruppe();
if (withSubscriberCount) g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
return g;
}).toList());
result.setCurrentPage(page.getNumber());
result.setTotalPages(page.getTotalPages());
result.setTotalElements(page.getTotalElements());
return result;
}
}

View File

@@ -1,10 +1,13 @@
package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.Sperre;
import de.oaa.xxx.aufgaben.Toy;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.SperreEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -13,11 +16,14 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController("aufgabenSperreController")
@@ -29,10 +35,14 @@ public class SperreController {
private final SperreRepository sperreRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public SperreController(SperreRepository sperreRepository, AufgabenGruppeRepository gruppeRepository) {
public SperreController(SperreRepository sperreRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.sperreRepository = sperreRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{sperreId}")
@@ -49,16 +59,39 @@ public class SperreController {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null);
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity);
if (gruppeEntity.getSperren().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(sperre.getBenoetigteToys());
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity, toys);
sperreRepository.save(entity);
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSperreId()).toUri()
).build();
}
@PutMapping("/{sperreId}")
public ResponseEntity<Void> update(@PathVariable UUID sperreId, @RequestBody Sperre sperre) {
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null
|| sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) {
return ResponseEntity.badRequest().build();
}
SperreEntity entity = sperreRepository.findById(sperreId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(sperre.getKurzText());
entity.setText(sperre.getText());
entity.setReleaseText(sperre.getReleaseText());
entity.setMinutenVon(sperre.getMinutenVon());
entity.setMinutenBis(sperre.getMinutenBis());
entity.setSperreFuer(sperre.getSperreFuer());
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
sperreRepository.save(entity);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Sperre sperre) {
try {
@@ -69,4 +102,14 @@ public class SperreController {
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -1,10 +1,13 @@
package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.Strafe;
import de.oaa.xxx.aufgaben.Toy;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -13,11 +16,14 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@@ -29,10 +35,14 @@ public class StrafeController {
private final StrafeRepository strafeRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public StrafeController(StrafeRepository strafeRepository, AufgabenGruppeRepository gruppeRepository) {
public StrafeController(StrafeRepository strafeRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.strafeRepository = strafeRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{strafeId}")
@@ -48,16 +58,39 @@ public class StrafeController {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(strafe.getGruppeId()).orElse(null);
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity);
if (gruppeEntity.getStrafen().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(strafe.getBenoetigteToys());
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity, toys);
strafeRepository.save(entity);
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getStrafeId()).toUri()
).build();
}
@PutMapping("/{strafeId}")
public ResponseEntity<Void> update(@PathVariable UUID strafeId, @RequestBody Strafe strafe) {
if (strafe.getKurzText() == null || strafe.getText() == null || strafe.getLevel() == null) {
return ResponseEntity.badRequest().build();
}
StrafeEntity entity = strafeRepository.findById(strafeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(strafe.getKurzText());
entity.setText(strafe.getText());
entity.setLevel(strafe.getLevel());
entity.setSekundenVon(strafe.getSekundenVon());
entity.setSekundenBis(strafe.getSekundenBis());
entity.setBenoetigtAktiv(strafe.getBenoetigtAktiv());
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(strafe.getBenoetigteToys()));
strafeRepository.save(entity);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Strafe strafe) {
try {
@@ -68,4 +101,14 @@ public class StrafeController {
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -1,23 +1,38 @@
package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.Toy;
import de.oaa.xxx.aufgaben.ToyPage;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@RestController
@@ -26,13 +41,83 @@ import java.util.UUID;
public class ToyController {
private static final Logger LOGGER = LoggerFactory.getLogger(ToyController.class);
private static final int DEFAULT_PAGE_SIZE = 12;
private final ToyRepository toyRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final UserRepository userRepository;
private final GruppenAboRepository aboRepository;
public ToyController(ToyRepository toyRepository, AufgabenGruppeRepository gruppeRepository) {
public ToyController(ToyRepository toyRepository,
UserRepository userRepository,
GruppenAboRepository aboRepository) {
this.toyRepository = toyRepository;
this.gruppeRepository = gruppeRepository;
this.userRepository = userRepository;
this.aboRepository = aboRepository;
}
@GetMapping("/list/user")
public ResponseEntity<ToyPage> listUser(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
Principal principal) {
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) {
return ResponseEntity.status(401).build();
}
Page<ToyEntity> result = toyRepository.findByUserId(
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toToyPage(result));
}
@GetMapping("/list/system")
public ResponseEntity<ToyPage> listSystem(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
Page<ToyEntity> result = toyRepository.findByUserIdIsNull(
PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toToyPage(result));
}
/**
* Returns all toys available to the current user for assignment to items:
* own toys + system toys + toys referenced in subscribed groups' items.
*/
@GetMapping("/available")
public ResponseEntity<List<Toy>> available(Principal principal) {
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) return ResponseEntity.status(401).build();
List<ToyEntity> own = toyRepository.findByUserId(user.getUserId(), PageRequest.of(0, 500, Sort.by("name"))).getContent();
List<ToyEntity> system = toyRepository.findByUserIdIsNull(PageRequest.of(0, 500, Sort.by("name"))).getContent();
Set<UUID> knownIds = new HashSet<>();
own.forEach(t -> knownIds.add(t.getToyId()));
system.forEach(t -> knownIds.add(t.getToyId()));
Set<ToyEntity> fromAbos = new HashSet<>();
aboRepository.findByUserId(user.getUserId()).forEach(abo -> {
AufgabenGruppeEntity gruppe = abo.getAufgabenGruppe();
gruppe.getAufgaben().forEach(a -> {
if (a.getBenoetigteToys() != null)
a.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
});
gruppe.getStrafen().forEach(s -> {
if (s.getBenoetigteToys() != null)
s.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
});
gruppe.getSperren().forEach(sp -> {
if (sp.getBenoetigteToys() != null)
sp.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
});
});
List<Toy> result = new ArrayList<>();
result.addAll(own.stream().map(ToyEntity::toToy).toList());
result.addAll(system.stream().map(ToyEntity::toToy).toList());
result.addAll(fromAbos.stream()
.sorted(Comparator.comparing(ToyEntity::getName, String.CASE_INSENSITIVE_ORDER))
.map(ToyEntity::toToy).toList());
return ResponseEntity.ok(result);
}
@GetMapping("/{toyId}")
@@ -43,31 +128,120 @@ public class ToyController {
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Toy toy) {
if (toy.getName() == null || toy.getGruppeId() == null) {
public ResponseEntity<Void> create(@RequestBody Toy toy, Principal principal) {
if (toy.getName() == null || toy.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(toy.getGruppeId()).orElse(null);
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
return ResponseEntity.badRequest().build();
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) {
return ResponseEntity.status(401).build();
}
ToyEntity entity = ToyEntity.create(toy, gruppeEntity);
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNull(toy.getName())
|| toyRepository.existsByNameIgnoreCaseAndUserId(toy.getName(), user.getUserId())) {
return ResponseEntity.status(409)
.header("X-Error", "duplicate-name")
.build();
}
ToyEntity entity = ToyEntity.create(toy);
entity.setUserId(user.getUserId());
toyRepository.save(entity);
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getToyId()).toUri()
).build();
}
@DeleteMapping
@Transactional
public ResponseEntity<Void> delete(@RequestBody Toy toy) {
// Bug fix: original code had transaction.rollback() here - now correctly uses @Transactional
@PostMapping("/copy/{toyId}")
public ResponseEntity<Void> copy(@PathVariable UUID toyId, Principal principal) {
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) {
return ResponseEntity.status(401).build();
}
ToyEntity source = toyRepository.findById(toyId).orElse(null);
if (source == null) {
return ResponseEntity.notFound().build();
}
if (source.getUserId() != null) {
return ResponseEntity.status(403).build();
}
if (toyRepository.existsByNameIgnoreCaseAndUserId(source.getName(), user.getUserId())) {
return ResponseEntity.status(409)
.header("X-Error", "duplicate-name")
.build();
}
ToyEntity copy = new ToyEntity();
copy.setToyId(UUID.randomUUID());
copy.setName(source.getName());
copy.setBeschreibung(source.getBeschreibung());
copy.setUserId(user.getUserId());
copy.setBild(source.getBild());
toyRepository.save(copy);
return ResponseEntity.status(201).build();
}
@PutMapping("/{toyId}")
public ResponseEntity<Void> update(@PathVariable UUID toyId, @RequestBody Toy toy, Principal principal) {
if (toy.getName() == null || toy.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) {
return ResponseEntity.status(401).build();
}
ToyEntity entity = toyRepository.findById(toyId).orElse(null);
if (entity == null) {
return ResponseEntity.notFound().build();
}
if (!user.getUserId().equals(entity.getUserId())) {
return ResponseEntity.status(403).build();
}
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(toy.getName(), toyId)
|| toyRepository.existsByNameIgnoreCaseAndUserIdAndToyIdNot(toy.getName(), user.getUserId(), toyId)) {
return ResponseEntity.status(409)
.header("X-Error", "duplicate-name")
.build();
}
entity.setName(toy.getName().trim());
entity.setBeschreibung(toy.getBeschreibung());
if (toy.getBild() != null) {
entity.setBild(Base64.getDecoder().decode(toy.getBild()));
}
toyRepository.save(entity);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{toyId}")
public ResponseEntity<Void> delete(@PathVariable UUID toyId, Principal principal) {
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
if (user == null) {
return ResponseEntity.status(401).build();
}
ToyEntity toy = toyRepository.findById(toyId).orElse(null);
if (toy == null) {
return ResponseEntity.noContent().build();
}
if (!user.getUserId().equals(toy.getUserId())) {
return ResponseEntity.status(403).build();
}
if (toyRepository.countAufgabeUsage(toyId) > 0
|| toyRepository.countStrafeUsage(toyId) > 0
|| toyRepository.countSperreUsage(toyId) > 0) {
return ResponseEntity.status(409).build();
}
try {
toyRepository.findById(toy.getToyId()).ifPresent(toyRepository::delete);
toyRepository.delete(toy);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
private ToyPage toToyPage(Page<ToyEntity> page) {
ToyPage toyPage = new ToyPage();
toyPage.setContent(page.getContent().stream().map(ToyEntity::toToy).toList());
toyPage.setCurrentPage(page.getNumber());
toyPage.setTotalPages(page.getTotalPages());
toyPage.setTotalElements(page.getTotalElements());
return toyPage;
}
}

View File

@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -98,12 +99,12 @@ public class AufgabeEntity {
return aufgabe;
}
public static AufgabeEntity create(Aufgabe aufgabe, AufgabenGruppeEntity aufgabenGruppeEntity) {
public static AufgabeEntity create(Aufgabe aufgabe, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
AufgabeEntity entity = new AufgabeEntity();
entity.setAufgabeId(UUID.randomUUID());
entity.setAufgabenGruppe(aufgabenGruppeEntity);
entity.setBenoetigtAktiv(aufgabe.getBenoetigtAktiv());
entity.setBenoetigteToys(aufgabe.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
entity.setKurzText(aufgabe.getKurzText());
entity.setLevel(aufgabe.getLevel());

View File

@@ -33,10 +33,6 @@ public class AufgabenGruppeEntity {
private byte[] bild;
@Column
private String von;
@Column
private Integer relevanz;
@OneToMany(mappedBy = "aufgabenGruppe")
private List<ToyEntity> toys;
@OneToMany(mappedBy = "aufgabenGruppe")
private List<AufgabeEntity> aufgaben;
@OneToMany(mappedBy = "aufgabenGruppe")
@@ -65,12 +61,6 @@ public class AufgabenGruppeEntity {
public String getVon() { return von; }
public void setVon(String von) { this.von = von; }
public Integer getRelevanz() { return relevanz; }
public void setRelevanz(Integer relevanz) { this.relevanz = relevanz; }
public List<ToyEntity> getToys() { return toys; }
public void setToys(List<ToyEntity> toys) { this.toys = toys; }
public List<AufgabeEntity> getAufgaben() { return aufgaben; }
public void setAufgaben(List<AufgabeEntity> aufgaben) { this.aufgaben = aufgaben; }
@@ -89,7 +79,6 @@ public class AufgabenGruppeEntity {
gruppe.setPrivateGruppe(privateGruppe);
gruppe.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
gruppe.setVon(von);
gruppe.setToys(toys.stream().map(ToyEntity::toToy).toList());
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());

View File

@@ -0,0 +1,35 @@
package de.oaa.xxx.aufgaben.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.UUID;
@Entity
@Table(name = "gruppen_abo")
public class GruppenAboEntity {
@Id
@Column
private UUID aboId;
@Column
private UUID userId;
@ManyToOne
@JoinColumn(name = "gruppenId")
private AufgabenGruppeEntity aufgabenGruppe;
public UUID getAboId() { return aboId; }
public void setAboId(UUID aboId) { this.aboId = aboId; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
}

View File

@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -85,14 +86,15 @@ public class SperreEntity {
sperre.setReleaseText(releaseText);
sperre.setSperreFuer(sperreFuer);
sperre.setText(text);
sperre.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
return sperre;
}
public static SperreEntity create(Sperre sperre, AufgabenGruppeEntity aufgabenGruppeEntity) {
public static SperreEntity create(Sperre sperre, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
SperreEntity entity = new SperreEntity();
entity.setSperreId(UUID.randomUUID());
entity.setAufgabenGruppe(aufgabenGruppeEntity);
entity.setBenoetigteToys(sperre.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
entity.setKurzText(sperre.getKurzText());
entity.setMinutenBis(sperre.getMinutenBis());
entity.setMinutenVon(sperre.getMinutenVon());

View File

@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -98,12 +99,12 @@ public class StrafeEntity {
return strafe;
}
public static StrafeEntity create(Strafe strafe, AufgabenGruppeEntity aufgabenGruppeEntity) {
public static StrafeEntity create(Strafe strafe, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
StrafeEntity entity = new StrafeEntity();
entity.setStrafeId(UUID.randomUUID());
entity.setAufgabenGruppe(aufgabenGruppeEntity);
entity.setBenoetigtAktiv(strafe.getBenoetigtAktiv());
entity.setBenoetigteToys(strafe.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
entity.setKurzText(strafe.getKurzText());
entity.setLevel(strafe.getLevel());

View File

@@ -4,10 +4,10 @@ import de.oaa.xxx.aufgaben.Toy;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import java.util.Base64;
import java.util.UUID;
@Entity
@@ -21,9 +21,11 @@ public class ToyEntity {
private String name;
@Column
private String beschreibung;
@ManyToOne
@JoinColumn(name = "gruppeId")
private AufgabenGruppeEntity aufgabenGruppe;
@Column
private UUID userId;
@Lob
@Column(columnDefinition = "BLOB")
private byte[] bild;
public UUID getToyId() { return toyId; }
public void setToyId(UUID toyId) { this.toyId = toyId; }
@@ -34,24 +36,29 @@ public class ToyEntity {
public String getBeschreibung() { return beschreibung; }
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public byte[] getBild() { return bild; }
public void setBild(byte[] bild) { this.bild = bild; }
public Toy toToy() {
Toy toy = new Toy();
toy.setBeschreibung(beschreibung);
toy.setName(name);
toy.setGruppeId(aufgabenGruppe.getGruppenId());
toy.setToyId(toyId);
toy.setName(name);
toy.setBeschreibung(beschreibung);
toy.setUserId(userId);
toy.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
return toy;
}
public static ToyEntity create(Toy toy, AufgabenGruppeEntity aufgabenGruppeEntity) {
public static ToyEntity create(Toy toy) {
ToyEntity entity = new ToyEntity();
entity.setAufgabenGruppe(aufgabenGruppeEntity);
entity.setBeschreibung(toy.getBeschreibung());
entity.setName(toy.getName());
entity.setToyId(UUID.randomUUID());
entity.setName(toy.getName());
entity.setBeschreibung(toy.getBeschreibung());
entity.setUserId(toy.getUserId());
entity.setBild(toy.getBild() != null ? Base64.getDecoder().decode(toy.getBild()) : null);
return entity;
}
}

View File

@@ -1,7 +1,9 @@
package de.oaa.xxx.aufgaben.repository;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -14,9 +16,18 @@ public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEn
@Query("select age from AufgabenGruppeEntity age where age.userId = :userId")
List<AufgabenGruppeEntity> findByUserId(@Param("userId") UUID userId);
long countByUserId(UUID userId);
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
Page<AufgabenGruppeEntity> findByUserId(UUID userId, Pageable pageable);
@Query("select age from AufgabenGruppeEntity age where (age.privateGruppe = false or age.userId = :userId) and (:search is null or age.name like :search)")
List<AufgabenGruppeEntity> listWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and (:search is null or age.name like :search)")
List<AufgabenGruppeEntity> listPublicWithSearch(@Param("search") String search, PageRequest pageable);
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and age.userId is not null and age.userId <> :userId and (:name is null or lower(age.name) like lower(:name))")
List<AufgabenGruppeEntity> findPublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
}

View File

@@ -0,0 +1,21 @@
package de.oaa.xxx.aufgaben.repository;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.GruppenAboEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface GruppenAboRepository extends JpaRepository<GruppenAboEntity, UUID> {
List<GruppenAboEntity> findByUserId(UUID userId);
boolean existsByUserIdAndAufgabenGruppe(UUID userId, AufgabenGruppeEntity gruppe);
void deleteByUserIdAndAufgabenGruppe(UUID userId, AufgabenGruppeEntity gruppe);
long countByAufgabenGruppe(AufgabenGruppeEntity gruppe);
void deleteByAufgabenGruppe(AufgabenGruppeEntity gruppe);
}

View File

@@ -1,9 +1,37 @@
package de.oaa.xxx.aufgaben.repository;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
import java.util.UUID;
public interface ToyRepository extends JpaRepository<ToyEntity, UUID> {
Page<ToyEntity> findByUserIdIsNull(Pageable pageable);
Page<ToyEntity> findByUserId(UUID userId, Pageable pageable);
boolean existsByNameIgnoreCaseAndUserIdIsNull(String name);
boolean existsByNameIgnoreCaseAndUserId(String name, UUID userId);
boolean existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(String name, UUID toyId);
boolean existsByNameIgnoreCaseAndUserIdAndToyIdNot(String name, UUID userId, UUID toyId);
Optional<ToyEntity> findByNameIgnoreCaseAndUserId(String name, UUID userId);
@Query("SELECT COUNT(a) FROM AufgabeEntity a JOIN a.benoetigteToys t WHERE t.toyId = :toyId")
long countAufgabeUsage(@Param("toyId") UUID toyId);
@Query("SELECT COUNT(s) FROM StrafeEntity s JOIN s.benoetigteToys t WHERE t.toyId = :toyId")
long countStrafeUsage(@Param("toyId") UUID toyId);
@Query("SELECT COUNT(sp) FROM SperreEntity sp JOIN sp.benoetigteToys t WHERE t.toyId = :toyId")
long countSperreUsage(@Param("toyId") UUID toyId);
}

View File

@@ -0,0 +1,48 @@
package de.oaa.xxx.config;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("jwt".equals(cookie.getName())) {
try {
Claims claims = jwtService.validateAndGetClaims(cookie.getValue());
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, Collections.emptyList()
);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
// Ungültiger oder abgelaufener Token ohne Authentifizierung weiter
}
break;
}
}
}
chain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,49 @@
package de.oaa.xxx.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
@Service
public class JwtService {
private static final long EXPIRATION_MS = 24L * 60 * 60 * 1000; // 24 Stunden
private final PrivateKey privateKey;
private final PublicKey publicKey;
public JwtService(
@Value("${jwt.keystore.path}") Resource keystoreResource,
@Value("${jwt.keystore.password}") String password,
@Value("${jwt.keystore.alias}") String alias) throws Exception {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(keystoreResource.getInputStream(), password.toCharArray());
this.privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
this.publicKey = keyStore.getCertificate(alias).getPublicKey();
}
public String generateToken(String email, String name) {
return Jwts.builder()
.subject(email)
.claim("name", name)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(privateKey)
.compact();
}
public Claims validateAndGetClaims(String token) {
return Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}

View File

@@ -8,14 +8,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public SecurityConfig() {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
@@ -23,9 +26,16 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) ->
response.sendRedirect("/login.html")))
.authorizeHttpRequests(auth -> auth
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/userhome.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/toys.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/aufgaben.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/entdecken.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll()
@@ -40,7 +50,8 @@ public class SecurityConfig {
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/filler")).permitAll()
.anyRequest().authenticated()
);
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@@ -1,10 +1,11 @@
package de.oaa.xxx.user;
import java.util.Optional;
import java.util.UUID;
import de.oaa.xxx.config.JwtService;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -12,7 +13,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/login")
@@ -21,23 +25,43 @@ public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
private final UserRepository userRepository;
private final JwtService jwtService;
public LoginController(UserRepository userRepository) {
public LoginController(UserRepository userRepository, JwtService jwtService) {
this.userRepository = userRepository;
this.jwtService = jwtService;
}
@GetMapping
public ResponseEntity<User> login(@RequestParam String email, @RequestParam String hash,
HttpServletRequest req) {
HttpServletResponse response) {
Optional<UserEntity> user = userRepository.findByEmailAndPassword(email, hash);
if (user.isPresent()) {
LOGGER.info("User erfolgreich angemeldet: {}", email);
String token = jwtService.generateToken(user.get().getEmail(), user.get().getName());
ResponseCookie cookie = ResponseCookie.from("jwt", token)
.httpOnly(true)
.sameSite("Strict")
.path("/")
.maxAge(Duration.ofHours(24))
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(user.get().toUser());
} else {
return ResponseEntity.noContent().build();
}
}
@GetMapping("/me")
public ResponseEntity<User> me(Principal principal) {
if (principal == null) {
return ResponseEntity.status(401).build();
}
return userRepository.findByEmail(principal.getName())
.map(entity -> ResponseEntity.ok(entity.toUser()))
.orElse(ResponseEntity.status(401).build());
}
@GetMapping("/{userId}")
public ResponseEntity<User> get(@PathVariable UUID userId) {
return userRepository.findById(userId)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entdecken XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
body { display: block; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
/* ── Sidebar ── */
.sidebar {
width: 240px; background: var(--color-card);
border-right: 1px solid var(--color-secondary);
display: flex; flex-direction: column;
position: fixed; top: 0; left: 0;
height: 100vh; overflow-y: auto;
z-index: 100; transition: transform 0.25s ease;
}
.sidebar-brand {
color: var(--color-primary); font-size: 1.1rem; font-weight: 700;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid var(--color-secondary); flex-shrink: 0;
}
.sidebar ul { list-style: none; padding: 0.5rem 0; }
.sidebar ul li a {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.7rem 1.25rem; color: var(--color-text);
text-decoration: none; font-size: 0.95rem;
border-left: 3px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sidebar ul li a:hover, .sidebar ul li a.active {
background: var(--color-secondary); color: var(--color-primary);
border-left-color: var(--color-primary);
}
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
/* ── Main ── */
.main { margin-left: 240px; flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
/* ── Topbar ── */
.topbar {
display: flex; align-items: center; gap: 1rem;
padding: 0.9rem 1.5rem; background: var(--color-card);
border-bottom: 1px solid var(--color-secondary);
position: sticky; top: 0; z-index: 50;
}
.topbar h1 { font-size: 1.2rem; font-weight: 600; }
.burger {
display: none; background: none; border: none; cursor: pointer;
color: var(--color-text); padding: 0.25rem 0.4rem;
border-radius: 4px; transition: background 0.15s; flex-shrink: 0;
}
.burger:hover { background: var(--color-secondary); }
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
.burger-icon span {
display: block; height: 2px; background: var(--color-text);
border-radius: 2px; transition: transform 0.25s, opacity 0.25s;
}
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.content { padding: 2rem 1.5rem; flex: 1; }
.overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.55); z-index: 90;
}
/* ── Search ── */
.search-bar {
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
}
.search-bar input[type="text"] {
flex: 1; padding: 0.55rem 0.85rem;
border: 1px solid var(--color-secondary); border-radius: 6px;
background: var(--color-card); color: var(--color-text);
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
}
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
.btn-search {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.55rem 1rem;
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
.btn-search:hover { background: var(--color-primary); color: #fff; }
/* ── Paging ── */
.paging {
display: flex; align-items: center; justify-content: center;
gap: 0.75rem; margin-top: 1rem;
}
.paging button {
background: var(--color-secondary); color: var(--color-text);
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
}
.paging button:hover:not(:disabled) { background: var(--color-primary); }
.paging button:disabled { opacity: 0.35; cursor: default; }
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
/* ── Gruppe card ── */
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
.gruppe-card {
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
}
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
.gruppe-header {
display: flex; align-items: center; gap: 0.9rem;
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
}
.gruppe-img {
width: 48px; height: 48px; border-radius: 7px;
object-fit: cover; flex-shrink: 0;
}
.gruppe-img-placeholder {
width: 48px; height: 48px; border-radius: 7px;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
}
.gruppe-meta { flex: 1; min-width: 0; }
.gruppe-name {
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
.gruppe-badge {
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-muted);
}
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
/* ── Subscribe button ── */
.btn-sub {
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
flex-shrink: 0; white-space: nowrap;
}
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
.btn-sub.subscribed {
border-color: rgba(46,204,113,0.5); color: var(--color-success);
}
.btn-sub.subscribed:hover {
border-color: var(--color-primary); color: var(--color-primary);
background: rgba(233,69,96,0.08);
}
.btn-sub:disabled { opacity: 0.4; cursor: default; }
/* ── Gruppe body ── */
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
.sub-section + .sub-section { margin-top: 0.85rem; }
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
.sub-section-title {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--color-primary);
}
/* ── Items ── */
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
.item-row {
display: flex; align-items: center; justify-content: space-between;
gap: 0.75rem; padding: 0.35rem 0.6rem;
cursor: pointer; user-select: none; transition: background 0.12s;
}
.item-row:hover { background: rgba(255,255,255,0.04); }
.item.open .item-row { background: rgba(233,69,96,0.08); }
.item-text {
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
.badge {
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
}
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
/* ── Item detail ── */
.item-detail {
display: none; padding: 0.5rem 0.6rem 0.6rem;
border-top: 1px solid rgba(255,255,255,0.06);
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
}
.item.open .item-detail { display: block; }
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
.item-detail-chip {
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
background: rgba(255,255,255,0.07); color: var(--color-text);
}
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
/* ── Mobile ── */
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.5); }
.main { margin-left: 0; }
.burger { display: flex; }
.overlay.visible { display: block; }
}
</style>
</head>
<body>
<div class="overlay" id="overlay"></div>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">XXX The Game</div>
<ul>
<li><a href="/userhome.html"><span class="icon"></span> Dashboard</a></li>
<li><a href="#"><span class="icon"></span> Meine Session</a></li>
<li><a href="/aufgaben.html"><span class="icon"></span> Aufgaben</a></li>
<li><a href="/entdecken.html" class="active"><span class="icon"></span> Entdecken</a></li>
<li><a href="#"><span class="icon"></span> Strafen</a></li>
<li><a href="/toys.html"><span class="icon"></span> Toys</a></li>
<li><a href="#"><span class="icon"></span> Favoriten</a></li>
<li><a href="#"><span class="icon"></span> Rangliste</a></li>
<li><a href="#"><span class="icon"></span> Nachrichten</a></li>
<li><a href="#"><span class="icon"></span> Einstellungen</a></li>
<li><a href="#" id="logoutLink"><span class="icon"></span> Abmelden</a></li>
</ul>
</aside>
<div class="main">
<header class="topbar">
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span>
</button>
<h1>Entdecken</h1>
</header>
<div class="content">
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
<button class="btn-search" id="searchBtn">Suchen</button>
</div>
<div id="loading" class="loading">Wird geladen…</div>
<div id="groupList" class="gruppe-list"></div>
<div class="paging" id="paging" style="display:none;">
<button id="prevBtn"> Zurück</button>
<span class="page-info" id="pageInfo"></span>
<button id="nextBtn">Weiter </button>
</div>
</div>
</div>
</div>
<script>
const PAGE_SIZE = 10;
let currentPage = 0, totalPages = 1;
let currentName = '';
// ── XSS ──
function esc(str) {
if (str == null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Auth ──
fetch('/login/me')
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
.then(user => { if (!user) return; loadGroups(); })
.catch(() => { window.location.href = '/login.html'; });
document.getElementById('logoutLink').addEventListener('click', e => {
e.preventDefault();
document.cookie = 'jwt=; Max-Age=0; path=/';
window.location.href = '/login.html';
});
// ── Load ──
function loadGroups() {
document.getElementById('loading').style.display = 'block';
document.getElementById('groupList').innerHTML = '';
document.getElementById('paging').style.display = 'none';
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
.then(r => r.json())
.then(data => {
totalPages = data.totalPages || 1;
renderGroups(data.content || []);
updatePaging(currentPage, totalPages);
document.getElementById('loading').style.display = 'none';
})
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
}
// ── Render ──
const WERKZEUG_LABEL = {
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
};
function werkzeugChips(list) {
if (!list || list.length === 0) return '';
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
}
function toyChips(list) {
if (!list || list.length === 0) return '';
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
}
function formatSek(von, bis) {
if (von != null && bis != null) return `${von}${bis} s`;
if (von != null) return `ab ${von} s`;
if (bis != null) return `bis ${bis} s`;
return '';
}
function formatMin(von, bis) {
if (von != null && bis != null) return `${von}${bis} min`;
if (von != null) return `ab ${von} min`;
if (bis != null) return `bis ${bis} min`;
return '';
}
// Track which group card is open
let openGroupId = null;
// Track which item detail is open
let openItemId = null;
function renderGroups(groups) {
const list = document.getElementById('groupList');
if (!groups || groups.length === 0) {
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
return;
}
list.innerHTML = groups.map(g => {
const aufgabenCount = (g.aufgaben || []).length;
const strafeCount = (g.strafen || []).length;
const sperreCount = (g.sperren || []).length;
const counts = [
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
].filter(Boolean).join(' · ');
const subLabel = g.subscribed
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
: '';
const subCount = g.subscriberCount > 0
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
: '';
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
return `
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
<div class="gruppe-header">
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
onclick="toggleGroup('${esc(g.gruppenId)}')">
${g.bild
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
: `<div class="gruppe-img-placeholder">⊙</div>`}
<div class="gruppe-meta">
<div class="gruppe-name">${esc(g.name)}</div>
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
</div>
<span class="gruppe-toggle">▶</span>
</div>
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
${subBtnText}
</button>
</div>
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
</div>
</div>`;
}).join('');
openItemId = null;
}
function renderSubSection(title, items, renderFn) {
return `<div class="sub-section">
<div class="sub-section-header">
<span class="sub-section-title">${esc(title)} (${items.length})</span>
</div>
${items.length === 0
? '<div class="sub-empty">Keine Einträge</div>'
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
</div>`;
}
function renderAufgabe(a) {
const badges = [];
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
const detailRows = [];
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
<span class="item-text">${esc(a.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
function renderStrafe(s) {
const badges = [];
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
const detailRows = [];
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(s.strafeId)}">
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
<span class="item-text">${esc(s.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
function renderZeitstrafe(z) {
const badges = [];
const zeit = formatMin(z.minutenVon, z.minutenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
const detailRows = [];
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
return `<div class="item" id="ditem-${esc(z.sperreId)}">
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
<span class="item-text">${esc(z.kurzText)}</span>
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
</div>
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
</div>`;
}
// ── Sort ──
function sortByLevelThenName(items) {
return items.slice().sort((a, b) => {
const la = a.level ?? 999, lb = b.level ?? 999;
if (la !== lb) return la - lb;
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
});
}
function sortByName(items) {
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
}
// ── Group toggle ──
function toggleGroup(gruppenId) {
const card = document.getElementById('dgroup-' + gruppenId);
const body = document.getElementById('dbody-' + gruppenId);
if (!card) return;
if (card.classList.contains('open')) {
card.classList.remove('open');
body.style.display = 'none';
if (openGroupId === gruppenId) openGroupId = null;
} else {
if (openGroupId) {
const prev = document.getElementById('dgroup-' + openGroupId);
const prevBody = document.getElementById('dbody-' + openGroupId);
if (prev) prev.classList.remove('open');
if (prevBody) prevBody.style.display = 'none';
}
card.classList.add('open');
body.style.display = 'block';
openGroupId = gruppenId;
openItemId = null;
}
}
// ── Item toggle ──
function toggleItem(itemId) {
if (openItemId === itemId) {
const el = document.getElementById('ditem-' + itemId);
if (el) el.classList.remove('open');
openItemId = null;
return;
}
if (openItemId) {
const prev = document.getElementById('ditem-' + openItemId);
if (prev) prev.classList.remove('open');
}
const el = document.getElementById('ditem-' + itemId);
if (el) el.classList.add('open');
openItemId = itemId;
}
// ── Subscribe / Unsubscribe ──
function toggleSubscribe(gruppenId, btn) {
btn.disabled = true;
const isSubscribed = btn.classList.contains('subscribed');
const method = isSubscribed ? 'DELETE' : 'POST';
fetch(`/abo/${gruppenId}`, { method })
.then(r => {
if (r.ok || r.status === 201 || r.status === 202) {
if (isSubscribed) {
btn.classList.remove('subscribed');
btn.textContent = '♥ Abonnieren';
updateBadge(gruppenId, false);
} else {
btn.classList.add('subscribed');
btn.textContent = '♥ Abonniert';
updateBadge(gruppenId, true);
}
btn.disabled = false;
} else {
btn.disabled = false;
}
})
.catch(() => { btn.disabled = false; });
}
function updateBadge(gruppenId, subscribed) {
const card = document.getElementById('dgroup-' + gruppenId);
if (!card) return;
const badgesEl = card.querySelector('.gruppe-badges');
if (!badgesEl) return;
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
if (subscribed && !subBadge) {
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
} else if (!subscribed && subBadge) {
subBadge.remove();
}
}
// ── Search ──
document.getElementById('searchBtn').addEventListener('click', () => {
currentName = document.getElementById('searchInput').value.trim();
currentPage = 0;
loadGroups();
});
document.getElementById('searchInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('searchBtn').click();
});
// ── Paging ──
function updatePaging(current, total) {
const el = document.getElementById('paging');
if (total <= 1) { el.style.display = 'none'; return; }
el.style.display = 'flex';
document.getElementById('prevBtn').disabled = current === 0;
document.getElementById('nextBtn').disabled = current >= total - 1;
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentPage > 0) { currentPage--; loadGroups(); }
});
document.getElementById('nextBtn').addEventListener('click', () => {
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
});
// ── Burger menu ──
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('overlay');
function openMenu() {
sidebar.classList.add('open'); overlay.classList.add('visible');
burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen');
}
function closeMenu() {
sidebar.classList.remove('open'); overlay.classList.remove('visible');
burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen');
}
burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu());
overlay.addEventListener('click', closeMenu);
sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }));
</script>
</body>
</html>

View File

@@ -0,0 +1,780 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
body { display: block; min-height: 100vh; }
/* ── Layout ── */
.layout { display: flex; min-height: 100vh; }
/* ── Sidebar ── */
.sidebar {
width: 240px;
background: var(--color-card);
border-right: 1px solid var(--color-secondary);
display: flex;
flex-direction: column;
position: fixed;
top: 0; left: 0;
height: 100vh;
overflow-y: auto;
z-index: 100;
transition: transform 0.25s ease;
}
.sidebar-brand {
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 700;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid var(--color-secondary);
flex-shrink: 0;
}
.sidebar ul { list-style: none; padding: 0.5rem 0; }
.sidebar ul li a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.7rem 1.25rem;
color: var(--color-text);
text-decoration: none;
font-size: 0.95rem;
border-left: 3px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sidebar ul li a:hover,
.sidebar ul li a.active {
background: var(--color-secondary);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
/* ── Main ── */
.main { margin-left: 240px; flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
/* ── Topbar ── */
.topbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.9rem 1.5rem;
background: var(--color-card);
border-bottom: 1px solid var(--color-secondary);
position: sticky;
top: 0; z-index: 50;
}
.topbar h1 { font-size: 1.2rem; font-weight: 600; }
.burger {
display: none;
background: none;
border: none;
cursor: pointer;
color: var(--color-text);
padding: 0.25rem 0.4rem;
border-radius: 4px;
transition: background 0.15s;
flex-shrink: 0;
}
.burger:hover { background: var(--color-secondary); }
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
.burger-icon span {
display: block; height: 2px;
background: var(--color-text);
border-radius: 2px;
transition: transform 0.25s, opacity 0.25s;
}
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* ── Content ── */
.content { padding: 2rem 1.5rem; flex: 1; }
/* ── Overlay ── */
.overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.55);
z-index: 90;
}
/* ── Section ── */
.section + .section { margin-top: 2.5rem; }
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-primary);
margin: 0;
}
.btn-add {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.4rem 0.85rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.btn-add:hover { background: #c73652; }
/* ── Toy grid ── */
.toy-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 0.85rem;
}
/* ── Toy card ── */
.toy-card {
display: flex;
align-items: center;
gap: 0.85rem;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 0.8rem 0.9rem;
transition: border-color 0.15s;
position: relative;
}
.toy-card { cursor: pointer; }
.toy-card:hover { border-color: var(--color-primary); }
.toy-card.selected {
border-color: var(--color-primary);
background: rgba(233,69,96,0.06);
}
.toy-img {
width: 52px; height: 52px;
border-radius: 7px;
object-fit: cover;
flex-shrink: 0;
}
.toy-img-placeholder {
width: 52px; height: 52px;
border-radius: 7px;
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
flex-shrink: 0;
color: var(--color-muted);
}
.toy-info { flex: 1; min-width: 0; }
.toy-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toy-desc {
font-size: 0.78rem;
color: var(--color-muted);
margin-top: 0.2rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ── Section action buttons ── */
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
.btn-action {
background: var(--color-secondary);
color: var(--color-text);
border: none;
border-radius: 6px;
padding: 0.4rem 0.85rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s, opacity 0.15s;
}
.btn-action:disabled { opacity: 0.35; cursor: default; }
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
.action-error {
font-size: 0.82rem;
color: var(--color-primary);
min-height: 1.1em;
margin-bottom: 0.4rem;
}
/* ── Paging ── */
.paging {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 1rem;
}
.paging button {
background: var(--color-secondary);
color: var(--color-text);
border: none;
border-radius: 6px;
padding: 0.4rem 0.9rem;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.15s;
}
.paging button:hover:not(:disabled) { background: var(--color-primary); }
.paging button:disabled { opacity: 0.35; cursor: default; }
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
/* ── Empty / Loading ── */
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
/* ── Inline-Fehler im Grid ── */
.grid-error {
font-size: 0.85rem;
color: var(--color-primary);
padding: 0.5rem 0;
}
/* ── Modal ── */
.modal-backdrop {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-backdrop.open { display: flex; }
.modal {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 420px;
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
}
.modal h2 {
color: var(--color-primary);
font-size: 1.1rem;
margin-bottom: 1.25rem;
}
.modal label {
display: block;
font-size: 0.8rem;
color: #aaa;
margin-top: 1rem;
margin-bottom: 0.3rem;
}
.modal input[type="text"],
.modal textarea {
width: 100%;
padding: 0.6rem 0.85rem;
border: 1px solid var(--color-secondary);
border-radius: 6px;
background: var(--color-secondary);
color: var(--color-text);
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
resize: vertical;
}
.modal input[type="text"]:focus,
.modal textarea:focus { border-color: var(--color-primary); }
.modal input[type="file"] {
font-size: 0.85rem;
color: var(--color-muted);
margin-top: 0.25rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
.modal-actions .btn-cancel {
background: var(--color-secondary);
color: var(--color-text);
border: none;
border-radius: 6px;
padding: 0.55rem 1.1rem;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s;
}
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
.modal-actions .btn-save {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.55rem 1.1rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.modal-actions .btn-save:hover { background: #c73652; }
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
.modal-error {
color: var(--color-primary);
font-size: 0.82rem;
margin-top: 0.75rem;
display: none;
}
/* ── Mobile ── */
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.5); }
.main { margin-left: 0; }
.burger { display: flex; }
.overlay.visible { display: block; }
.toy-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="overlay" id="overlay"></div>
<!-- Erstell-/Bearbeitungs-Modal -->
<div class="modal-backdrop" id="createModal">
<div class="modal">
<h2 id="modalTitle">Neues Toy</h2>
<label for="toyName">Name *</label>
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
<label for="toyDesc">Beschreibung</label>
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
<label>Bild (optional)</label>
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild neues Bild wählen zum Ersetzen</span>
</div>
<input type="file" id="toyBild" accept="image/*">
<div class="modal-error" id="modalError"></div>
<div class="modal-actions">
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
<button class="btn-save" id="saveBtn">Speichern</button>
</div>
</div>
</div>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">XXX The Game</div>
<ul>
<li><a href="/userhome.html"><span class="icon"></span> Dashboard</a></li>
<li><a href="#"><span class="icon"></span> Meine Session</a></li>
<li><a href="/aufgaben.html"><span class="icon"></span> Aufgaben</a></li>
<li><a href="#"><span class="icon"></span> Strafen</a></li>
<li><a href="/toys.html" class="active"><span class="icon"></span> Toys</a></li>
<li><a href="#"><span class="icon"></span> Favoriten</a></li>
<li><a href="#"><span class="icon"></span> Rangliste</a></li>
<li><a href="#"><span class="icon"></span> Nachrichten</a></li>
<li><a href="#"><span class="icon"></span> Einstellungen</a></li>
<li><a href="#" id="logoutLink"><span class="icon"></span> Abmelden</a></li>
</ul>
</aside>
<div class="main">
<header class="topbar">
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span>
</button>
<h1>Toys</h1>
</header>
<div class="content">
<!-- Meine Toys -->
<div class="section">
<div class="section-header">
<h2 class="section-title">Meine Toys</h2>
<div class="section-actions">
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
<button class="btn-add" id="openCreateBtn">+ Neu</button>
</div>
</div>
<div class="action-error" id="actionError"></div>
<div id="userLoading" class="loading">Wird geladen…</div>
<div class="toy-grid" id="userGrid"></div>
<div class="paging" id="userPaging" style="display:none;">
<button id="userPrev"> Zurück</button>
<span class="page-info" id="userPageInfo"></span>
<button id="userNext">Weiter </button>
</div>
</div>
<!-- System-Toys -->
<div class="section">
<div class="section-header">
<h2 class="section-title">System-Toys</h2>
<div class="section-actions">
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
</div>
</div>
<div class="action-error" id="systemActionError"></div>
<div id="systemLoading" class="loading">Wird geladen…</div>
<div class="toy-grid" id="systemGrid"></div>
<div class="paging" id="systemPaging" style="display:none;">
<button id="systemPrev"> Zurück</button>
<span class="page-info" id="systemPageInfo"></span>
<button id="systemNext">Weiter </button>
</div>
</div>
</div>
</div>
</div>
<script>
const PAGE_SIZE = 12;
let userPage = 0, userTotalPages = 1;
let systemPage = 0, systemTotalPages = 1;
// ── Auth + initial load ──
fetch('/login/me')
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
.then(user => { if (!user) return; loadUserToys(); loadSystemToys(); })
.catch(() => { window.location.href = '/login.html'; });
// ── Load user toys ──
function loadUserToys() {
resetSelection();
document.getElementById('userLoading').style.display = 'block';
fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`)
.then(r => r.json())
.then(data => {
userTotalPages = data.totalPages || 1;
renderGrid('userGrid', data.content, 'selectToy');
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
document.getElementById('userLoading').style.display = 'none';
})
.catch(() => { document.getElementById('userLoading').textContent = 'Fehler beim Laden.'; });
}
// ── Load system toys ──
function loadSystemToys() {
resetSystemSelection();
document.getElementById('systemLoading').style.display = 'block';
fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`)
.then(r => r.json())
.then(data => {
systemTotalPages = data.totalPages || 1;
renderGrid('systemGrid', data.content, 'selectSystemToy');
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
document.getElementById('systemLoading').style.display = 'none';
})
.catch(() => { document.getElementById('systemLoading').textContent = 'Fehler beim Laden.'; });
}
// ── Selection ──
let selectedUserToyId = null;
function selectToy(toyId) {
const prev = document.querySelector('#userGrid .toy-card.selected');
if (prev) prev.classList.remove('selected');
if (selectedUserToyId === toyId) {
selectedUserToyId = null;
} else {
selectedUserToyId = toyId;
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
}
const has = selectedUserToyId != null;
document.getElementById('editBtn').disabled = !has;
document.getElementById('deleteBtn').disabled = !has;
document.getElementById('actionError').textContent = '';
}
function resetSelection() {
selectedUserToyId = null;
document.getElementById('editBtn').disabled = true;
document.getElementById('deleteBtn').disabled = true;
document.getElementById('actionError').textContent = '';
}
// ── System-Toy selection ──
let selectedSystemToyId = null;
function selectSystemToy(toyId) {
const prev = document.querySelector('#systemGrid .toy-card.selected');
if (prev) prev.classList.remove('selected');
if (selectedSystemToyId === toyId) {
selectedSystemToyId = null;
} else {
selectedSystemToyId = toyId;
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
}
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
document.getElementById('systemActionError').textContent = '';
}
function resetSystemSelection() {
selectedSystemToyId = null;
document.getElementById('copyBtn').disabled = true;
document.getElementById('systemActionError').textContent = '';
}
// ── Copy system toy ──
document.getElementById('copyBtn').addEventListener('click', () => {
if (!selectedSystemToyId) return;
const btn = document.getElementById('copyBtn');
btn.disabled = true;
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
.then(r => {
if (r.ok || r.status === 201) {
userPage = 0;
loadUserToys();
document.getElementById('systemActionError').textContent = '';
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
document.getElementById('systemActionError').textContent =
'Du hast bereits ein Toy mit diesem Namen.';
btn.disabled = false;
} else {
document.getElementById('systemActionError').textContent =
'Fehler beim Kopieren (HTTP ' + r.status + ').';
btn.disabled = false;
}
})
.catch(() => {
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
btn.disabled = false;
});
});
// ── Render a grid ──
function renderGrid(gridId, toys, selectFn) {
const grid = document.getElementById(gridId);
if (!toys || toys.length === 0) {
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
return;
}
grid.innerHTML = toys.map(toy => `
<div class="toy-card" data-id="${esc(toy.toyId)}"
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
${toy.bild
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
: `<div class="toy-img-placeholder">◈</div>`}
<div class="toy-info">
<div class="toy-name">${esc(toy.name)}</div>
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
</div>
</div>
`).join('');
}
// ── Update paging controls ──
function updatePaging(pagingId, prevId, nextId, infoId, current, total) {
const el = document.getElementById(pagingId);
if (total <= 1) { el.style.display = 'none'; return; }
el.style.display = 'flex';
document.getElementById(prevId).disabled = current === 0;
document.getElementById(nextId).disabled = current >= total - 1;
document.getElementById(infoId).textContent = `Seite ${current + 1} von ${total}`;
}
// ── Paging button handlers ──
document.getElementById('userPrev').addEventListener('click', () => { if (userPage > 0) { userPage--; loadUserToys(); } });
document.getElementById('userNext').addEventListener('click', () => { if (userPage < userTotalPages - 1) { userPage++; loadUserToys(); } });
document.getElementById('systemPrev').addEventListener('click', () => { if (systemPage > 0) { systemPage--; loadSystemToys(); } });
document.getElementById('systemNext').addEventListener('click', () => { if (systemPage < systemTotalPages - 1) { systemPage++; loadSystemToys(); } });
// ── Header action buttons ──
document.getElementById('editBtn').addEventListener('click', () => {
if (selectedUserToyId) openModal(selectedUserToyId);
});
document.getElementById('deleteBtn').addEventListener('click', () => {
if (!selectedUserToyId) return;
if (!confirm('Toy wirklich löschen?')) return;
const btn = document.getElementById('deleteBtn');
btn.disabled = true;
const toyId = selectedUserToyId;
fetch(`/toy/${toyId}`, { method: 'DELETE' })
.then(r => {
if (r.status === 409) {
showActionError('Wird in Aufgaben verwendet nicht löschbar.');
btn.disabled = false;
} else if (r.status === 403) {
showActionError('Keine Berechtigung.');
btn.disabled = false;
} else if (r.ok || r.status === 202) {
userPage = 0;
loadUserToys();
} else {
showActionError('Fehler beim Löschen.');
btn.disabled = false;
}
})
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
});
function showActionError(msg) {
const el = document.getElementById('actionError');
el.textContent = msg;
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
}
// ── Create / Edit modal ──
const modal = document.getElementById('createModal');
const saveBtn = document.getElementById('saveBtn');
let currentEditId = null;
function openModal(editId) {
currentEditId = editId || null;
document.getElementById('modalError').style.display = 'none';
document.getElementById('toyBild').value = '';
if (currentEditId) {
fetch(`/toy/${currentEditId}`)
.then(r => r.ok ? r.json() : null)
.then(toy => {
if (!toy) return;
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
document.getElementById('toyName').value = toy.name || '';
document.getElementById('toyDesc').value = toy.beschreibung || '';
const imgWrap = document.getElementById('currentImageWrap');
if (toy.bild) {
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
imgWrap.style.display = 'flex';
} else {
imgWrap.style.display = 'none';
}
modal.classList.add('open');
document.getElementById('toyName').focus();
})
.catch(() => alert('Fehler beim Laden des Toys.'));
} else {
document.getElementById('modalTitle').textContent = 'Neues Toy';
document.getElementById('toyName').value = '';
document.getElementById('toyDesc').value = '';
document.getElementById('currentImageWrap').style.display = 'none';
modal.classList.add('open');
document.getElementById('toyName').focus();
}
}
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
document.getElementById('cancelBtn').addEventListener('click', closeModal);
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
function closeModal() { modal.classList.remove('open'); }
function editToy(toyId) { openModal(toyId); }
saveBtn.addEventListener('click', async () => {
const name = document.getElementById('toyName').value.trim();
if (!name) {
showModalError('Bitte einen Namen eingeben.');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = 'Speichert…';
let bildBase64 = null;
const fileInput = document.getElementById('toyBild');
if (fileInput.files.length > 0) {
bildBase64 = await toBase64(fileInput.files[0]);
}
const payload = {
name,
beschreibung: document.getElementById('toyDesc').value.trim() || null,
bild: bildBase64
};
const isEdit = currentEditId != null;
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => {
if (r.ok || r.status === 201) {
closeModal();
userPage = 0;
loadUserToys();
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
showModalError('Ein Toy mit diesem Namen existiert bereits.');
} else {
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
}
})
.catch(() => showModalError('Verbindungsfehler.'))
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
});
function showModalError(msg) {
const el = document.getElementById('modalError');
el.textContent = msg;
el.style.display = 'block';
}
function toBase64(file) {
const MAX = 128;
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.naturalWidth, h = img.naturalHeight;
if (w > MAX || h > MAX) {
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/png').split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
}
// ── XSS-Schutz ──
function esc(str) {
if (str == null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Burger menu ──
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('overlay');
function openMenu() {
sidebar.classList.add('open'); overlay.classList.add('visible');
burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen');
}
function closeMenu() {
sidebar.classList.remove('open'); overlay.classList.remove('visible');
burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen');
}
burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu());
overlay.addEventListener('click', closeMenu);
sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }));
</script>
</body>
</html>

View File

@@ -6,19 +6,274 @@
<title>XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
body {
display: block;
min-height: 100vh;
}
/* ── Layout ── */
.layout {
display: flex;
min-height: 100vh;
}
/* ── Sidebar ── */
.sidebar {
width: 240px;
background: var(--color-card);
border-right: 1px solid var(--color-secondary);
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
z-index: 100;
transition: transform 0.25s ease;
}
.sidebar-brand {
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 700;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid var(--color-secondary);
flex-shrink: 0;
}
.sidebar ul {
list-style: none;
padding: 0.5rem 0;
}
.sidebar ul li a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.7rem 1.25rem;
color: var(--color-text);
text-decoration: none;
font-size: 0.95rem;
border-left: 3px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sidebar ul li a:hover,
.sidebar ul li a.active {
background: var(--color-secondary);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
.sidebar ul li a .icon {
font-size: 1rem;
width: 1.2rem;
text-align: center;
flex-shrink: 0;
}
/* ── Main ── */
.main {
margin-left: 240px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ── Topbar ── */
.topbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.9rem 1.5rem;
background: var(--color-card);
border-bottom: 1px solid var(--color-secondary);
position: sticky;
top: 0;
z-index: 50;
}
.topbar h1 {
font-size: 1.2rem;
font-weight: 600;
}
.burger {
display: none;
background: none;
border: none;
cursor: pointer;
color: var(--color-text);
padding: 0.25rem 0.4rem;
border-radius: 4px;
transition: background 0.15s;
flex-shrink: 0;
}
.burger:hover {
background: var(--color-secondary);
}
.burger-icon {
display: flex;
flex-direction: column;
gap: 5px;
width: 22px;
}
.burger-icon span {
display: block;
height: 2px;
background: var(--color-text);
border-radius: 2px;
transition: transform 0.25s, opacity 0.25s;
}
.burger.open .burger-icon span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.burger.open .burger-icon span:nth-child(2) {
opacity: 0;
}
.burger.open .burger-icon span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* ── Content ── */
.content {
padding: 2rem 1.5rem;
flex: 1;
}
/* ── Overlay ── */
.overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 90;
}
/* ── Mobile ── */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
}
.main {
margin-left: 0;
}
.burger {
display: flex;
}
.overlay.visible {
display: block;
}
}
</style>
</head>
<body>
<h1>XXX The Game</h1>
<p id="greeting"></p>
<div class="overlay" id="overlay"></div>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">XXX The Game</div>
<ul>
<li><a href="#" class="active"><span class="icon"></span> Dashboard</a></li>
<li><a href="#"><span class="icon"></span> Meine Session</a></li>
<li><a href="/aufgaben.html"><span class="icon"></span> Aufgaben</a></li>
<li><a href="/entdecken.html"><span class="icon"></span> Entdecken</a></li>
<li><a href="#"><span class="icon"></span> Strafen</a></li>
<li><a href="/toys.html"><span class="icon"></span> Toys</a></li>
<li><a href="#"><span class="icon"></span> Favoriten</a></li>
<li><a href="#"><span class="icon"></span> Rangliste</a></li>
<li><a href="#"><span class="icon"></span> Nachrichten</a></li>
<li><a href="#"><span class="icon"></span> Einstellungen</a></li>
<li><a href="#" id="logoutLink"><span class="icon"></span> Abmelden</a></li>
</ul>
</aside>
<div class="main">
<header class="topbar">
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon">
<span></span>
<span></span>
<span></span>
</span>
</button>
<h1>XXX The Game</h1>
</header>
<div class="content">
<p id="greeting"></p>
</div>
</div>
</div>
<script>
const userJson = sessionStorage.getItem('user');
if (!userJson) {
window.location.href = '/login.html';
} else {
const user = JSON.parse(userJson);
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
// ── Auth check ──
fetch('/login/me')
.then(response => {
if (response.status === 401) {
window.location.href = '/login.html';
return null;
}
return response.json();
})
.then(user => {
if (user) {
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
}
})
.catch(() => {
window.location.href = '/login.html';
});
// ── Burger menu ──
const sidebar = document.getElementById('sidebar');
const burgerBtn = document.getElementById('burgerBtn');
const overlay = document.getElementById('overlay');
function openMenu() {
sidebar.classList.add('open');
overlay.classList.add('visible');
burgerBtn.classList.add('open');
burgerBtn.setAttribute('aria-label', 'Menü schließen');
}
function closeMenu() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
burgerBtn.classList.remove('open');
burgerBtn.setAttribute('aria-label', 'Menü öffnen');
}
burgerBtn.addEventListener('click', () => {
sidebar.classList.contains('open') ? closeMenu() : openMenu();
});
overlay.addEventListener('click', closeMenu);
// Close on nav link click (mobile)
sidebar.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 768) closeMenu();
});
});
</script>
</body>
</html>