Weiter an der Konfig Maske für das BDSM Game gearbeitet, Refactoring der Controller Klassen
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package de.oaa.xxx.aufgaben;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
|
||||
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.FinisherRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service für komplexe AufgabenGruppen-Operationen.
|
||||
* Kapselt die Kopier-Logik (Systemgruppe → eigene Gruppe) inkl. Toy-Mapping.
|
||||
*/
|
||||
@Service
|
||||
public class AufgabenGruppeService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeService.class);
|
||||
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final FinisherRepository finisherRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public AufgabenGruppeService(AufgabenGruppeRepository gruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
FinisherRepository finisherRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.finisherRepository = finisherRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopiert eine öffentliche (System-)Gruppe in die eigene Sammlung des Users.
|
||||
*
|
||||
* @param sourceId UUID der Quellgruppe
|
||||
* @param userId UUID des Ziel-Users
|
||||
* @return UUID der neu erstellten Gruppe
|
||||
* @throws IllegalStateException wenn das Gruppen-Limit (10) erreicht ist
|
||||
* @throws IllegalArgumentException wenn die Quellgruppe nicht gefunden oder nicht kopierbar ist
|
||||
*/
|
||||
@Transactional
|
||||
public UUID copyGruppe(UUID sourceId, UUID userId) {
|
||||
if (gruppeRepository.countByUserId(userId) >= 10) {
|
||||
throw new IllegalStateException("Gruppen-Limit erreicht");
|
||||
}
|
||||
|
||||
AufgabenGruppeEntity source = gruppeRepository.findById(sourceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Gruppe nicht gefunden: " + sourceId));
|
||||
if (source.isPrivateGruppe()) {
|
||||
throw new IllegalArgumentException("Privat-Gruppen können nicht kopiert werden");
|
||||
}
|
||||
if (userId.equals(source.getUserId())) {
|
||||
throw new IllegalArgumentException("Eigene Gruppen können nicht kopiert werden");
|
||||
}
|
||||
|
||||
// Toy-Mapping aufbauen: Source-ToyId → Ziel-ToyEntity
|
||||
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()); });
|
||||
source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.getBenoetigteToys()); });
|
||||
|
||||
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
|
||||
for (ToyEntity sourceToy : allSourceToys) {
|
||||
if (sourceToy.getUserId() == null) {
|
||||
// System-Toy: direkt referenzieren
|
||||
toyMapping.put(sourceToy.getToyId(), sourceToy);
|
||||
} else {
|
||||
// User-Toy: gleichnamiges Toy suchen oder kopieren
|
||||
ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), userId)
|
||||
.orElseGet(() -> {
|
||||
ToyEntity tc = new ToyEntity();
|
||||
tc.setToyId(UUID.randomUUID());
|
||||
tc.setName(sourceToy.getName());
|
||||
tc.setBeschreibung(sourceToy.getBeschreibung());
|
||||
tc.setBild(sourceToy.getBild());
|
||||
tc.setUserId(userId);
|
||||
return toyRepository.save(tc);
|
||||
});
|
||||
toyMapping.put(sourceToy.getToyId(), mapped);
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Gruppe anlegen
|
||||
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(userId);
|
||||
copy.setPrivateGruppe(true);
|
||||
gruppeRepository.save(copy);
|
||||
|
||||
// Aufgaben kopieren
|
||||
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);
|
||||
}
|
||||
|
||||
// Strafen kopieren
|
||||
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);
|
||||
}
|
||||
|
||||
// Sperren kopieren
|
||||
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);
|
||||
}
|
||||
|
||||
// Finisher kopieren
|
||||
for (FinisherEntity f : source.getFinisher()) {
|
||||
FinisherEntity fc = new FinisherEntity();
|
||||
fc.setFinisherId(UUID.randomUUID());
|
||||
fc.setAufgabenGruppe(copy);
|
||||
fc.setKurzText(f.getKurzText());
|
||||
fc.setText(f.getText());
|
||||
fc.setGeschlecht(f.getGeschlecht());
|
||||
fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null);
|
||||
fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null);
|
||||
fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping));
|
||||
finisherRepository.save(fc);
|
||||
}
|
||||
|
||||
LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", userId, copy.getGruppenId(), sourceId);
|
||||
return copy.getGruppenId();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,9 @@
|
||||
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.FinisherEntity;
|
||||
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.FinisherRepository;
|
||||
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 java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -37,15 +23,19 @@ 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;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppe;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppeList;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppePage;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppeService;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
|
||||
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.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/gruppe")
|
||||
@@ -62,7 +52,7 @@ public class AufgabenGruppeController {
|
||||
private final FinisherRepository finisherRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
private final AufgabenGruppeService aufgabenGruppeService;
|
||||
|
||||
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
@@ -71,7 +61,7 @@ public class AufgabenGruppeController {
|
||||
FinisherRepository finisherRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository,
|
||||
ToyRepository toyRepository) {
|
||||
AufgabenGruppeService aufgabenGruppeService) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
@@ -79,7 +69,7 @@ public class AufgabenGruppeController {
|
||||
this.finisherRepository = finisherRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
this.aufgabenGruppeService = aufgabenGruppeService;
|
||||
}
|
||||
|
||||
// ── Paginierte Listen ──
|
||||
@@ -190,118 +180,16 @@ public class AufgabenGruppeController {
|
||||
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) {
|
||||
try {
|
||||
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
|
||||
return ResponseEntity.status(201).build();
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.status(409).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build();
|
||||
return ResponseEntity.status(403).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()); });
|
||||
source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.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);
|
||||
}
|
||||
|
||||
for (FinisherEntity f : source.getFinisher()) {
|
||||
FinisherEntity fc = new FinisherEntity();
|
||||
fc.setFinisherId(UUID.randomUUID());
|
||||
fc.setAufgabenGruppe(copy);
|
||||
fc.setKurzText(f.getKurzText());
|
||||
fc.setText(f.getText());
|
||||
fc.setGeschlecht(f.getGeschlecht());
|
||||
fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null);
|
||||
fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null);
|
||||
fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping));
|
||||
finisherRepository.save(fc);
|
||||
}
|
||||
|
||||
LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", user.getUserId(), copy.getGruppenId(), gruppeId);
|
||||
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 ──
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@@ -49,6 +51,9 @@ public class SecurityConfig {
|
||||
.requestMatchers("/sessionbdsmtasks.html").authenticated()
|
||||
.requestMatchers("/sessionbdsmtoys.html").authenticated()
|
||||
.requestMatchers("/sessionbdsmingame.html").authenticated()
|
||||
.requestMatchers("/neubdsm.html").authenticated()
|
||||
.requestMatchers("/bdsmingame.html").authenticated()
|
||||
.requestMatchers("/bdsmwarten.html").authenticated()
|
||||
.requestMatchers("/personen-suchen.html").authenticated()
|
||||
.requestMatchers("/freunde.html").authenticated()
|
||||
.requestMatchers("/nachrichten.html").authenticated()
|
||||
@@ -79,6 +84,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/*.svg").permitAll()
|
||||
.requestMatchers("/*.webp").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/login").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/login").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/login/publickey").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/login/logout").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/user").permitAll()
|
||||
@@ -96,4 +102,9 @@ public class SecurityConfig {
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package de.oaa.xxx.games.bdsm;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameRole;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
/**
|
||||
* Service für komplexe BDSM-Game-Operationen.
|
||||
* Kapselt Spielabschluss-Logik (XP-Vergabe, History) und den BDSM→Chastity-Übergang.
|
||||
*/
|
||||
@Service
|
||||
public class BdsmGameService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameService.class);
|
||||
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
|
||||
public BdsmGameService(BdsmGameRepository sessionRepository,
|
||||
MitspielerRepository mitspielerRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository,
|
||||
UserRepository userRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
CardlockRepository cardlockRepository,
|
||||
SystemMessageService systemMessageService) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beendet eine BDSM-Session ordentlich: History speichern, XP vergeben,
|
||||
* Gäste auf eigenem Gerät benachrichtigen, Daten aufräumen.
|
||||
*/
|
||||
@Transactional
|
||||
public void spielAbschliessen(BdsmGameEntity entity) {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
});
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
}));
|
||||
|
||||
// Gäste auf eigenem Gerät benachrichtigen
|
||||
String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉";
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.isEigenesGeraet() && m.getUserId() != null)
|
||||
.forEach(m -> systemMessageService.send(entity.getUserId(), m.getUserId(),
|
||||
endNachricht, "/userhome.html", MessageCause.GAME_STATE));
|
||||
|
||||
bereinige(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Überführt eine BDSM-Session in ein neues Chastity-Lock (BDSM→Chastity-Transition).
|
||||
* History + XP werden wie beim normalen Spielabschluss vergeben.
|
||||
*
|
||||
* @return Das neu angelegte CardLockEntity
|
||||
* @throws IllegalArgumentException wenn Session oder Template nicht gefunden
|
||||
* @throws IllegalStateException wenn Lockee bereits ein aktives Lock hat
|
||||
*/
|
||||
@Transactional
|
||||
public CardLockEntity zuChastity(UUID sessionId, UUID templateLockId, UUID lockeeUserId, UUID keyholderUserId) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Session nicht gefunden: " + sessionId));
|
||||
|
||||
CardLockEntity template = cardlockRepository.findById(templateLockId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Template-Lock nicht gefunden: " + templateLockId));
|
||||
|
||||
if (lockeeUserId != null
|
||||
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeUserId)) {
|
||||
throw new IllegalStateException("Lockee hat bereits ein aktives Chastity-Lock");
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CardLockEntity newLock = new CardLockEntity();
|
||||
newLock.setName(template.getName());
|
||||
newLock.setLockee(lockeeUserId);
|
||||
newLock.setKeyholder(keyholderUserId);
|
||||
newLock.setInitialCards(template.getInitialCards());
|
||||
newLock.setPickEveryMinute(template.getPickEveryMinute());
|
||||
newLock.setAccumulatePicks(template.isAccumulatePicks());
|
||||
newLock.setShowRemainingCards(template.isShowRemainingCards());
|
||||
newLock.setLatestOpeningtime(template.getLatestOpeningtime());
|
||||
newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
|
||||
newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
|
||||
newLock.setTasks(template.getTasks());
|
||||
newLock.setRequiresVerification(template.isRequiresVerification());
|
||||
newLock.setTestLock(false);
|
||||
newLock.setTaskCardMode(template.getTaskCardMode());
|
||||
|
||||
int codeLines = template.getUnlockCodeLines() != null ? template.getUnlockCodeLines() : 5;
|
||||
newLock.setUnlockCodeLines(codeLines);
|
||||
StringBuilder codeBuilder = new StringBuilder();
|
||||
java.util.Random rng = new java.util.Random();
|
||||
for (int i = 0; i < codeLines; i++) codeBuilder.append(rng.nextInt(10));
|
||||
newLock.setUnlockCode(codeBuilder.toString());
|
||||
|
||||
newLock.setStartTime(now);
|
||||
newLock.setAvailableCards(template.getInitialCards() != null
|
||||
? new ArrayList<>(template.getInitialCards()) : new ArrayList<>());
|
||||
newLock.setOpenPicks(0);
|
||||
if (template.getPickEveryMinute() != null) {
|
||||
newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute()));
|
||||
}
|
||||
if (template.getHygineOpeningEveryMinites() != null) {
|
||||
newLock.setLastHygineOpening(now);
|
||||
}
|
||||
cardlockRepository.save(newLock);
|
||||
|
||||
// Lockee benachrichtigen
|
||||
if (lockeeUserId != null) {
|
||||
userRepository.findById(keyholderUserId).ifPresent(keyholder ->
|
||||
systemMessageService.send(keyholderUserId, lockeeUserId,
|
||||
keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.",
|
||||
"/activelock.html", MessageCause.GAME_STATE));
|
||||
}
|
||||
|
||||
// Spielabschluss-Logik (History + XP + Cleanup)
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
});
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
}));
|
||||
|
||||
bereinige(entity);
|
||||
|
||||
LOGGER.info("BDSM-Session {} in Chastity-Lock {} überführt (Lockee: {}, Keyholder: {})",
|
||||
sessionId, newLock.getLockId(), lockeeUserId, keyholderUserId);
|
||||
return newLock;
|
||||
}
|
||||
|
||||
/** Löscht alle Session-Daten (Sperren, Mitspieler, Session selbst). */
|
||||
private void bereinige(BdsmGameEntity entity) {
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,8 @@ public class BdsmEinladungController {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
if (req.setupId() == null) return ResponseEntity.badRequest().build();
|
||||
|
||||
// Prüfen ob Person bereits aktiv eingeladen oder Teil des Spiels
|
||||
boolean alreadyInvited = einladungRepository.findBySetupId(req.setupId()).stream()
|
||||
.anyMatch(e -> req.inviteeId().equals(e.getInviteeId())
|
||||
@@ -113,6 +115,10 @@ public class BdsmEinladungController {
|
||||
if (e == null) return ResponseEntity.notFound().build();
|
||||
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
|
||||
e.setStatus(Status.CANCELLED);
|
||||
String inviterName = userRepository.findById(userId).map(u -> u.getName()).orElse("Jemand");
|
||||
systemMessageService.send(userId, e.getInviteeId(),
|
||||
inviterName + " hat die BDSM-Spieleinladung zurückgezogen.",
|
||||
"/einladungen.html", MessageCause.INVITATION);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@@ -168,7 +174,7 @@ public class BdsmEinladungController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
|
||||
public ResponseEntity<Map<String, Object>> getById(@PathVariable("id") UUID id, Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
package de.oaa.xxx.games.bdsm.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
|
||||
import de.oaa.xxx.games.bdsm.Mitspieler;
|
||||
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
|
||||
import de.oaa.xxx.games.bdsm.Werkzeug;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGame;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
|
||||
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
|
||||
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameRole;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -45,52 +28,71 @@ 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.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGame;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGameService;
|
||||
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
|
||||
import de.oaa.xxx.games.bdsm.Mitspieler;
|
||||
import de.oaa.xxx.games.bdsm.Werkzeug;
|
||||
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bdsm")
|
||||
@Transactional
|
||||
public class BdsmGameController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
|
||||
/** Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen beendet wurden. */
|
||||
private static final Set<UUID> ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>());
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
|
||||
/**
|
||||
* Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen
|
||||
* beendet wurden.
|
||||
*/
|
||||
private static final Set<UUID> ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final BdsmEinladungRepository einladungRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final BdsmEinladungRepository einladungRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final BdsmGameService bdsmGameService;
|
||||
|
||||
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
|
||||
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository,
|
||||
ObjectMapper objectMapper, SystemMessageService systemMessageService,
|
||||
CardlockRepository cardlockRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.einladungRepository = einladungRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
}
|
||||
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
|
||||
BdsmEinladungRepository einladungRepository, ObjectMapper objectMapper,
|
||||
SystemMessageService systemMessageService, CardlockRepository cardlockRepository,
|
||||
BdsmGameService bdsmGameService) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.einladungRepository = einladungRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.bdsmGameService = bdsmGameService;
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}")
|
||||
public ResponseEntity<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
|
||||
@@ -159,46 +161,8 @@ public class BdsmGameController {
|
||||
public ResponseEntity<Void> spielAbgeschlossen(@PathVariable UUID sessionId) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
// BDSM-XP für alle Teilnehmer gutschreiben (Minuten = XP)
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
});
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
}));
|
||||
|
||||
// Eigene-Gerät-Gäste über ordentliches Spielende benachrichtigen
|
||||
ORDENTLICH_BEENDET.add(sessionId);
|
||||
String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉";
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.isEigenesGeraet() && m.getUserId() != null)
|
||||
.forEach(m -> systemMessageService.send(entity.getUserId(), m.getUserId(),
|
||||
endNachricht, "/userhome.html", MessageCause.GAME_STATE));
|
||||
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
bdsmGameService.spielAbschliessen(entity);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@@ -547,89 +511,20 @@ public class BdsmGameController {
|
||||
@PostMapping("/{sessionId}/zu-chastity")
|
||||
public ResponseEntity<Map<String, Object>> zuChastity(
|
||||
@PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
|
||||
CardLockEntity template = cardlockRepository.findById(req.lockId()).orElse(null);
|
||||
if (template == null) return ResponseEntity.badRequest().build();
|
||||
|
||||
// Lockee darf kein aktives Chastity-Lock haben
|
||||
if (req.lockeeUserId() != null
|
||||
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(req.lockeeUserId())) {
|
||||
try {
|
||||
CardLockEntity newLock = bdsmGameService.zuChastity(
|
||||
sessionId, req.lockId(), req.lockeeUserId(), req.keyholderUserId());
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("lockId", newLock.getLockId().toString());
|
||||
response.put("unlockCode", newLock.getUnlockCode());
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.contains("Session")) return ResponseEntity.notFound().build();
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
// Neues Lock mit Template-Einstellungen für den BDSM-Lockee erstellen
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CardLockEntity newLock = new CardLockEntity();
|
||||
newLock.setName(template.getName());
|
||||
newLock.setLockee(req.lockeeUserId());
|
||||
newLock.setKeyholder(req.keyholderUserId());
|
||||
newLock.setInitialCards(template.getInitialCards());
|
||||
newLock.setPickEveryMinute(template.getPickEveryMinute());
|
||||
newLock.setAccumulatePicks(template.isAccumulatePicks());
|
||||
newLock.setShowRemainingCards(template.isShowRemainingCards());
|
||||
newLock.setLatestOpeningtime(template.getLatestOpeningtime());
|
||||
newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
|
||||
newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
|
||||
newLock.setTasks(template.getTasks());
|
||||
newLock.setRequiresVerification(template.isRequiresVerification());
|
||||
newLock.setTestLock(false);
|
||||
newLock.setTaskCardMode(template.getTaskCardMode());
|
||||
int codeLines = template.getUnlockCodeLines() != null ? template.getUnlockCodeLines() : 5;
|
||||
newLock.setUnlockCodeLines(codeLines);
|
||||
StringBuilder codeBuilder = new StringBuilder();
|
||||
java.util.Random rng = new java.util.Random();
|
||||
for (int i = 0; i < codeLines; i++) codeBuilder.append(rng.nextInt(10));
|
||||
newLock.setUnlockCode(codeBuilder.toString());
|
||||
newLock.setStartTime(now);
|
||||
newLock.setAvailableCards(template.getInitialCards() != null
|
||||
? new java.util.ArrayList<>(template.getInitialCards()) : new java.util.ArrayList<>());
|
||||
newLock.setOpenPicks(0);
|
||||
if (template.getPickEveryMinute() != null) {
|
||||
newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute()));
|
||||
}
|
||||
if (template.getHygineOpeningEveryMinites() != null) {
|
||||
newLock.setLastHygineOpening(now);
|
||||
}
|
||||
cardlockRepository.save(newLock);
|
||||
|
||||
// Lockee benachrichtigen (falls UserAccount vorhanden)
|
||||
if (req.lockeeUserId() != null) {
|
||||
userRepository.findById(req.keyholderUserId()).ifPresent(keyholder ->
|
||||
systemMessageService.send(req.keyholderUserId(), req.lockeeUserId(),
|
||||
keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.",
|
||||
"/activelock.html", MessageCause.GAME_STATE));
|
||||
}
|
||||
|
||||
// Spielabschluss-Logik (wie spielAbgeschlossen, aber ohne eigenen Delete)
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); });
|
||||
entity.getMitspieler().stream().filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); }));
|
||||
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("lockId", newLock.getLockId().toString());
|
||||
response.put("unlockCode", newLock.getUnlockCode());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */
|
||||
|
||||
@@ -33,10 +33,15 @@ public class BdsmSetupDraftController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getDraft(Principal principal) {
|
||||
public ResponseEntity<Map<String, Object>> getDraft(
|
||||
@RequestParam(required = false) String setupId,
|
||||
Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
return draftRepository.findByUserId(userId)
|
||||
var lookup = (setupId != null && !setupId.isBlank())
|
||||
? draftRepository.findBySetupId(setupId)
|
||||
: draftRepository.findByUserId(userId);
|
||||
return lookup
|
||||
.map(d -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("setupId", d.getSetupId());
|
||||
|
||||
@@ -8,4 +8,5 @@ import java.util.UUID;
|
||||
|
||||
public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> {
|
||||
Optional<BdsmSetupDraftEntity> findByUserId(UUID userId);
|
||||
Optional<BdsmSetupDraftEntity> findBySetupId(String setupId);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Factory für CardLockService-Instanzen.
|
||||
*
|
||||
* CardLockService hält pro Instanz den Zustand eines konkreten CardLockEntity
|
||||
* und kann daher kein Singleton-Bean sein. Diese Factory zentralisiert die
|
||||
* Erzeugung und verwaltet alle Abhängigkeiten als injizierte Singletons.
|
||||
*/
|
||||
@Service
|
||||
public class CardLockServiceFactory {
|
||||
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final CardLockRepository cardLockRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public CardLockServiceFactory(VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository,
|
||||
CardLockRepository cardLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.cardLockRepository = cardLockRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
|
||||
*/
|
||||
public CardLockService create(CardLockEntity lock) {
|
||||
return new CardLockService(
|
||||
lock,
|
||||
verificationRepository,
|
||||
verificationVoteRepository,
|
||||
cardLockRepository,
|
||||
gameHistoryRepository,
|
||||
userRepository
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
public record PasswordResetConfirm(String token, String passwordHash) {}
|
||||
public record PasswordResetConfirm(String token, String password) {}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
@@ -25,15 +26,18 @@ public class PasswordResetController {
|
||||
private final UserRepository userRepository;
|
||||
private final MailService mailService;
|
||||
private final MailTemplateService mailTemplateService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public PasswordResetController(PasswordResetRepository passwordResetRepository,
|
||||
UserRepository userRepository,
|
||||
MailService mailService,
|
||||
MailTemplateService mailTemplateService) {
|
||||
MailTemplateService mailTemplateService,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.mailService = mailService;
|
||||
this.mailTemplateService = mailTemplateService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@PostMapping("/request")
|
||||
@@ -67,7 +71,7 @@ public class PasswordResetController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> {
|
||||
user.setPassword(confirm.passwordHash());
|
||||
user.setPassword(passwordEncoder.encode(confirm.password()));
|
||||
userRepository.save(user);
|
||||
LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail());
|
||||
});
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
package de.oaa.xxx.registration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.user.UserController;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/activation")
|
||||
public class ActivationController {
|
||||
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final UserController userController;
|
||||
|
||||
public ActivationController(RegistrationRepository registrationRepository, UserController userController) {
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.userController = userController;
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
public ResponseEntity<Void> activate(@PathVariable String uuid) {
|
||||
RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null);
|
||||
if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) {
|
||||
ResponseEntity<Void> response = userController.userAnlegen(registration.toRegistration());
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
registration.setActivated(Boolean.TRUE);
|
||||
registrationRepository.save(registration);
|
||||
String redirect = "/login.html?email=" + java.net.URLEncoder.encode(registration.getEmail(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
return ResponseEntity.status(302).location(URI.create(redirect)).build();
|
||||
} else {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
} else {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
package de.oaa.xxx.registration;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/activation")
|
||||
public class ActivationController {
|
||||
|
||||
private final RegistrationService registrationService;
|
||||
|
||||
public ActivationController(RegistrationService registrationService) {
|
||||
this.registrationService = registrationService;
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
public ResponseEntity<Void> activate(@PathVariable String uuid) {
|
||||
try {
|
||||
String email = registrationService.activate(uuid);
|
||||
String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8);
|
||||
return ResponseEntity.status(302).location(URI.create(redirect)).build();
|
||||
} catch (IllegalStateException e) {
|
||||
// Bereits aktiviert → trotzdem zum Login weiterleiten (idempotent)
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public class Registration {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String email;
|
||||
private String passwordHash;
|
||||
private String password;
|
||||
private LocalDate geburtsdatum;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,7 @@ import de.oaa.xxx.mail.Email;
|
||||
import de.oaa.xxx.mail.MailService;
|
||||
import de.oaa.xxx.mail.MailTemplateService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
@@ -30,13 +31,16 @@ public class RegistrationController {
|
||||
private final UserRepository userRepository;
|
||||
private final MailService mailService;
|
||||
private final MailTemplateService mailTemplateService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public RegistrationController(RegistrationRepository registrationRepository, UserRepository userRepository,
|
||||
MailService mailService, MailTemplateService mailTemplateService) {
|
||||
MailService mailService, MailTemplateService mailTemplateService,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.mailService = mailService;
|
||||
this.mailTemplateService = mailTemplateService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -57,6 +61,8 @@ public class RegistrationController {
|
||||
LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName());
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
// Passwort serverseitig mit BCrypt hashen
|
||||
registration.setPassword(passwordEncoder.encode(registration.getPassword()));
|
||||
RegistrationEntity entity = RegistrationEntity.create(registration);
|
||||
registrationRepository.save(entity);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class RegistrationEntity {
|
||||
registration.setId(registrationId);
|
||||
registration.setEmail(email);
|
||||
registration.setName(name);
|
||||
registration.setPasswordHash(password);
|
||||
registration.setPassword(password);
|
||||
registration.setGeburtsdatum(geburtsdatum);
|
||||
return registration;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class RegistrationEntity {
|
||||
entity.setEmail(registration.getEmail());
|
||||
entity.setActivated(Boolean.FALSE);
|
||||
entity.setName(registration.getName());
|
||||
entity.setPassword(registration.getPasswordHash());
|
||||
entity.setPassword(registration.getPassword());
|
||||
entity.setGeburtsdatum(registration.getGeburtsdatum());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.oaa.xxx.registration;
|
||||
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Koordiniert den Aktivierungsflow: liest die RegistrationEntity aus der DB
|
||||
* und delegiert die User-Anlage an den UserService.
|
||||
* Ersetzt den direkten Controller→Controller-Aufruf im ActivationController.
|
||||
*/
|
||||
@Service
|
||||
public class RegistrationService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationService.class);
|
||||
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public RegistrationService(RegistrationRepository registrationRepository, UserService userService) {
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert eine Registrierung: legt den User an und markiert die Registration als aktiviert.
|
||||
*
|
||||
* @return E-Mail des aktivierten Users (für Redirect im Controller)
|
||||
* @throws IllegalArgumentException wenn UUID ungültig oder Registration nicht gefunden
|
||||
* @throws IllegalStateException wenn Registration bereits aktiviert
|
||||
*/
|
||||
public String activate(String uuid) {
|
||||
UUID registrationId;
|
||||
try {
|
||||
registrationId = UUID.fromString(uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Ungültige UUID: " + uuid);
|
||||
}
|
||||
|
||||
RegistrationEntity registration = registrationRepository.findById(registrationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + uuid));
|
||||
|
||||
if (Boolean.TRUE.equals(registration.getActivated())) {
|
||||
throw new IllegalStateException("Registration bereits aktiviert");
|
||||
}
|
||||
|
||||
userService.createUser(registration.toRegistration());
|
||||
|
||||
registration.setActivated(Boolean.TRUE);
|
||||
registrationRepository.save(registration);
|
||||
|
||||
LOGGER.info("Registration {} aktiviert, User {} angelegt", uuid, registration.getEmail());
|
||||
return registration.getEmail();
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,88 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
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;
|
||||
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.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/login")
|
||||
public class LoginController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public LoginController(UserRepository userRepository, JwtService jwtService) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<User> login(@RequestParam String email, @RequestParam String hash,
|
||||
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("/logout")
|
||||
public void logout(HttpServletResponse response) throws java.io.IOException {
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> get(@PathVariable UUID userId) {
|
||||
return userRepository.findById(userId)
|
||||
.map(entity -> ResponseEntity.ok(entity.toUser()))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
}
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
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.security.crypto.password.PasswordEncoder;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/login")
|
||||
public class LoginController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
|
||||
|
||||
record LoginRequest(String email, String password) {}
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public LoginController(UserRepository userRepository, JwtService jwtService, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtService = jwtService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> login(@RequestBody LoginRequest request, HttpServletResponse response) {
|
||||
var userOpt = userRepository.findByEmail(request.email());
|
||||
if (userOpt.isPresent() && passwordEncoder.matches(request.password(), userOpt.get().getPassword())) {
|
||||
UserEntity user = userOpt.get();
|
||||
LOGGER.info("User erfolgreich angemeldet: {}", request.email());
|
||||
String token = jwtService.generateToken(user.getEmail(), user.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.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("/logout")
|
||||
public void logout(HttpServletResponse response) throws java.io.IOException {
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> get(@PathVariable UUID userId) {
|
||||
return userRepository.findById(userId)
|
||||
.map(entity -> ResponseEntity.ok(entity.toUser()))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,443 +1,287 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.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 de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
|
||||
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.emailchange.EmailChangeRepository;
|
||||
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.passwordreset.PasswordResetRepository;
|
||||
import de.oaa.xxx.registration.Registration;
|
||||
import de.oaa.xxx.registration.RegistrationRepository;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
|
||||
import de.oaa.xxx.social.repository.KommentarLikeRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
private final FavoritRepository favoritRepository;
|
||||
private final GruppenAboRepository gruppenAboRepository;
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final EmailChangeRepository emailChangeRepository;
|
||||
private final PasswordResetRepository passwordResetRepository;
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
private final ProfileImageLikeRepository profileImageLikeRepository;
|
||||
private final PinnwandEintragRepository pinnwandEintragRepository;
|
||||
private final PinnwandLikeRepository pinnwandLikeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final KommentarLikeRepository kommentarLikeRepository;
|
||||
private final NotificationPreferenceRepository notificationPreferenceRepository;
|
||||
private final BdsmDefaultsRepository bdsmDefaultsRepository;
|
||||
|
||||
public UserController(UserRepository userRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
ToyRepository toyRepository,
|
||||
FavoritRepository favoritRepository,
|
||||
GruppenAboRepository gruppenAboRepository,
|
||||
BdsmGameRepository sessionRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository,
|
||||
MitspielerRepository mitspielerRepository,
|
||||
EmailChangeRepository emailChangeRepository,
|
||||
PasswordResetRepository passwordResetRepository,
|
||||
ProfileImageRepository profileImageRepository,
|
||||
ProfileImageLikeRepository profileImageLikeRepository,
|
||||
PinnwandEintragRepository pinnwandEintragRepository,
|
||||
PinnwandLikeRepository pinnwandLikeRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
KommentarLikeRepository kommentarLikeRepository,
|
||||
NotificationPreferenceRepository notificationPreferenceRepository,
|
||||
BdsmDefaultsRepository bdsmDefaultsRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
this.favoritRepository = favoritRepository;
|
||||
this.gruppenAboRepository = gruppenAboRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.emailChangeRepository = emailChangeRepository;
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.profileImageRepository = profileImageRepository;
|
||||
this.profileImageLikeRepository = profileImageLikeRepository;
|
||||
this.pinnwandEintragRepository = pinnwandEintragRepository;
|
||||
this.pinnwandLikeRepository = pinnwandLikeRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.kommentarLikeRepository = kommentarLikeRepository;
|
||||
this.notificationPreferenceRepository = notificationPreferenceRepository;
|
||||
this.bdsmDefaultsRepository = bdsmDefaultsRepository;
|
||||
}
|
||||
|
||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||
record NameChangeRequest(String name) {}
|
||||
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
|
||||
record ProfileRequest(Integer groesse, Integer gewicht,
|
||||
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
|
||||
record PrivacyRequest(
|
||||
Sichtbarkeit sichtbarkeitGrunddaten,
|
||||
Sichtbarkeit sichtbarkeitGalerie,
|
||||
Sichtbarkeit sichtbarkeitFreunde,
|
||||
Sichtbarkeit sichtbarkeitFeed,
|
||||
Sichtbarkeit sichtbarkeitPinnwand,
|
||||
Sichtbarkeit sichtbarkeitXp,
|
||||
Sichtbarkeit sichtbarkeitLockhistorie) {}
|
||||
|
||||
@PutMapping("/me/picture")
|
||||
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setProfilePicture(request.picture());
|
||||
user.get().setProfilePictureHq(request.pictureHq());
|
||||
userRepository.save(user.get());
|
||||
LOGGER.debug("User {} hat Profilbild aktualisiert", user.get().getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/profile")
|
||||
public ResponseEntity<Void> updateProfile(@RequestBody ProfileRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
if (request.beschreibung() != null && request.beschreibung().length() > 600) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
user.setGroesse(request.groesse());
|
||||
user.setGewicht(request.gewicht());
|
||||
user.setGeschlecht(request.geschlecht());
|
||||
user.setNeigung(request.neigung());
|
||||
user.setBeziehungsstatus(request.beziehungsstatus());
|
||||
user.setBeschreibung(request.beschreibung());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Profil aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/privacy")
|
||||
public ResponseEntity<Void> updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten());
|
||||
if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie());
|
||||
if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde());
|
||||
if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed());
|
||||
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
|
||||
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
|
||||
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
record NotificationPreferenceRequest(boolean inApp, boolean email) {}
|
||||
|
||||
@GetMapping("/me/notifications")
|
||||
public ResponseEntity<Map<String, Object>> getNotifications(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
|
||||
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (MessageCause cause : MessageCause.values()) {
|
||||
NotificationPreferenceEntity pref = byKey.getOrDefault(
|
||||
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("inApp", pref.isInApp());
|
||||
entry.put("email", pref.isEmail());
|
||||
result.put(cause.name(), entry);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/me/notifications")
|
||||
public ResponseEntity<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
for (var entry : request.entrySet()) {
|
||||
MessageCause cause;
|
||||
try {
|
||||
cause = MessageCause.valueOf(entry.getKey());
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
NotificationPreferenceEntity pref = notificationPreferenceRepository
|
||||
.findByUserIdAndCause(userId, cause)
|
||||
.orElseGet(() -> {
|
||||
NotificationPreferenceEntity n = new NotificationPreferenceEntity();
|
||||
n.setUserId(userId);
|
||||
n.setCause(cause);
|
||||
return n;
|
||||
});
|
||||
pref.setInApp(entry.getValue().inApp());
|
||||
pref.setEmail(entry.getValue().email());
|
||||
notificationPreferenceRepository.save(pref);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
record BdsmDefaultsRequest(List<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
|
||||
|
||||
@GetMapping("/me/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaults(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElse(new BdsmDefaultsEntity());
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
|
||||
result.put("rollen", splitOrEmpty(d.getRollen()));
|
||||
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable UUID userId) {
|
||||
var userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
UserEntity user = userOpt.get();
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElse(new BdsmDefaultsEntity());
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null);
|
||||
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
|
||||
result.put("rollen", splitOrEmpty(d.getRollen()));
|
||||
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/me/bdsm-defaults")
|
||||
public ResponseEntity<Void> updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; });
|
||||
d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit()));
|
||||
d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen()));
|
||||
d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge()));
|
||||
bdsmDefaultsRepository.save(d);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static List<String> splitOrEmpty(String s) {
|
||||
if (s == null || s.isBlank()) return List.of();
|
||||
return List.of(s.split(","));
|
||||
}
|
||||
|
||||
@PutMapping("/me/geburtsdatum")
|
||||
public ResponseEntity<Void> updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) {
|
||||
if (request.geburtsdatum() == null
|
||||
|| Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) {
|
||||
return ResponseEntity.status(422).build();
|
||||
}
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
user.setGeburtsdatum(request.geburtsdatum());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/name")
|
||||
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
|
||||
String newName = request.name();
|
||||
if (userRepository.findByName(newName).isPresent()
|
||||
|| registrationRepository.findByName(newName).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setName(newName);
|
||||
userRepository.save(user.get());
|
||||
LOGGER.info("User {} hat Namen zu '{}' geändert", user.get().getUserId(), newName);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/me")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteAccount(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
UUID userId = user.getUserId();
|
||||
String email = user.getEmail();
|
||||
|
||||
LOGGER.info("Lösche Konto für User {}", email);
|
||||
|
||||
// 1. Delete user's AufgabenGruppen and all their content
|
||||
var gruppen = aufgabenGruppeRepository.findByUserId(userId);
|
||||
if (!gruppen.isEmpty()) {
|
||||
aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen));
|
||||
for (var gruppe : gruppen) {
|
||||
gruppenAboRepository.deleteByAufgabenGruppe(gruppe);
|
||||
favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId());
|
||||
}
|
||||
aufgabenGruppeRepository.deleteAll(gruppen);
|
||||
}
|
||||
|
||||
// 2. Delete user's Toys (join table refs already cleared above)
|
||||
toyRepository.deleteAll(toyRepository.findByUserId(userId));
|
||||
|
||||
// 3. Delete user's own Favoriten and Gruppenabos (to other groups)
|
||||
favoritRepository.deleteAll(favoritRepository.findByUserId(userId));
|
||||
gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId));
|
||||
|
||||
// 4. Delete Session with Mitspieler and AktiveSperre
|
||||
var sessionOpt = sessionRepository.findByUserId(userId);
|
||||
if (sessionOpt.isPresent()) {
|
||||
var session = sessionOpt.get();
|
||||
List<AktiveSperreEntity> sperren = session.getAktiveSperren();
|
||||
List<MitspielerEntity> mitspieler = session.getMitspieler();
|
||||
aktiveSperreRepository.deleteAll(sperren);
|
||||
mitspielerRepository.deleteAll(mitspieler);
|
||||
sessionRepository.delete(session);
|
||||
}
|
||||
|
||||
// 5. Delete pending tokens
|
||||
emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete);
|
||||
passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete);
|
||||
|
||||
// 5b. Delete profile images and likes
|
||||
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
|
||||
for (var img : profileImages) {
|
||||
profileImageLikeRepository.deleteByImageId(img.getImageId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
profileImageRepository.deleteAll(profileImages);
|
||||
profileImageLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 5c. Delete pinnwand entries (authored by or on user's wall) + their likes/comments
|
||||
var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId);
|
||||
for (var e : ownWallEntries) {
|
||||
pinnwandLikeRepository.deleteByEintragId(e.getEintragId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
pinnwandEintragRepository.deleteAll(ownWallEntries);
|
||||
pinnwandEintragRepository.deleteByAuthorId(userId);
|
||||
pinnwandLikeRepository.deleteByUserId(userId);
|
||||
kommentarRepository.deleteByAuthorId(userId);
|
||||
kommentarLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 6. Delete user
|
||||
userRepository.delete(user);
|
||||
|
||||
// Clear JWT cookie
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, cookie.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> userAnlegen(@RequestBody Registration registration) {
|
||||
if (registration.getEmail() == null || registration.getPasswordHash() == null || registration.getName() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (userRepository.findByEmail(registration.getEmail()).isPresent()) {
|
||||
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
try {
|
||||
UserEntity entity = new UserEntity();
|
||||
entity.setUserId(UUID.randomUUID());
|
||||
entity.setEmail(registration.getEmail());
|
||||
entity.setName(registration.getName());
|
||||
entity.setPassword(registration.getPasswordHash());
|
||||
entity.setGeburtsdatum(registration.getGeburtsdatum());
|
||||
userRepository.save(entity);
|
||||
|
||||
for (MessageCause cause : MessageCause.values()) {
|
||||
notificationPreferenceRepository.save(
|
||||
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.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 de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
|
||||
import de.oaa.xxx.registration.Registration;
|
||||
import de.oaa.xxx.registration.RegistrationRepository;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
|
||||
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final NotificationPreferenceRepository notificationPreferenceRepository;
|
||||
private final BdsmDefaultsRepository bdsmDefaultsRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserRepository userRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
NotificationPreferenceRepository notificationPreferenceRepository,
|
||||
BdsmDefaultsRepository bdsmDefaultsRepository,
|
||||
UserService userService) {
|
||||
this.userRepository = userRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.notificationPreferenceRepository = notificationPreferenceRepository;
|
||||
this.bdsmDefaultsRepository = bdsmDefaultsRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||
record NameChangeRequest(String name) {}
|
||||
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
|
||||
record ProfileRequest(Integer groesse, Integer gewicht,
|
||||
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
|
||||
record PrivacyRequest(
|
||||
Sichtbarkeit sichtbarkeitGrunddaten,
|
||||
Sichtbarkeit sichtbarkeitGalerie,
|
||||
Sichtbarkeit sichtbarkeitFreunde,
|
||||
Sichtbarkeit sichtbarkeitFeed,
|
||||
Sichtbarkeit sichtbarkeitPinnwand,
|
||||
Sichtbarkeit sichtbarkeitXp,
|
||||
Sichtbarkeit sichtbarkeitLockhistorie) {}
|
||||
|
||||
@PutMapping("/me/picture")
|
||||
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setProfilePicture(request.picture());
|
||||
user.get().setProfilePictureHq(request.pictureHq());
|
||||
userRepository.save(user.get());
|
||||
LOGGER.debug("User {} hat Profilbild aktualisiert", user.get().getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/profile")
|
||||
public ResponseEntity<Void> updateProfile(@RequestBody ProfileRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
if (request.beschreibung() != null && request.beschreibung().length() > 600) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
user.setGroesse(request.groesse());
|
||||
user.setGewicht(request.gewicht());
|
||||
user.setGeschlecht(request.geschlecht());
|
||||
user.setNeigung(request.neigung());
|
||||
user.setBeziehungsstatus(request.beziehungsstatus());
|
||||
user.setBeschreibung(request.beschreibung());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Profil aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/privacy")
|
||||
public ResponseEntity<Void> updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten());
|
||||
if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie());
|
||||
if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde());
|
||||
if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed());
|
||||
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
|
||||
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
|
||||
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
record NotificationPreferenceRequest(boolean inApp, boolean email) {}
|
||||
|
||||
@GetMapping("/me/notifications")
|
||||
public ResponseEntity<Map<String, Object>> getNotifications(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
|
||||
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (MessageCause cause : MessageCause.values()) {
|
||||
NotificationPreferenceEntity pref = byKey.getOrDefault(
|
||||
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("inApp", pref.isInApp());
|
||||
entry.put("email", pref.isEmail());
|
||||
result.put(cause.name(), entry);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/me/notifications")
|
||||
public ResponseEntity<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
for (var entry : request.entrySet()) {
|
||||
MessageCause cause;
|
||||
try {
|
||||
cause = MessageCause.valueOf(entry.getKey());
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
NotificationPreferenceEntity pref = notificationPreferenceRepository
|
||||
.findByUserIdAndCause(userId, cause)
|
||||
.orElseGet(() -> {
|
||||
NotificationPreferenceEntity n = new NotificationPreferenceEntity();
|
||||
n.setUserId(userId);
|
||||
n.setCause(cause);
|
||||
return n;
|
||||
});
|
||||
pref.setInApp(entry.getValue().inApp());
|
||||
pref.setEmail(entry.getValue().email());
|
||||
notificationPreferenceRepository.save(pref);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
record BdsmDefaultsRequest(List<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
|
||||
|
||||
@GetMapping("/me/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaults(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElse(new BdsmDefaultsEntity());
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("geschlecht", userOpt.get().getGeschlecht() != null ? userOpt.get().getGeschlecht().name() : null);
|
||||
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
|
||||
result.put("rollen", splitOrEmpty(d.getRollen()));
|
||||
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable UUID userId) {
|
||||
var userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
UserEntity user = userOpt.get();
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElse(new BdsmDefaultsEntity());
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null);
|
||||
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
|
||||
result.put("rollen", splitOrEmpty(d.getRollen()));
|
||||
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/me/bdsm-defaults")
|
||||
public ResponseEntity<Void> updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; });
|
||||
d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit()));
|
||||
d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen()));
|
||||
d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge()));
|
||||
bdsmDefaultsRepository.save(d);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static List<String> splitOrEmpty(String s) {
|
||||
if (s == null || s.isBlank()) return List.of();
|
||||
return List.of(s.split(","));
|
||||
}
|
||||
|
||||
@PutMapping("/me/geburtsdatum")
|
||||
public ResponseEntity<Void> updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) {
|
||||
if (request.geburtsdatum() == null
|
||||
|| Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) {
|
||||
return ResponseEntity.status(422).build();
|
||||
}
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
user.setGeburtsdatum(request.geburtsdatum());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/name")
|
||||
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
|
||||
String newName = request.name();
|
||||
if (userRepository.findByName(newName).isPresent()
|
||||
|| registrationRepository.findByName(newName).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setName(newName);
|
||||
userRepository.save(user.get());
|
||||
LOGGER.info("User {} hat Namen zu '{}' geändert", user.get().getUserId(), newName);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/me")
|
||||
public ResponseEntity<Void> deleteAccount(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = userOpt.get().getUserId();
|
||||
String email = userOpt.get().getEmail();
|
||||
|
||||
userService.deleteAccount(userId, email);
|
||||
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, cookie.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> userAnlegen(@RequestBody Registration registration) {
|
||||
try {
|
||||
userService.createUser(registration);
|
||||
return ResponseEntity.status(201).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.status(409).build();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.util.UUID;
|
||||
|
||||
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
|
||||
|
||||
Optional<UserEntity> findByEmailAndPassword(String email, String password);
|
||||
Optional<UserEntity> findByEmail(String email);
|
||||
Optional<UserEntity> findByName(String name);
|
||||
List<UserEntity> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
210
xxxthegame/src/main/java/de/oaa/xxx/user/UserService.java
Normal file
210
xxxthegame/src/main/java/de/oaa/xxx/user/UserService.java
Normal file
@@ -0,0 +1,210 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
|
||||
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.emailchange.EmailChangeRepository;
|
||||
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.passwordreset.PasswordResetRepository;
|
||||
import de.oaa.xxx.registration.Registration;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
|
||||
import de.oaa.xxx.social.repository.KommentarLikeRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
private final FavoritRepository favoritRepository;
|
||||
private final GruppenAboRepository gruppenAboRepository;
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final EmailChangeRepository emailChangeRepository;
|
||||
private final PasswordResetRepository passwordResetRepository;
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
private final ProfileImageLikeRepository profileImageLikeRepository;
|
||||
private final PinnwandEintragRepository pinnwandEintragRepository;
|
||||
private final PinnwandLikeRepository pinnwandLikeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final KommentarLikeRepository kommentarLikeRepository;
|
||||
private final NotificationPreferenceRepository notificationPreferenceRepository;
|
||||
|
||||
public UserService(UserRepository userRepository,
|
||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
ToyRepository toyRepository,
|
||||
FavoritRepository favoritRepository,
|
||||
GruppenAboRepository gruppenAboRepository,
|
||||
BdsmGameRepository sessionRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository,
|
||||
MitspielerRepository mitspielerRepository,
|
||||
EmailChangeRepository emailChangeRepository,
|
||||
PasswordResetRepository passwordResetRepository,
|
||||
ProfileImageRepository profileImageRepository,
|
||||
ProfileImageLikeRepository profileImageLikeRepository,
|
||||
PinnwandEintragRepository pinnwandEintragRepository,
|
||||
PinnwandLikeRepository pinnwandLikeRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
KommentarLikeRepository kommentarLikeRepository,
|
||||
NotificationPreferenceRepository notificationPreferenceRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
this.favoritRepository = favoritRepository;
|
||||
this.gruppenAboRepository = gruppenAboRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.emailChangeRepository = emailChangeRepository;
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.profileImageRepository = profileImageRepository;
|
||||
this.profileImageLikeRepository = profileImageLikeRepository;
|
||||
this.pinnwandEintragRepository = pinnwandEintragRepository;
|
||||
this.pinnwandLikeRepository = pinnwandLikeRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.kommentarLikeRepository = kommentarLikeRepository;
|
||||
this.notificationPreferenceRepository = notificationPreferenceRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht einen User-Account vollständig inklusive aller abhängigen Daten.
|
||||
* Gibt die gelöschte E-Mail zurück (wird für Cookie-Clearing im Controller benötigt).
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteAccount(UUID userId, String email) {
|
||||
var user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden: " + userId));
|
||||
|
||||
LOGGER.info("Lösche Konto für User {}", email);
|
||||
|
||||
// 1. AufgabenGruppen und deren Inhalte löschen
|
||||
var gruppen = aufgabenGruppeRepository.findByUserId(userId);
|
||||
if (!gruppen.isEmpty()) {
|
||||
aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen));
|
||||
for (var gruppe : gruppen) {
|
||||
gruppenAboRepository.deleteByAufgabenGruppe(gruppe);
|
||||
favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId());
|
||||
}
|
||||
aufgabenGruppeRepository.deleteAll(gruppen);
|
||||
}
|
||||
|
||||
// 2. Toys löschen
|
||||
toyRepository.deleteAll(toyRepository.findByUserId(userId));
|
||||
|
||||
// 3. Eigene Favoriten und Gruppenabos löschen
|
||||
favoritRepository.deleteAll(favoritRepository.findByUserId(userId));
|
||||
gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId));
|
||||
|
||||
// 4. BDSM-Session mit Mitspieler und AktiveSperre löschen
|
||||
var sessionOpt = sessionRepository.findByUserId(userId);
|
||||
if (sessionOpt.isPresent()) {
|
||||
var session = sessionOpt.get();
|
||||
List<AktiveSperreEntity> sperren = session.getAktiveSperren();
|
||||
List<MitspielerEntity> mitspieler = session.getMitspieler();
|
||||
aktiveSperreRepository.deleteAll(sperren);
|
||||
mitspielerRepository.deleteAll(mitspieler);
|
||||
sessionRepository.delete(session);
|
||||
}
|
||||
|
||||
// 5. Pending Tokens löschen
|
||||
emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete);
|
||||
passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete);
|
||||
|
||||
// 5b. Profilbilder und Likes löschen
|
||||
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
|
||||
for (var img : profileImages) {
|
||||
profileImageLikeRepository.deleteByImageId(img.getImageId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
profileImageRepository.deleteAll(profileImages);
|
||||
profileImageLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 5c. Pinnwand-Einträge und Likes/Kommentare löschen
|
||||
var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId);
|
||||
for (var e : ownWallEntries) {
|
||||
pinnwandLikeRepository.deleteByEintragId(e.getEintragId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
pinnwandEintragRepository.deleteAll(ownWallEntries);
|
||||
pinnwandEintragRepository.deleteByAuthorId(userId);
|
||||
pinnwandLikeRepository.deleteByUserId(userId);
|
||||
kommentarRepository.deleteByAuthorId(userId);
|
||||
kommentarLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 6. User löschen
|
||||
userRepository.delete(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt einen neuen User aus einer bestätigten Registration an
|
||||
* und erstellt die Standard-Benachrichtigungseinstellungen.
|
||||
*/
|
||||
public void createUser(Registration registration) {
|
||||
if (registration.getEmail() == null || registration.getPassword() == null || registration.getName() == null) {
|
||||
throw new IllegalArgumentException("E-Mail, Passwort und Name sind Pflichtfelder");
|
||||
}
|
||||
if (userRepository.findByEmail(registration.getEmail()).isPresent()) {
|
||||
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
|
||||
throw new IllegalStateException("E-Mail bereits vorhanden");
|
||||
}
|
||||
|
||||
UserEntity entity = new UserEntity();
|
||||
entity.setUserId(UUID.randomUUID());
|
||||
entity.setEmail(registration.getEmail());
|
||||
entity.setName(registration.getName());
|
||||
entity.setPassword(registration.getPassword());
|
||||
entity.setGeburtsdatum(registration.getGeburtsdatum());
|
||||
userRepository.save(entity);
|
||||
|
||||
for (MessageCause cause : MessageCause.values()) {
|
||||
notificationPreferenceRepository.save(
|
||||
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
|
||||
}
|
||||
|
||||
LOGGER.info("User {} angelegt", entity.getUserId());
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@
|
||||
document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.';
|
||||
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
|
||||
} else if (mode === 'OWN_DEVICE') {
|
||||
window.location.replace(`/bdsmwarten.html?id=${einladungId}`);
|
||||
window.location.replace(`/neubdsm.html`);
|
||||
} else {
|
||||
zeigeBestaetigt();
|
||||
}
|
||||
|
||||
@@ -1,357 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Neue Session – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.session-setup { }
|
||||
|
||||
.setup-section { margin-bottom: 2.5rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.setting-row { margin-bottom: 1.25rem; }
|
||||
.setting-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.setting-header label {
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
margin: 0;
|
||||
}
|
||||
.setting-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
min-width: 3.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.modal-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 2rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.modal-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.modal-actions { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.modal-actions button { width: 100%; padding: 0.75rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="modal-overlay" id="modal" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
<div class="modal-text" id="modalText"></div>
|
||||
<div class="modal-actions" id="modalActions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 1 von 4 – Session-Einstellungen</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Session-Einstellungen</h2>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldStrafe">Wahrscheinlichkeit Strafe</label>
|
||||
<span class="setting-value"><span id="valStrafe">15</span> %</span>
|
||||
</div>
|
||||
<input type="range" id="sldStrafe" min="0" max="100" value="15"
|
||||
oninput="document.getElementById('valStrafe').textContent=this.value; updateWarnung()">
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldZeitstrafe">Wahrscheinlichkeit Zeitstrafe</label>
|
||||
<span class="setting-value"><span id="valZeitstrafe">15</span> %</span>
|
||||
</div>
|
||||
<input type="range" id="sldZeitstrafe" min="0" max="100" value="15"
|
||||
oninput="document.getElementById('valZeitstrafe').textContent=this.value; updateWarnung()">
|
||||
</div>
|
||||
|
||||
<div class="message" id="wahrschWarnung" style="display:none; margin-top:0.75rem;"></div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldAufgaben">Aufgaben pro Level</label>
|
||||
<span class="setting-value" id="valAufgaben">5</span>
|
||||
</div>
|
||||
<input type="range" id="sldAufgaben" min="1" max="20" value="5"
|
||||
oninput="document.getElementById('valAufgaben').textContent=this.value">
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldZeit">Zeitfaktor Zeitstrafen</label>
|
||||
<span class="setting-value" id="valZeit">1,0</span>
|
||||
</div>
|
||||
<input type="range" id="sldZeit" min="5" max="20" value="10"
|
||||
oninput="document.getElementById('valZeit').textContent=(this.value/10).toFixed(1).replace('.',',')">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
<button class="full-width" onclick="weiter()">Weiter</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
function updateWarnung() {
|
||||
const strafe = parseInt(document.getElementById('sldStrafe').value);
|
||||
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
|
||||
const summe = strafe + zeitstrafe;
|
||||
const el = document.getElementById('wahrschWarnung');
|
||||
if (summe > 98) {
|
||||
el.textContent = `Kombiniert ${summe} % – Werte über 98 % sind nicht möglich.`;
|
||||
el.className = 'message error';
|
||||
el.style.display = 'block';
|
||||
} else if (summe > 60) {
|
||||
el.textContent = `Hinweis: Bei ${summe} % kombinierten Wahrscheinlichkeiten ist die Chance auf Vanilla-Aufgaben sehr gering.`;
|
||||
el.className = 'message warning';
|
||||
el.style.display = 'block';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function weiter() {
|
||||
hideMessage();
|
||||
const strafe = parseInt(document.getElementById('sldStrafe').value);
|
||||
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
|
||||
if (strafe + zeitstrafe > 98) {
|
||||
showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error');
|
||||
return;
|
||||
}
|
||||
const settings = {
|
||||
wahrscheinlichkeitStrafe: strafe,
|
||||
wahrscheinlichkeitSperre: zeitstrafe,
|
||||
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
|
||||
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
|
||||
};
|
||||
// Immer neue Setup-ID → Mitspieler-Konfig und Einladungen gehören zur neuen Runde
|
||||
const newSetupId = crypto.randomUUID();
|
||||
sessionStorage.setItem('bdsm-setup-id', newSetupId);
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(settings));
|
||||
sessionStorage.removeItem('bdsm-session-setup');
|
||||
sessionStorage.removeItem('bdsm-session-gruppen');
|
||||
sessionStorage.removeItem('bdsm-session-toys');
|
||||
// Alten Draft löschen, neuen mit frischer Setup-ID anlegen
|
||||
try { await fetch('/bdsm/setup-draft', { method: 'DELETE' }); } catch (_) {}
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ setupId: newSetupId, settingsJson: JSON.stringify(settings) }),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmplayers.html';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
el.textContent = text;
|
||||
el.className = `message ${type}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Aktive-Session-Check ──
|
||||
function zeigeModal(title, text, actions) {
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
const textEl = document.getElementById('modalText');
|
||||
textEl.textContent = text;
|
||||
textEl.style.display = text ? '' : 'none';
|
||||
const actEl = document.getElementById('modalActions');
|
||||
actEl.innerHTML = '';
|
||||
actions.forEach(a => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = a.label;
|
||||
btn.className = a.primary ? 'full-width' : 'full-width secondary';
|
||||
btn.onclick = () => a.onClick();
|
||||
actEl.appendChild(btn);
|
||||
});
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function versteckeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}
|
||||
|
||||
const BDSM_STORAGE_KEYS = [
|
||||
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
|
||||
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
|
||||
];
|
||||
|
||||
function sessionFortfahren(sid) {
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
sessionStorage.setItem('bdsm-session-id', sid);
|
||||
window.location.href = '/bdsmingame.html';
|
||||
}
|
||||
|
||||
function sessionBeendenFragen(sid) {
|
||||
zeigeModal(
|
||||
'Session wirklich beenden?',
|
||||
'Die Session und alle aktiven Sperren werden gelöscht.',
|
||||
[
|
||||
{ label: 'Ja, beenden', primary: true, onClick: () => sessionLoeschen(sid) },
|
||||
{ label: 'Nein, fortfahren', onClick: () => sessionFortfahren(sid) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function sessionLoeschen(sid) {
|
||||
versteckeModal();
|
||||
try {
|
||||
await fetch('/bdsm', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: sid }),
|
||||
});
|
||||
} catch (_) { /* ignorieren */ }
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
|
||||
(async function checkAktiveSession() {
|
||||
try {
|
||||
const meRes = await fetch('/login/me');
|
||||
if (!meRes.ok) return;
|
||||
const user = await meRes.json();
|
||||
|
||||
// 1. Prüfen ob User selbst eine aktive Host-Session hat
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sessionRes.ok) {
|
||||
const session = await sessionRes.json();
|
||||
zeigeModal(
|
||||
'Aktive Session vorhanden',
|
||||
'Du hast noch eine laufende Session. Möchtest du fortfahren?',
|
||||
[
|
||||
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
|
||||
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Prüfen ob User als Mitspieler (ACCEPTED_OWN) eingeladen wurde
|
||||
const einladungRes = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (!einladungRes.ok) return;
|
||||
const einladung = await einladungRes.json();
|
||||
|
||||
if (!einladung.sessionId) {
|
||||
// Spiel noch nicht gestartet → Warteseite
|
||||
window.location.replace(`/bdsmwarten.html?id=${einladung.einladungId}`);
|
||||
} else {
|
||||
// Spiel läuft bereits → direkt als Gast rein
|
||||
const mRes = await fetch(`/bdsm/${einladung.sessionId}/mitspieler/me`);
|
||||
if (mRes.ok) {
|
||||
const mData = await mRes.json();
|
||||
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
|
||||
sessionStorage.setItem('bdsm-guest-name', mData.name);
|
||||
}
|
||||
sessionStorage.setItem('bdsm-session-id', einladung.sessionId);
|
||||
sessionStorage.setItem('bdsm-is-guest', 'true');
|
||||
window.location.replace('/bdsmingame.html');
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
})();
|
||||
|
||||
// Gespeicherte Einstellungen wiederherstellen
|
||||
function applySettings(s) {
|
||||
function setSlider(id, displayId, value, transform) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.value = value;
|
||||
document.getElementById(displayId).textContent = transform ? transform(value) : value;
|
||||
}
|
||||
setSlider('sldStrafe', 'valStrafe', s.wahrscheinlichkeitStrafe);
|
||||
setSlider('sldZeitstrafe', 'valZeitstrafe', s.wahrscheinlichkeitSperre);
|
||||
setSlider('sldAufgaben', 'valAufgaben', s.aufgabenProLevel);
|
||||
setSlider('sldZeit', 'valZeit', Math.round(s.zeitfaktorZeitstrafen * 10),
|
||||
v => (v / 10).toFixed(1).replace('.', ','));
|
||||
updateWarnung();
|
||||
}
|
||||
|
||||
(async function restore() {
|
||||
const saved = sessionStorage.getItem('bdsm-session-settings');
|
||||
if (saved) {
|
||||
applySettings(JSON.parse(saved));
|
||||
return;
|
||||
}
|
||||
// Fallback: Draft aus DB laden
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) return;
|
||||
const draft = await res.json();
|
||||
if (draft.setupId && !sessionStorage.getItem('bdsm-setup-id')) {
|
||||
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
}
|
||||
if (draft.settingsJson) {
|
||||
const s = JSON.parse(draft.settingsJson);
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(s));
|
||||
applySettings(s);
|
||||
}
|
||||
if (draft.setupJson) {
|
||||
sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
}
|
||||
if (draft.gruppenJson) {
|
||||
sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||
<title>BDSM Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
|
||||
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
|
||||
const sessionId = sessionStorage.getItem('bdsm-session-id');
|
||||
if (!sessionId) window.location.replace('/bdsm.html');
|
||||
if (!sessionId) window.location.replace('/neubdsm.html');
|
||||
|
||||
// Multi-Device: bin ich Gast?
|
||||
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
|
||||
@@ -424,7 +424,7 @@
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`);
|
||||
if (res.status === 204) { zeigeFinaleDialog(); return; }
|
||||
if (res.status === 400) { window.location.replace('/bdsmtoys.html'); return; }
|
||||
if (res.status === 400) { window.location.replace('/neubdsm.html'); return; }
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
currentTask = await res.json();
|
||||
await saveAktiveAufgabe(currentTask, null);
|
||||
|
||||
@@ -2,984 +2,10 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Mitspieler – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.session-setup { }
|
||||
|
||||
.setup-section { margin-bottom: 2.5rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* ── Player cards ── */
|
||||
.player-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.player-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.player-title {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.player-badge {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.player-badge-pending {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.player-badge-accepted {
|
||||
background: #1a5c2a;
|
||||
color: #6fcf97;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.player-remove {
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: normal;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.player-remove:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
.btn-invite {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 0.45rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-invite:hover { background: var(--color-primary); color: #fff; }
|
||||
.btn-cancel-invite {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
padding: 0.2rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: normal;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
|
||||
|
||||
.pending-info {
|
||||
text-align: center;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.9rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.pending-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.pending-mode {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.card-field { margin-bottom: 1rem; }
|
||||
.card-field > label {
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
margin: 0 0 0.5rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.check-item {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.45rem;
|
||||
background: var(--color-secondary);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.check-item.is-checked { border-color: var(--color-primary); }
|
||||
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
|
||||
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
|
||||
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
|
||||
|
||||
.add-player-btn {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
|
||||
|
||||
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
|
||||
|
||||
/* ── Freunde-Modal ── */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
z-index: 1000;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.modal-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.75rem;
|
||||
max-width: 420px; width: 100%;
|
||||
}
|
||||
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 1rem; }
|
||||
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
|
||||
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
|
||||
.friend-combobox { position: relative; }
|
||||
.friend-dropdown {
|
||||
display: none; position: absolute; top: 100%; left: 0; right: 0;
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem;
|
||||
}
|
||||
.friend-dropdown-item {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s;
|
||||
font-size: 0.9rem; font-weight: 600;
|
||||
}
|
||||
.friend-dropdown-item:hover { background: var(--color-secondary); }
|
||||
.selected-friend-box {
|
||||
display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem;
|
||||
background: var(--color-secondary); border-radius: 8px;
|
||||
font-size: 0.9rem; font-weight: 600;
|
||||
border: 1px solid var(--color-primary); color: var(--color-text);
|
||||
}
|
||||
.modal-cancel { margin-top: 0.6rem; width: 100%; }
|
||||
</style>
|
||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||
<title>BDSM Game</title>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="modal-overlay" id="errorModal" style="display:none;">
|
||||
<div class="modal-card" style="text-align:center;">
|
||||
<div style="font-size:1.5rem;margin-bottom:0.75rem;">⚠️</div>
|
||||
<div class="modal-title" id="errorModalTitle"></div>
|
||||
<div class="modal-text" id="errorModalText" style="font-size:0.9rem;color:var(--color-muted);line-height:1.5;margin-bottom:1.25rem;"></div>
|
||||
<button onclick="document.getElementById('errorModal').style.display='none'">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="friendModal" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-title">Freund einladen</div>
|
||||
<div class="friend-combobox">
|
||||
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
|
||||
<div class="friend-dropdown" id="friendDropdown"></div>
|
||||
</div>
|
||||
<div class="selected-friend-box" id="selectedFriendBox"></div>
|
||||
<button id="btnEinladen" style="margin-top:1rem; width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
|
||||
<button class="secondary modal-cancel" onclick="schliesseFriendModal()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 2 von 4 – Mitspieler</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Mitspieler</h2>
|
||||
<div id="playersContainer"></div>
|
||||
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
<div style="display:flex; gap:1rem;">
|
||||
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsm.html'">← Zurück</button>
|
||||
<button style="flex:2;" id="weiterBtn" onclick="weiter()">Weiter</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
// SetupId erzeugen (persistent über sessionStorage)
|
||||
if (!sessionStorage.getItem('bdsm-setup-id')) {
|
||||
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
|
||||
}
|
||||
let setupId = sessionStorage.getItem('bdsm-setup-id');
|
||||
|
||||
// Draft aus DB laden wenn sessionStorage leer
|
||||
async function ladeSessionOderDraft() {
|
||||
if (sessionStorage.getItem('bdsm-session-settings')) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) {
|
||||
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
setupId = draft.setupId;
|
||||
}
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
if (!draft.settingsJson) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const GESCHLECHTER = [
|
||||
{ value: 'MAENNLICH', label: 'Männlich' },
|
||||
{ value: 'WEIBLICH', label: 'Weiblich' },
|
||||
{ value: 'DIVERS', label: 'Divers' },
|
||||
];
|
||||
const ROLLEN = [
|
||||
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe – Aktiv' },
|
||||
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe – Passiv' },
|
||||
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung – Aktiv' },
|
||||
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung – Passiv' },
|
||||
];
|
||||
const WERKZEUGE_DEFAULTS = {
|
||||
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
};
|
||||
const WERKZEUGE = [
|
||||
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
|
||||
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
|
||||
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
|
||||
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
|
||||
];
|
||||
const ROLE_LABELS = {
|
||||
AUFGABE_AKTIV: 'Aufgabe – Aktiv', AUFGABE_PASSIV: 'Aufgabe – Passiv',
|
||||
BESTRAFUNG_AKTIV: 'Bestrafung – Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung – Passiv',
|
||||
};
|
||||
|
||||
let playerSeq = 0;
|
||||
let playerIds = [];
|
||||
// { [playerId]: { einladungId, status, inviteeId, inviteeName, mode } | null }
|
||||
let playerInvitations = {};
|
||||
let pollIntervalId = null;
|
||||
let myUserId = null;
|
||||
let selfPlayerId = null;
|
||||
let freundeListe = [];
|
||||
|
||||
function buildCheckItems(name, items, type, disabled = false) {
|
||||
return items.map(({ value, label, desc }) => `
|
||||
<label class="check-item${disabled ? ' is-disabled' : ''}">
|
||||
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
|
||||
<span>
|
||||
<span class="check-item-label">${label}</span>
|
||||
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
|
||||
</span>
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
function createCardHtml(id, prefillName, isSelf) {
|
||||
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
|
||||
const num = playerIds.indexOf(id) + 1;
|
||||
const nameField = isSelf
|
||||
? `<input type="text" id="p${id}-name" value="${prefillName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
|
||||
: `<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">`;
|
||||
const inviteBtn = isSelf ? '' : `<button class="btn-invite" onclick="oeffneFreundeModal(${id})">👥 Einladen</button>`;
|
||||
return `
|
||||
<div class="player-card" id="player-${id}">
|
||||
<div class="player-card-header">
|
||||
<span class="player-title">Spieler ${num}</span>
|
||||
${badge}
|
||||
${inviteBtn}
|
||||
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
|
||||
</div>
|
||||
<div id="p${id}-body">
|
||||
${buildPlayerBody(id, nameField, isSelf)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function buildPlayerBody(id, nameField, genderDisabled = false) {
|
||||
return `
|
||||
<div class="card-field">
|
||||
<label>Name</label>
|
||||
${nameField}
|
||||
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
|
||||
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio', genderDisabled)}</div>
|
||||
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Spielt mit</label>
|
||||
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
|
||||
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
|
||||
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Rollen</label>
|
||||
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
|
||||
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Verfügbar</label>
|
||||
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
|
||||
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
|
||||
<div id="p${id}-chastity-hint" style="display:none;font-size:0.78rem;color:var(--color-muted);margin-top:0.4rem;font-style:italic;line-height:1.4;"></div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Finale</label>
|
||||
<label class="check-item is-checked" id="p${id}-sperre-label">
|
||||
<input type="checkbox" id="p${id}-sperrenAufloesen" checked onchange="toggleSperreWarning(${id})">
|
||||
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
|
||||
</label>
|
||||
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="p${id}-sperre-warn">
|
||||
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Diese Person könnte im Finale leer ausgehen.
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function addPlayer(prefillName = '', isSelf = false) {
|
||||
playerSeq++;
|
||||
const id = playerSeq;
|
||||
if (isSelf) selfPlayerId = id;
|
||||
playerIds.push(id);
|
||||
playerInvitations[id] = null;
|
||||
document.getElementById('playersContainer')
|
||||
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
|
||||
refreshRemoveButtons();
|
||||
return id;
|
||||
}
|
||||
|
||||
function removePlayer(id) {
|
||||
const inv = playerInvitations[id];
|
||||
// Einladung serverseitig canceln
|
||||
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
|
||||
fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
playerInvitations[id] = null;
|
||||
|
||||
if (playerIds.length <= 2) {
|
||||
// Letzter Slot: nur leeren, nicht entfernen
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
if (header) {
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
const invBtn = header.querySelector('.btn-invite');
|
||||
if (invBtn) invBtn.style.display = '';
|
||||
}
|
||||
const body = document.getElementById(`p${id}-body`);
|
||||
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('player-' + id)?.remove();
|
||||
playerIds = playerIds.filter(x => x !== id);
|
||||
delete playerInvitations[id];
|
||||
refreshPlayerTitles();
|
||||
refreshRemoveButtons();
|
||||
}
|
||||
|
||||
function refreshPlayerTitles() {
|
||||
playerIds.forEach((id, idx) => {
|
||||
const el = document.querySelector(`#player-${id} .player-title`);
|
||||
if (el) el.textContent = 'Spieler ' + (idx + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshRemoveButtons() {
|
||||
playerIds.forEach((id, idx) => {
|
||||
const btn = document.querySelector(`#player-${id} .player-remove`);
|
||||
if (btn) btn.style.display = idx === 0 ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const input = e.target;
|
||||
if (input.type !== 'checkbox' && input.type !== 'radio') return;
|
||||
if (input.type === 'radio') {
|
||||
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r => {
|
||||
r.closest('.check-item')?.classList.toggle('is-checked', r.checked);
|
||||
});
|
||||
if (input.checked && input.name.endsWith('-geschlecht')) {
|
||||
const prefix = input.name.slice(0, -'-geschlecht'.length);
|
||||
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
|
||||
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
|
||||
if (cb.closest('.check-item')?.dataset.chastitylocked) return;
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const label = input.closest('.check-item');
|
||||
if (label?.dataset.chastitylocked) {
|
||||
// Revert immediately and flash hint
|
||||
input.checked = false;
|
||||
label.classList.remove('is-checked');
|
||||
const card = label.closest('[id^="player-"]');
|
||||
if (card) flashChastityHint(card.id.replace('player-', ''));
|
||||
return;
|
||||
}
|
||||
label?.classList.toggle('is-checked', input.checked);
|
||||
}
|
||||
});
|
||||
|
||||
function getChecked(name) {
|
||||
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
|
||||
}
|
||||
|
||||
function toggleSperreWarning(id) {
|
||||
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
const warn = document.getElementById(`p${id}-sperre-warn`);
|
||||
const label = document.getElementById(`p${id}-sperre-label`);
|
||||
if (!cb) return;
|
||||
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
|
||||
if (label) label.classList.toggle('is-checked', cb.checked);
|
||||
}
|
||||
|
||||
function setFieldError(id, show) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// ── Freunde-Modal ──
|
||||
let currentInvitePlayerId = null;
|
||||
let selectedFriend = null; // { userId, name }
|
||||
|
||||
async function oeffneFreundeModal(playerId) {
|
||||
currentInvitePlayerId = playerId;
|
||||
selectedFriend = null;
|
||||
document.getElementById('friendSearch').value = '';
|
||||
document.getElementById('friendDropdown').style.display = 'none';
|
||||
document.getElementById('friendDropdown').innerHTML = '';
|
||||
document.getElementById('selectedFriendBox').style.display = 'none';
|
||||
document.getElementById('selectedFriendBox').textContent = '';
|
||||
document.getElementById('btnEinladen').disabled = true;
|
||||
document.getElementById('friendModal').style.display = 'flex';
|
||||
if (freundeListe.length === 0) {
|
||||
try {
|
||||
const res = await fetch('/social/friends');
|
||||
freundeListe = res.ok ? await res.json() : [];
|
||||
} catch (_) { freundeListe = []; }
|
||||
}
|
||||
}
|
||||
|
||||
function filterFreunde(query) {
|
||||
selectedFriend = null;
|
||||
document.getElementById('selectedFriendBox').style.display = 'none';
|
||||
document.getElementById('btnEinladen').disabled = true;
|
||||
const dropdown = document.getElementById('friendDropdown');
|
||||
dropdown.innerHTML = '';
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) { dropdown.style.display = 'none'; return; }
|
||||
const invitedIds = new Set(
|
||||
Object.values(playerInvitations)
|
||||
.filter(inv => inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST'))
|
||||
.map(inv => inv.inviteeId)
|
||||
);
|
||||
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q) && !invitedIds.has(f.user.userId));
|
||||
if (!matches.length) {
|
||||
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
|
||||
dropdown.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
matches.forEach(f => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'friend-dropdown-item';
|
||||
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt'));
|
||||
if (f.user.profilePicture) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'friend-avatar';
|
||||
img.src = 'data:image/png;base64,' + f.user.profilePicture;
|
||||
img.alt = '';
|
||||
item.appendChild(img);
|
||||
} else {
|
||||
const av = document.createElement('div');
|
||||
av.className = 'friend-avatar';
|
||||
item.appendChild(av);
|
||||
}
|
||||
const span = document.createElement('span');
|
||||
span.textContent = f.user.name || 'Unbekannt';
|
||||
item.appendChild(span);
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
function selectFriend(userId, name) {
|
||||
selectedFriend = { userId, name };
|
||||
document.getElementById('friendSearch').value = name;
|
||||
document.getElementById('friendDropdown').style.display = 'none';
|
||||
const box = document.getElementById('selectedFriendBox');
|
||||
box.textContent = '✓ ' + name;
|
||||
box.style.display = 'block';
|
||||
document.getElementById('btnEinladen').disabled = false;
|
||||
}
|
||||
|
||||
async function confirmedEinladen() {
|
||||
if (!selectedFriend) return;
|
||||
await einladen(selectedFriend.userId, selectedFriend.name);
|
||||
}
|
||||
|
||||
function schliesseFriendModal() {
|
||||
document.getElementById('friendModal').style.display = 'none';
|
||||
currentInvitePlayerId = null;
|
||||
selectedFriend = null;
|
||||
}
|
||||
|
||||
function zeigePopup(title, text) {
|
||||
document.getElementById('errorModalTitle').textContent = title;
|
||||
document.getElementById('errorModalText').textContent = text;
|
||||
document.getElementById('errorModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function einladen(inviteeId, inviteeName) {
|
||||
const id = currentInvitePlayerId;
|
||||
schliesseFriendModal();
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await fetch('/bdsm/einladung', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
|
||||
});
|
||||
if (res.status === 409) {
|
||||
zeigePopup('Bereits eingeladen', `${inviteeName} ist bereits eingeladen oder nimmt schon am Spiel teil.`);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
|
||||
renderPending(id);
|
||||
startPoll();
|
||||
} catch (_) {
|
||||
zeigePopup('Fehler', 'Einladung konnte nicht gesendet werden. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
function renderPending(id) {
|
||||
const inv = playerInvitations[id];
|
||||
if (!inv) return;
|
||||
const body = document.getElementById(`p${id}-body`);
|
||||
if (!body) return;
|
||||
const headerInvBtn = document.querySelector(`#player-${id} .btn-invite`);
|
||||
if (headerInvBtn) headerInvBtn.style.display = 'none';
|
||||
|
||||
if (inv.status === 'PENDING') {
|
||||
// Badge nach "Spieler X" einfügen
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-pending">Ausstehend</span>`);
|
||||
body.innerHTML = `
|
||||
<div class="pending-info">
|
||||
<div class="pending-name">${inv.inviteeName}</div>
|
||||
<div>Einladung wurde gesendet – warte auf Antwort…</div>
|
||||
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
|
||||
</div>`;
|
||||
} else if (inv.status === 'ACCEPTED_OWN') {
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Eigenes Gerät</span>`);
|
||||
body.innerHTML = `
|
||||
<div class="pending-info">
|
||||
<div class="pending-name">${inv.inviteeName}</div>
|
||||
<div class="pending-mode">Spieler konfiguriert Präferenzen auf dem eigenen Gerät.</div>
|
||||
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
|
||||
</div>`;
|
||||
} else if (inv.status === 'ACCEPTED_HOST') {
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Host-Gerät</span>`);
|
||||
const nameField = `<input type="text" id="p${id}-name" value="${inv.inviteeName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`;
|
||||
const hasGeschlecht = inv.defaults && inv.defaults.geschlecht;
|
||||
body.innerHTML = buildPlayerBody(id, nameField, hasGeschlecht);
|
||||
if (inv.defaults) {
|
||||
restorePlayer(id, {
|
||||
geschlecht: inv.defaults.geschlecht,
|
||||
spieltMit: inv.defaults.spieltMit || [],
|
||||
rollen: inv.defaults.rollen || [],
|
||||
werkzeuge: inv.defaults.werkzeuge || [],
|
||||
});
|
||||
}
|
||||
pruefeChastityConstraint(id, inv.inviteeId);
|
||||
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
|
||||
// Slot wieder freigeben
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
if (headerInvBtn) headerInvBtn.style.display = '';
|
||||
playerInvitations[id] = null;
|
||||
body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelEinladung(id) {
|
||||
const inv = playerInvitations[id];
|
||||
if (!inv) return;
|
||||
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
|
||||
playerInvitations[id] = null;
|
||||
// UI zurücksetzen
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
if (header) {
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
const invBtn = header.querySelector('.btn-invite');
|
||||
if (invBtn) invBtn.style.display = '';
|
||||
}
|
||||
const body = document.getElementById(`p${id}-body`);
|
||||
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
|
||||
}
|
||||
|
||||
// ── Polling ──
|
||||
function startPoll() {
|
||||
if (pollIntervalId) return;
|
||||
pollIntervalId = setInterval(pollEinladungen, 3000);
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
|
||||
}
|
||||
|
||||
async function pollEinladungen() {
|
||||
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
|
||||
if (!hasPending) { stopPoll(); return; }
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
|
||||
if (!res.ok) return;
|
||||
const liste = await res.json();
|
||||
for (const e of liste) {
|
||||
const id = playerIds.find(pid => {
|
||||
const inv = playerInvitations[pid];
|
||||
return inv && inv.einladungId === e.einladungId;
|
||||
});
|
||||
if (!id) continue;
|
||||
const inv = playerInvitations[id];
|
||||
if (!inv || inv.status === e.status) continue;
|
||||
inv.status = e.status;
|
||||
if (e.status === 'DECLINED' || e.status === 'CANCELLED') {
|
||||
showMessage(`${inv.inviteeName} hat die Einladung abgelehnt oder abgebrochen.`, 'error');
|
||||
}
|
||||
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
|
||||
// Profil + Defaults der eingeladenen Person laden
|
||||
try {
|
||||
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
|
||||
if (dRes.ok) inv.defaults = await dRes.json();
|
||||
} catch (_) {}
|
||||
}
|
||||
renderPending(id);
|
||||
}
|
||||
} catch (_) {}
|
||||
updateWeiterBtn();
|
||||
}
|
||||
|
||||
function updateWeiterBtn() {
|
||||
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
|
||||
document.getElementById('weiterBtn').disabled = hasPending;
|
||||
}
|
||||
|
||||
// ── Validation & Weiter ──
|
||||
function weiter() {
|
||||
hideMessage();
|
||||
|
||||
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
|
||||
if (hasPending) {
|
||||
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let valid = true;
|
||||
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
|
||||
|
||||
const mitspieler = playerIds.map(id => {
|
||||
const inv = playerInvitations[id];
|
||||
const isOwnDevice = inv && inv.status === 'ACCEPTED_OWN';
|
||||
|
||||
if (isOwnDevice) {
|
||||
// Daten werden vom Spieler selbst auf dem eigenen Gerät konfiguriert
|
||||
return {
|
||||
name: inv.inviteeName,
|
||||
geschlecht: null,
|
||||
spieltMit: [], rollen: [], werkzeuge: [],
|
||||
userId: inv.inviteeId,
|
||||
eigenesGeraet: true,
|
||||
einladungId: inv.einladungId,
|
||||
sperrenVorFinaleAufloesen: true, // Wird auf dem eigenen Gerät konfiguriert
|
||||
};
|
||||
}
|
||||
|
||||
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
|
||||
const geschlecht = getChecked(`p${id}-geschlecht`);
|
||||
const spieltMit = getChecked(`p${id}-spieltmit`);
|
||||
const rollen = getChecked(`p${id}-rollen`);
|
||||
const werkzeuge = getChecked(`p${id}-werkzeuge`);
|
||||
|
||||
setFieldError(`p${id}-name-err`, !name);
|
||||
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
|
||||
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
|
||||
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
|
||||
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
|
||||
|
||||
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
const sperrenAufloesen = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
return {
|
||||
name,
|
||||
geschlecht: geschlecht[0] || null,
|
||||
spieltMit, rollen, werkzeuge,
|
||||
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
|
||||
eigenesGeraet: false,
|
||||
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
|
||||
};
|
||||
});
|
||||
|
||||
if (!valid) { showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error'); return; }
|
||||
|
||||
const configuredMitspieler = mitspieler.filter(p => !p.eigenesGeraet);
|
||||
const hasOwnDeviceInRoleCheck = mitspieler.some(p => p.eigenesGeraet);
|
||||
if (!hasOwnDeviceInRoleCheck) {
|
||||
const allRoles = new Set(configuredMitspieler.flatMap(p => p.rollen));
|
||||
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
|
||||
if (missingRoles.length > 0) {
|
||||
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
|
||||
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const hasOwnDevicePlayers = mitspieler.some(p => p.eigenesGeraet);
|
||||
if (!hasOwnDevicePlayers) {
|
||||
let partnerFehler = false;
|
||||
configuredMitspieler.forEach((player, i) => {
|
||||
const andereGeschlechter = configuredMitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
|
||||
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
|
||||
if (!hatPartner) {
|
||||
const globalIdx = mitspieler.indexOf(player);
|
||||
setFieldError(`p${playerIds[globalIdx]}-partner-err`, true);
|
||||
partnerFehler = true;
|
||||
}
|
||||
});
|
||||
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
|
||||
}
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const sessionSetup = JSON.stringify({ settings, mitspieler });
|
||||
sessionStorage.setItem('bdsm-session-setup', sessionSetup);
|
||||
// Draft in DB aktualisieren
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
setupId,
|
||||
setupJson: sessionSetup,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmtasks.html';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
el.textContent = text; el.className = `message ${type}`; el.style.display = 'block';
|
||||
}
|
||||
function hideMessage() { document.getElementById('message').style.display = 'none'; }
|
||||
|
||||
// ── Chastity-Constraint: gesperrtes Werkzeug angehakt + disabled ──
|
||||
async function pruefeChastityConstraint(playerId, userId) {
|
||||
if (!userId) return;
|
||||
try {
|
||||
const res = await fetch(`/bdsm/chastity-constraint?userId=${userId}`);
|
||||
if (!res.ok) return;
|
||||
const { lockedWerkzeug } = await res.json();
|
||||
if (!lockedWerkzeug) return;
|
||||
const locked = lockedWerkzeug === 'BOTH' ? ['VAGINA', 'PENIS'] : [lockedWerkzeug];
|
||||
locked.forEach(w => sperrWerkzeugCheckbox(playerId, w));
|
||||
// Hinweis-Text im hidden div speichern, Popup beim Klick auf Checkbox
|
||||
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
|
||||
if (hintEl) {
|
||||
hintEl.dataset.hintText = '🔒 Das System weiß aus halbwegs verlässlicher Quelle, dass diese Person dieses Körperteil gerade nicht einsetzen kann – und lässt sich hier auch nicht austricksen.';
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function sperrWerkzeugCheckbox(playerId, werkzeug) {
|
||||
const cb = document.querySelector(`input[name="p${playerId}-werkzeuge"][value="${werkzeug}"]`);
|
||||
if (!cb) return;
|
||||
cb.checked = false;
|
||||
cb.disabled = false;
|
||||
const label = cb.closest('.check-item');
|
||||
if (label) {
|
||||
label.classList.remove('is-checked', 'is-disabled');
|
||||
label.dataset.chastitylocked = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function flashChastityHint(playerId) {
|
||||
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
|
||||
const text = hintEl?.dataset.hintText;
|
||||
if (!text) return;
|
||||
document.getElementById('errorModalTitle').textContent = 'Nicht verfügbar';
|
||||
document.getElementById('errorModalText').textContent = text;
|
||||
document.getElementById('errorModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Klick auf gesperrte Checkbox: Hint aufblinken lassen
|
||||
document.addEventListener('click', e => {
|
||||
const label = e.target.closest('.check-item[data-chastitylocked]');
|
||||
if (!label) return;
|
||||
const card = label.closest('[id^="player-"]');
|
||||
if (!card) return;
|
||||
const playerId = card.id.replace('player-', '');
|
||||
flashChastityHint(playerId);
|
||||
});
|
||||
|
||||
function restorePlayer(id, data) {
|
||||
if (data.geschlecht) {
|
||||
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
|
||||
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
|
||||
}
|
||||
(data.spieltMit || []).forEach(val => {
|
||||
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
(data.rollen || []).forEach(val => {
|
||||
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
(data.werkzeuge || []).forEach(val => {
|
||||
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
if (data.sperrenVorFinaleAufloesen === false) {
|
||||
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
if (cb) { cb.checked = false; toggleSperreWarning(id); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Einladungen aus DB wiederherstellen ──
|
||||
async function ladeEinladungenAusDb(userIdToInfo) {
|
||||
// userIdToInfo: { [userId]: { playerId, name } } oder null (dann Matching per slotIndex)
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
|
||||
if (!res.ok) return;
|
||||
const einladungen = await res.json();
|
||||
const aktive = einladungen.filter(e =>
|
||||
e.status === 'PENDING' || e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST');
|
||||
for (const e of aktive) {
|
||||
let playerId;
|
||||
if (userIdToInfo && userIdToInfo[e.inviteeId]) {
|
||||
playerId = userIdToInfo[e.inviteeId].playerId;
|
||||
} else {
|
||||
// Fallback: slotIndex direkt als playerId nutzen (wenn playerIds das enthält)
|
||||
playerId = playerIds.find(pid => pid === e.slotIndex);
|
||||
}
|
||||
if (!playerId) continue;
|
||||
const inviteeName = (userIdToInfo?.[e.inviteeId]?.name) || e.inviteeName || '';
|
||||
playerInvitations[playerId] = {
|
||||
einladungId: e.einladungId,
|
||||
status: e.status,
|
||||
inviteeId: e.inviteeId,
|
||||
inviteeName,
|
||||
};
|
||||
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
|
||||
try {
|
||||
const dRes = await fetch(`/user/${e.inviteeId}/bdsm-defaults`);
|
||||
if (dRes.ok) playerInvitations[playerId].defaults = await dRes.json();
|
||||
} catch (_) {}
|
||||
}
|
||||
renderPending(playerId);
|
||||
}
|
||||
if (aktive.some(e => e.status === 'PENDING')) startPoll();
|
||||
updateWeiterBtn();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
|
||||
// myUserId immer laden (wird für userId des Host-Spielers benötigt)
|
||||
const user = await fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
myUserId = user?.userId || null;
|
||||
|
||||
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
|
||||
if (savedSetup) {
|
||||
const { mitspieler } = JSON.parse(savedSetup);
|
||||
const userIdToInfo = {};
|
||||
mitspieler.forEach((p, i) => {
|
||||
const id = addPlayer(p.name, i === 0);
|
||||
restorePlayer(id, p);
|
||||
if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name };
|
||||
});
|
||||
mitspieler.forEach((p, i) => { if (p.userId) pruefeChastityConstraint(playerIds[i], p.userId); });
|
||||
await ladeEinladungenAusDb(userIdToInfo);
|
||||
} else {
|
||||
const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
|
||||
addPlayer(user ? user.name : '', true);
|
||||
addPlayer();
|
||||
const selfId = playerIds[0];
|
||||
restorePlayer(selfId, {
|
||||
geschlecht: user?.geschlecht || null,
|
||||
spieltMit: defaults.spieltMit || [],
|
||||
rollen: defaults.rollen || [],
|
||||
werkzeuge: defaults.werkzeuge || [],
|
||||
});
|
||||
if (myUserId) pruefeChastityConstraint(selfId, myUserId);
|
||||
// Auch für frischen Start: evtl. noch offene Einladungen aus vorheriger Session
|
||||
await ladeEinladungenAusDb(null);
|
||||
}
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<body>
|
||||
<script>window.location.replace('/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,352 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Aufgaben-Gruppen – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.session-setup { }
|
||||
|
||||
.setup-section { margin-bottom: 2.5rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.select-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.select-all-label input {
|
||||
accent-color: var(--color-primary);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gruppe-list { list-style: none; padding: 0; margin: 0; }
|
||||
.gruppe-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.gruppe-item.is-checked { border-color: var(--color-primary); }
|
||||
.gruppe-item input {
|
||||
accent-color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gruppe-item span { flex: 1; min-width: 0; }
|
||||
.item-img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gruppe-item-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.gruppe-item-desc {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.empty-hint {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 3 von 4 – Aufgaben</p>
|
||||
|
||||
<div class="setup-section" id="sectionOwn">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listOwn">
|
||||
Eigene Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listOwn"></ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-section" id="sectionSubscribed">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listSubscribed">
|
||||
Abonnierte Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listSubscribed"></ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-section" id="sectionSystem">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listSystem">
|
||||
System-Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listSystem"></ul>
|
||||
</div>
|
||||
|
||||
<div style="position:relative; margin-top:2rem;">
|
||||
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
|
||||
<div style="display:flex; gap:1rem;">
|
||||
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmplayers.html'">← Zurück</button>
|
||||
<button style="flex:2;" onclick="weiter()">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
let savedGruppen = new Set();
|
||||
|
||||
async function ladeSessionOderDraft() {
|
||||
if (sessionStorage.getItem('bdsm-session-setup')) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
if (!draft.setupJson) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let warnungsAkzeptiert = false;
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const cb = e.target;
|
||||
if (cb.type !== 'checkbox') return;
|
||||
|
||||
if (cb.classList.contains('select-all-cb')) {
|
||||
// Alle Gruppen in dieser Sektion (de-)selektieren
|
||||
const list = document.getElementById(cb.dataset.list);
|
||||
list.querySelectorAll('input[type="checkbox"]').forEach(itemCb => {
|
||||
itemCb.checked = cb.checked;
|
||||
itemCb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
} else {
|
||||
// Einzelne Gruppe: is-checked-Klasse anpassen und Alles-Haken aktualisieren
|
||||
cb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
|
||||
updateSelectAll(cb.closest('.gruppe-list'));
|
||||
}
|
||||
|
||||
warnungsAkzeptiert = false;
|
||||
hideMessage();
|
||||
});
|
||||
|
||||
function updateSelectAll(list) {
|
||||
if (!list) return;
|
||||
const itemCbs = [...list.querySelectorAll('input[type="checkbox"]')];
|
||||
if (!itemCbs.length) return;
|
||||
const section = list.closest('.setup-section');
|
||||
const selectAllCb = section?.querySelector('.select-all-cb');
|
||||
if (!selectAllCb) return;
|
||||
const checkedCount = itemCbs.filter(cb => cb.checked).length;
|
||||
selectAllCb.checked = checkedCount === itemCbs.length;
|
||||
selectAllCb.indeterminate = checkedCount > 0 && checkedCount < itemCbs.length;
|
||||
}
|
||||
|
||||
function renderList(containerId, gruppen) {
|
||||
const ul = document.getElementById(containerId);
|
||||
const section = ul.closest('.setup-section');
|
||||
const selectAllWrap = section?.querySelector('.select-all-label');
|
||||
|
||||
if (!gruppen.length) {
|
||||
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
|
||||
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
ul.innerHTML = gruppen.map(g => {
|
||||
const checked = savedGruppen.has(g.gruppenId);
|
||||
return `
|
||||
<li>
|
||||
<label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
|
||||
<span>
|
||||
<span class="gruppe-item-name">${g.name}</span>
|
||||
${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}
|
||||
</span>
|
||||
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
|
||||
</label>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
updateSelectAll(ul);
|
||||
}
|
||||
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
|
||||
|
||||
function validateContent(content, settings, mitspieler) {
|
||||
const errors = [], warnings = [];
|
||||
|
||||
const aufgabenByLevel = {};
|
||||
content.aufgaben.forEach(a => {
|
||||
const l = a.level ?? 0;
|
||||
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
|
||||
});
|
||||
|
||||
for (const [level, count] of Object.entries(aufgabenByLevel)) {
|
||||
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) – Minimum 5 erforderlich`);
|
||||
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben – empfohlen ≥ 10`);
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitStrafe > 1) {
|
||||
const strafenByLevel = {};
|
||||
content.strafen.forEach(s => {
|
||||
const l = s.level ?? 0;
|
||||
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
|
||||
});
|
||||
for (const level of Object.keys(aufgabenByLevel)) {
|
||||
const count = strafenByLevel[level] || 0;
|
||||
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
|
||||
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) – empfohlen ≥ 2`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitSperre > 1) {
|
||||
const count = content.sperren.length;
|
||||
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
|
||||
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) – empfohlen ≥ 5`);
|
||||
}
|
||||
|
||||
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
|
||||
for (const g of beteiligtGeschlecht) {
|
||||
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
|
||||
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
function showValidation(errors, warnings, mitHinweis) {
|
||||
const el = document.getElementById('message');
|
||||
el.innerHTML = [
|
||||
...errors.map(e => `<div>✕ ${e}</div>`),
|
||||
...warnings.map(w => `<div>⚠ ${w}</div>`),
|
||||
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Weiter klicken um fortzufahren.</div>'] : []),
|
||||
].join('');
|
||||
el.className = `message ${errors.length ? 'error' : 'warning'}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
|
||||
el.textContent = icon + text;
|
||||
el.className = `message ${type}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
async function weiter() {
|
||||
hideMessage();
|
||||
const selected = [...document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked')]
|
||||
.map(cb => cb.value);
|
||||
if (selected.length === 0) {
|
||||
showMessage('Bitte mindestens eine Aufgaben-Gruppe auswählen.', 'error');
|
||||
warnungsAkzeptiert = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('button[onclick="weiter()"]');
|
||||
btn.disabled = true;
|
||||
const gruppen = await Promise.all(
|
||||
selected.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
);
|
||||
btn.disabled = false;
|
||||
|
||||
const content = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
|
||||
gruppen.filter(Boolean).forEach(g => {
|
||||
content.aufgaben.push(...(g.aufgaben || []));
|
||||
content.strafen.push(...(g.strafen || []));
|
||||
content.sperren.push(...(g.sperren || []));
|
||||
content.finisher.push(...(g.finisher || []));
|
||||
});
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
const { errors, warnings } = validateContent(content, settings, setup?.mitspieler || []);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showValidation(errors, warnings, false);
|
||||
warnungsAkzeptiert = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !warnungsAkzeptiert) {
|
||||
showValidation([], warnings, true);
|
||||
warnungsAkzeptiert = true;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
|
||||
// Draft in DB aktualisieren
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gruppenJson: JSON.stringify(selected) }),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmtoys.html';
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
|
||||
|
||||
try {
|
||||
const [own, abo, system] = await Promise.all([
|
||||
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
]);
|
||||
renderList('listOwn', own.content || []);
|
||||
renderList('listSubscribed', abo.content || []);
|
||||
renderList('listSystem', system.content || []);
|
||||
} catch (err) { console.error('[bdsmtasks] Fehler beim Laden:', err); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||
<title>BDSM Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,372 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Toys – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.session-setup { }
|
||||
|
||||
.setup-section { margin-bottom: 2.5rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.toy-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.toy-item.is-checked { border-color: var(--color-primary); }
|
||||
.toy-item input {
|
||||
accent-color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toy-item span { flex: 1; min-width: 0; }
|
||||
.item-img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
|
||||
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
|
||||
.no-toys { color: var(--color-muted); font-size: 0.875rem; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 4 von 4 – Toys</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Benötigte Toys</h2>
|
||||
<p style="font-size:0.85rem; color:var(--color-muted); margin-bottom:1.25rem;">
|
||||
Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt.
|
||||
</p>
|
||||
<div id="toyList"></div>
|
||||
</div>
|
||||
|
||||
<div style="position:relative; margin-top:2rem;">
|
||||
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
|
||||
<div style="display:flex; gap:1rem;">
|
||||
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmtasks.html'">← Zurück</button>
|
||||
<button style="flex:2;" onclick="spielStarten()">Spiel starten</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
let savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
|
||||
|
||||
async function ladeSessionOderDraft() {
|
||||
if (savedGruppen) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) { sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson); savedGruppen = JSON.parse(draft.gruppenJson); }
|
||||
if (!savedGruppen) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Previously saved toy selection (when navigating back from game page)
|
||||
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
|
||||
const savedToyIds = savedToysRaw
|
||||
? new Set(JSON.parse(savedToysRaw).map(t => t.toyId))
|
||||
: null; // null = first visit → default all checked
|
||||
|
||||
// All content collected from selected groups
|
||||
const allContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
|
||||
|
||||
let warnungsAkzeptiert = false;
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const cb = e.target;
|
||||
if (cb.type !== 'checkbox') return;
|
||||
cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
|
||||
warnungsAkzeptiert = false;
|
||||
hideMessage();
|
||||
});
|
||||
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
|
||||
|
||||
function validateContent(content, settings, mitspieler) {
|
||||
const errors = [], warnings = [];
|
||||
|
||||
const aufgabenByLevel = {};
|
||||
content.aufgaben.forEach(a => {
|
||||
const l = a.level ?? 0;
|
||||
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
|
||||
});
|
||||
|
||||
for (const [level, count] of Object.entries(aufgabenByLevel)) {
|
||||
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) – Minimum 5 erforderlich`);
|
||||
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben – empfohlen ≥ 10`);
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitStrafe > 1) {
|
||||
const strafenByLevel = {};
|
||||
content.strafen.forEach(s => {
|
||||
const l = s.level ?? 0;
|
||||
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
|
||||
});
|
||||
for (const level of Object.keys(aufgabenByLevel)) {
|
||||
const count = strafenByLevel[level] || 0;
|
||||
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
|
||||
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) – empfohlen ≥ 2`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitSperre > 1) {
|
||||
const count = content.sperren.length;
|
||||
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
|
||||
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) – empfohlen ≥ 5`);
|
||||
}
|
||||
|
||||
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
|
||||
for (const g of beteiligtGeschlecht) {
|
||||
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
|
||||
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
function showValidation(errors, warnings, mitHinweis) {
|
||||
const el = document.getElementById('message');
|
||||
el.innerHTML = [
|
||||
...errors.map(e => `<div>✕ ${e}</div>`),
|
||||
...warnings.map(w => `<div>⚠ ${w}</div>`),
|
||||
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Spiel starten klicken um fortzufahren.</div>'] : []),
|
||||
].join('');
|
||||
el.className = `message ${errors.length ? 'error' : 'warning'}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
|
||||
el.textContent = icon + text;
|
||||
el.className = `message ${type}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
function renderToys(toys) {
|
||||
const container = document.getElementById('toyList');
|
||||
if (!toys.length) {
|
||||
container.innerHTML = '<p class="no-toys">Keine Toys erforderlich – alle Aufgaben können gespielt werden.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = toys.map(toy => {
|
||||
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
|
||||
return `
|
||||
<label class="toy-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
|
||||
<span>
|
||||
<span class="toy-item-name">${toy.name}</span>
|
||||
${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}
|
||||
</span>
|
||||
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
|
||||
</label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function spielStarten() {
|
||||
const checkedToyIds = new Set(
|
||||
[...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value)
|
||||
);
|
||||
|
||||
// Collect full toy objects for the checked ones (for name display on overview)
|
||||
const toyMap = new Map();
|
||||
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
|
||||
(item.benoetigteToys || []).forEach(t => toyMap.set(t.toyId, t));
|
||||
});
|
||||
const checkedToys = [...checkedToyIds].map(id => toyMap.get(id)).filter(Boolean);
|
||||
sessionStorage.setItem('bdsm-session-toys', JSON.stringify(checkedToys));
|
||||
|
||||
function toyOk(item) {
|
||||
const toys = item.benoetigteToys || [];
|
||||
return toys.length === 0 || toys.every(t => checkedToyIds.has(t.toyId));
|
||||
}
|
||||
|
||||
const gameContent = {
|
||||
aufgaben: allContent.aufgaben.filter(toyOk),
|
||||
strafen: allContent.strafen.filter(toyOk),
|
||||
sperren: allContent.sperren.filter(toyOk),
|
||||
finisher: allContent.finisher.filter(toyOk),
|
||||
};
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
|
||||
const btn = document.querySelector('button[onclick="spielStarten()"]');
|
||||
btn.disabled = true;
|
||||
|
||||
// Für ACCEPTED_OWN Spieler: bereit prüfen und spielerDaten laden
|
||||
const hasOwnDevice = (setup?.mitspieler || []).some(p => p.eigenesGeraet);
|
||||
if (hasOwnDevice) {
|
||||
const setupId = sessionStorage.getItem('bdsm-setup-id');
|
||||
const einladungRes = await fetch(`/bdsm/einladung?setupId=${setupId}`);
|
||||
if (einladungRes.ok) {
|
||||
const einladungen = await einladungRes.json();
|
||||
const nichtBereit = einladungen.filter(e => e.status === 'ACCEPTED_OWN' && !e.bereit);
|
||||
if (nichtBereit.length > 0) {
|
||||
showMessage('Noch nicht alle Mitspieler auf eigenem Gerät haben sich bereit erklärt.', 'error');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
// Spielerdaten der ACCEPTED_OWN Spieler in setup übernehmen
|
||||
for (const p of setup.mitspieler) {
|
||||
if (!p.eigenesGeraet || !p.einladungId) continue;
|
||||
const inv = einladungen.find(e => e.einladungId === p.einladungId);
|
||||
if (inv && inv.spielerDatenJson) {
|
||||
const daten = JSON.parse(inv.spielerDatenJson);
|
||||
p.geschlecht = daten.geschlecht;
|
||||
p.spieltMit = daten.spieltMit || [];
|
||||
p.rollen = daten.rollen || [];
|
||||
p.werkzeuge = daten.werkzeuge || [];
|
||||
p.sperrenVorFinaleAufloesen = daten.sperrenVorFinaleAufloesen !== false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { errors, warnings } = validateContent(gameContent, settings, setup?.mitspieler || []);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showValidation(errors, warnings, false);
|
||||
warnungsAkzeptiert = false;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !warnungsAkzeptiert) {
|
||||
showValidation([], warnings, true);
|
||||
warnungsAkzeptiert = true;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-game', JSON.stringify(gameContent));
|
||||
|
||||
try {
|
||||
// 1. Session anlegen
|
||||
const sessionRes = await fetch('/bdsm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
wahrscheinlichkeitStrafe: settings.wahrscheinlichkeitStrafe,
|
||||
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
|
||||
aufgabenProLevel: settings.aufgabenProLevel,
|
||||
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
|
||||
setupId: sessionStorage.getItem('bdsm-setup-id'),
|
||||
}),
|
||||
});
|
||||
if (sessionRes.status === 409) throw new Error('Du hast bereits ein laufendes BDSM-Spiel. Bitte beende es zuerst.');
|
||||
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
|
||||
const location = sessionRes.headers.get('Location');
|
||||
const sessionId = location.split('/').pop();
|
||||
|
||||
// 2. Mitspieler hinzufügen (setup bereits oben geladen und mit eigenesGeraet-Daten befüllt)
|
||||
for (const p of setup.mitspieler) {
|
||||
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: p.name,
|
||||
geschlecht: p.geschlecht,
|
||||
spieltMit: p.spieltMit,
|
||||
rollen: p.rollen,
|
||||
verfuegbareWerkzeuge: p.werkzeuge,
|
||||
userId: p.userId || null,
|
||||
eigenesGeraet: p.eigenesGeraet || false,
|
||||
sperrenVorFinaleAufloesen: p.sperrenVorFinaleAufloesen !== false,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
|
||||
}
|
||||
|
||||
// 3. Aufgaben setzen
|
||||
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gameContent),
|
||||
});
|
||||
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
|
||||
|
||||
sessionStorage.setItem('bdsm-session-id', sessionId);
|
||||
// Draft löschen, da Spiel jetzt läuft
|
||||
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
|
||||
window.location.href = '/bdsmingame.html';
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load all selected groups, collect content and unique toys
|
||||
(async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
|
||||
const gruppen = await Promise.all(
|
||||
savedGruppen.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
);
|
||||
gruppen.filter(Boolean).forEach(g => {
|
||||
allContent.aufgaben.push(...(g.aufgaben || []));
|
||||
allContent.strafen.push(...(g.strafen || []));
|
||||
allContent.sperren.push(...(g.sperren || []));
|
||||
allContent.finisher.push(...(g.finisher || []));
|
||||
});
|
||||
|
||||
const toyMap = new Map();
|
||||
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
|
||||
(item.benoetigteToys || []).forEach(t => {
|
||||
if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t);
|
||||
});
|
||||
});
|
||||
|
||||
renderToys([...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||
<title>BDSM Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,346 +2,10 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Warten – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.wait-card {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
|
||||
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
|
||||
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
|
||||
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
|
||||
|
||||
.setup-section { margin-bottom: 2rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.card-field { margin-bottom: 1rem; }
|
||||
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
|
||||
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.check-item {
|
||||
display: inline-flex; align-items: flex-start; gap: 0.45rem;
|
||||
background: var(--color-secondary); border: 1px solid transparent;
|
||||
border-radius: 6px; padding: 0.4rem 0.7rem;
|
||||
cursor: pointer; transition: border-color 0.15s; user-select: none;
|
||||
}
|
||||
.check-item.is-checked { border-color: var(--color-primary); }
|
||||
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
|
||||
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
|
||||
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
|
||||
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
|
||||
</style>
|
||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||
<title>BDSM Game</title>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<!-- Konfigurations-Ansicht (für ACCEPTED_OWN Spieler) -->
|
||||
<div class="content" id="configView" style="display:none;">
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Bitte konfiguriere deine Präferenzen, bevor das Spiel startet.</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Deine Daten</h2>
|
||||
<div class="card-field">
|
||||
<label>Geschlecht</label>
|
||||
<div class="check-group" id="geschlechtGroup"></div>
|
||||
<div class="field-error" id="geschlecht-err">Bitte Geschlecht auswählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Spielt mit</label>
|
||||
<div class="check-group" id="spieltMitGroup"></div>
|
||||
<div class="field-error" id="spieltmit-err">Bitte mindestens eine Option wählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Rollen</label>
|
||||
<div class="check-group" id="rollenGroup"></div>
|
||||
<div class="field-error" id="rollen-err">Bitte mindestens eine Rolle wählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Verfügbar</label>
|
||||
<div class="check-group check-group--two-col" id="werkzeugeGroup"></div>
|
||||
<div class="field-error" id="werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Finale</h2>
|
||||
<div class="card-field">
|
||||
<label class="check-item is-checked" id="sperreLabel">
|
||||
<input type="checkbox" id="sperrenAufloesen" checked onchange="toggleSperreWarn()">
|
||||
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
|
||||
</label>
|
||||
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="sperreWarn">
|
||||
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Du könntest im Finale leer ausgehen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" id="configMessage" style="display:none;"></div>
|
||||
<button onclick="bereitMachen()" style="width:100%;">Bereit</button>
|
||||
<button class="secondary" style="width:100%; margin-top:0.75rem;" onclick="abbrechen()">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Warte-Ansicht -->
|
||||
<div class="content wait-card" id="waitView" style="display:none;">
|
||||
<div class="wait-icon">⏳</div>
|
||||
<div class="wait-title">Warte auf Spielstart…</div>
|
||||
<div class="wait-sub" id="sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
|
||||
<div class="message" id="message" style="display:none;"></div>
|
||||
<button class="secondary" onclick="abbrechen()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const einladungId = params.get('id');
|
||||
if (!einladungId) window.location.replace('/userhome.html');
|
||||
|
||||
const GESCHLECHTER = [
|
||||
{ value: 'MAENNLICH', label: 'Männlich' },
|
||||
{ value: 'WEIBLICH', label: 'Weiblich' },
|
||||
{ value: 'DIVERS', label: 'Divers' },
|
||||
];
|
||||
const ROLLEN = [
|
||||
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe – Aktiv' },
|
||||
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe – Passiv' },
|
||||
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung – Aktiv' },
|
||||
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung – Passiv' },
|
||||
];
|
||||
const WERKZEUGE = [
|
||||
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
|
||||
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
|
||||
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
|
||||
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
|
||||
];
|
||||
const WERKZEUGE_DEFAULTS = {
|
||||
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
};
|
||||
|
||||
function buildCheckItems(containerId, items, type) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = items.map(({ value, label, desc }) => `
|
||||
<label class="check-item">
|
||||
<input type="${type}" name="${containerId}" value="${value}">
|
||||
<span>
|
||||
<span class="check-item-label">${label}</span>
|
||||
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
|
||||
</span>
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
function initForm() {
|
||||
buildCheckItems('geschlechtGroup', GESCHLECHTER, 'radio');
|
||||
buildCheckItems('spieltMitGroup', GESCHLECHTER, 'checkbox');
|
||||
buildCheckItems('rollenGroup', ROLLEN, 'checkbox');
|
||||
buildCheckItems('werkzeugeGroup', WERKZEUGE, 'checkbox');
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const input = e.target;
|
||||
if (input.type !== 'checkbox' && input.type !== 'radio') return;
|
||||
if (input.type === 'radio') {
|
||||
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r =>
|
||||
r.closest('.check-item')?.classList.toggle('is-checked', r.checked));
|
||||
if (input.checked && input.name === 'geschlechtGroup') {
|
||||
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
|
||||
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill from profile + bdsm-defaults
|
||||
Promise.all([
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
|
||||
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : null).catch(() => null),
|
||||
]).then(([user, bdsmDefaults]) => {
|
||||
if (user?.geschlecht) {
|
||||
const radio = document.querySelector(`input[name="geschlechtGroup"][value="${user.geschlecht}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
radio.closest('.check-item')?.classList.add('is-checked');
|
||||
const defaults = (bdsmDefaults?.werkzeuge?.length > 0)
|
||||
? bdsmDefaults.werkzeuge
|
||||
: (WERKZEUGE_DEFAULTS[user.geschlecht] || []);
|
||||
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bdsmDefaults?.spieltMit?.length > 0) {
|
||||
bdsmDefaults.spieltMit.forEach(val => {
|
||||
const cb = document.querySelector(`input[name="spieltMitGroup"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
}
|
||||
if (bdsmDefaults?.rollen?.length > 0) {
|
||||
bdsmDefaults.rollen.forEach(val => {
|
||||
const cb = document.querySelector(`input[name="rollenGroup"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getChecked(name) {
|
||||
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
|
||||
}
|
||||
|
||||
function toggleSperreWarn() {
|
||||
const cb = document.getElementById('sperrenAufloesen');
|
||||
const warn = document.getElementById('sperreWarn');
|
||||
const label = document.getElementById('sperreLabel');
|
||||
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
|
||||
if (label) label.classList.toggle('is-checked', cb.checked);
|
||||
}
|
||||
|
||||
function setFieldError(id, show) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function bereitMachen() {
|
||||
const geschlecht = getChecked('geschlechtGroup');
|
||||
const spieltMit = getChecked('spieltMitGroup');
|
||||
const rollen = getChecked('rollenGroup');
|
||||
const werkzeuge = getChecked('werkzeugeGroup');
|
||||
|
||||
setFieldError('geschlecht-err', geschlecht.length === 0);
|
||||
setFieldError('spieltmit-err', spieltMit.length === 0);
|
||||
setFieldError('rollen-err', rollen.length === 0);
|
||||
setFieldError('werkzeuge-err', werkzeuge.length === 0);
|
||||
|
||||
if (!geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) return;
|
||||
|
||||
const sperrenAufloesen = document.getElementById('sperrenAufloesen');
|
||||
const spielerDatenJson = JSON.stringify({
|
||||
geschlecht: geschlecht[0],
|
||||
spieltMit,
|
||||
rollen,
|
||||
werkzeuge,
|
||||
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${einladungId}/spielerdaten`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ spielerDatenJson }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
document.getElementById('configView').style.display = 'none';
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
// Sofort prüfen + Polling starten
|
||||
pruefen();
|
||||
pollInterval = setInterval(pruefen, 3000);
|
||||
} catch (_) {
|
||||
const el = document.getElementById('configMessage');
|
||||
el.textContent = 'Fehler beim Speichern. Bitte erneut versuchen.';
|
||||
el.className = 'message error';
|
||||
el.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
let pollInterval = null;
|
||||
|
||||
async function pruefen() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${einladungId}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'CANCELLED') {
|
||||
stopPoll();
|
||||
zeigeFehler('Die Einladung wurde abgebrochen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.sessionId) {
|
||||
try {
|
||||
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
|
||||
if (mRes.status === 200) {
|
||||
const mData = await mRes.json();
|
||||
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
|
||||
sessionStorage.setItem('bdsm-guest-name', mData.name);
|
||||
sessionStorage.setItem('bdsm-session-id', data.sessionId);
|
||||
sessionStorage.setItem('bdsm-is-guest', 'true');
|
||||
stopPoll();
|
||||
window.location.replace('/bdsmingame.html');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||||
}
|
||||
|
||||
function zeigeFehler(text) {
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
document.getElementById('configView').style.display = 'none';
|
||||
document.getElementById('sub').style.display = 'none';
|
||||
const el = document.getElementById('message');
|
||||
el.textContent = text;
|
||||
el.className = 'message error';
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
async function abbrechen() {
|
||||
stopPoll();
|
||||
await fetch(`/bdsm/einladung/${einladungId}`, { method: 'DELETE' }).catch(() => {});
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
// Init: check if already bereit or if config needed
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${einladungId}`);
|
||||
if (!res.ok) { window.location.replace('/userhome.html'); return; }
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'CANCELLED') {
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
zeigeFehler('Die Einladung wurde abgebrochen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'ACCEPTED_OWN' && !data.bereit) {
|
||||
// Show config form
|
||||
initForm();
|
||||
document.getElementById('configView').style.display = 'block';
|
||||
// Don't start polling yet – user must submit form first
|
||||
} else {
|
||||
// Already bereit or ACCEPTED_HOST → show waiting screen + start poll
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
pruefen();
|
||||
pollInterval = setInterval(pruefen, 3000);
|
||||
}
|
||||
} catch (_) {
|
||||
window.location.replace('/userhome.html');
|
||||
}
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<body>
|
||||
<script>window.location.replace('/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -837,7 +837,7 @@
|
||||
closeBdsmInviteDialog();
|
||||
removeRecvItem(key);
|
||||
if (mode === 'OWN_DEVICE') {
|
||||
window.location.href = `/bdsmwarten.html?id=${key}`;
|
||||
window.location.href = `/neubdsm.html`;
|
||||
}
|
||||
} catch (_) {
|
||||
errEl.textContent = 'Fehler beim Speichern der Antwort.';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
label: 'BDSM Game',
|
||||
icon: '◆',
|
||||
items: [
|
||||
{ href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '/neubdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
|
||||
@@ -118,7 +118,7 @@
|
||||
navAktiv.style.display = '';
|
||||
const ziel = aktiv.sessionId
|
||||
? '/bdsmingame.html'
|
||||
: `/bdsmwarten.html?id=${aktiv.einladungId}`;
|
||||
: `/neubdsm.html`;
|
||||
navAktiv.querySelector('a').href = ziel;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -50,14 +50,6 @@
|
||||
if (e.key === 'Enter') login();
|
||||
});
|
||||
|
||||
async function sha256(text) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
@@ -73,9 +65,11 @@
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
const hash = await sha256(password);
|
||||
const url = `/login?email=${encodeURIComponent(email)}&hash=${encodeURIComponent(hash)}`;
|
||||
const response = await fetch(url, { method: 'GET' });
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const user = await response.json();
|
||||
|
||||
1484
xxxthegame/src/main/resources/static/neubdsm.html
Normal file
1484
xxxthegame/src/main/resources/static/neubdsm.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,14 +42,6 @@
|
||||
if (e.key === 'Enter') register();
|
||||
});
|
||||
|
||||
async function sha256(text) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function register() {
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
@@ -84,11 +76,10 @@
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
const passwordHash = await sha256(password);
|
||||
const response = await fetch('/registration', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, passwordHash, geburtsdatum })
|
||||
body: JSON.stringify({ name, email, password, geburtsdatum })
|
||||
});
|
||||
|
||||
if (response.status === 202) {
|
||||
|
||||
@@ -78,14 +78,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function sha256(text) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const password = document.getElementById('password').value;
|
||||
const passwordConfirm = document.getElementById('passwordConfirm').value;
|
||||
@@ -110,11 +102,10 @@
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
const passwordHash = await sha256(password);
|
||||
const response = await fetch('/password-reset/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, passwordHash })
|
||||
body: JSON.stringify({ token, password })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
|
||||
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.
|
||||
</p>
|
||||
<a href="/bdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
|
||||
<a href="/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
|
||||
</div>
|
||||
|
||||
<div class="game-card">
|
||||
|
||||
Reference in New Issue
Block a user