Verschiebung nach anderem RePo - nun pro Projekt getrennt

This commit is contained in:
2026-04-01 10:41:19 +02:00
commit 7b9eda1d62
1048 changed files with 93351 additions and 0 deletions

36
src/main/java/Ideen.txt Normal file
View File

@@ -0,0 +1,36 @@
Sammeln von Erfahrung
TODO: Im Time Lock, wenn im Spinning Wheel tasks drin sind, dürfen keine sonst keine Tasks gefordert sein und umgekehrt
Ich kann Spieler einladen zu spielen, dann kriegt die Person eine E-Mail und muss bestätigen, dass es diese PErson ist, sie wird dann ins spiel übernommen
-- Falls fall mit Chastity auftritt wird die Spielpartnerin als Keyholder eingetragen, diese Person darf entscheiden, was für ein Lock das wird.
Hier ein paar Ideen für neue Kartentypen:
Bestrafungskarten
- Straf-Karte Lockee muss eine vorher definierte Strafe erfüllen (ähnlich Task, aber negativer konnotiert)
- Extra-Rot Fügt sofort 2-3 rote Karten hinzu, kein Ziehen möglich
Belohnungskarten
- Bonus-Grün LatestOpeningTime wird auf jetzt gesetzt (sofortige Öffnungsmöglichkeit), aber nur kurz gültig (z.B. 30 Minuten Fenster)
- Karten entfernen Lockee darf eine bestimmte Anzahl roter Karten aus dem Deck entfernen
Ereigniskarten
- Würfel-Karte Zufällige Aktion: 1-2 = Freeze, 3-4 = Nichts, 5-6 = Grüne Karte
- Umkehr-Karte Die nächste Karte hat den umgekehrten Effekt (Rot → Grün, Freeze → Beschleunigung)
- Überraschungs-Karte Community, Keyholder oder Zufalls-Task, je nachdem was gerade konfiguriert ist
Zeitkarten
- Verlängerungs-Karte Verschiebt die latestOpeningtime nach hinten (nur bei Keyholder-Locks sinnvoll)
- Countdown-Karte Setzt einen Timer; wenn die Lockee innerhalb der Zeit eine Aufgabe erledigt, wird eine grüne Karte freigeschaltet
- Hygiene-Skip Nächste Hygiene-Öffnung wird übersprungen/gezählt ohne tatsächliche Öffnung
Soziale Karten
- Verifizierungs-Karte Erzwingt sofort eine Verifikations-Session
- Keyholder-Wahl Keyholder entscheidet frei was passiert (Freitext-Eingabe möglich)
- Community-Entscheid Community stimmt nicht über eine Aufgabe ab, sondern darüber was als nächstes passiert (z.B. Freeze vs. Aufgabe)
Die interessantesten wären wohl Würfel und Countdown, da sie mehr Spannung erzeugen ohne den Ablauf zu sehr zu unterbrechen.

View File

@@ -0,0 +1,14 @@
package de.oaa.xxx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class XxxThegameApplication {
public static void main(String[] args) {
SpringApplication.run(XxxThegameApplication.class, args);
}
}

View File

@@ -0,0 +1,563 @@
package de.oaa.xxx.admin;
import java.security.Principal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.feedback.FeedbackEntity;
import de.oaa.xxx.feedback.FeedbackRepository;
import de.oaa.xxx.feedback.FeedbackStatus;
import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeDisplay;
import de.oaa.xxx.games.common.aufgaben.Toy;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabeRepository;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.FinisherRepository;
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
import de.oaa.xxx.games.common.repository.SperreRepository;
import de.oaa.xxx.games.common.repository.StrafeRepository;
import de.oaa.xxx.games.common.repository.ToyRepository;
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity;
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
import de.oaa.xxx.meldung.MeldungEntity;
import de.oaa.xxx.meldung.MeldungRepository;
import de.oaa.xxx.meldung.MeldungStatus;
import de.oaa.xxx.subscription.SubscriptionType;
import de.oaa.xxx.subscription.UserSubscriptionEntity;
import de.oaa.xxx.subscription.UserSubscriptionRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;
@RestController
@RequestMapping("/admin")
@Transactional
public class AdminController {
private final AdminRepository adminRepository;
private final UserRepository userRepository;
private final UserService userService;
private final MeldungRepository meldungRepository;
private final FeedbackRepository feedbackRepository;
private final SupportUserService supportUserService;
private final AufgabenGruppeRepository aufgabenGruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final FinisherRepository finisherRepository;
private final GruppenAboRepository gruppenAboRepository;
private final ToyRepository toyRepository;
private final TTLockConfigRepository ttLockConfigRepository;
private final UserSubscriptionRepository userSubscriptionRepository;
public AdminController(AdminRepository adminRepository, UserRepository userRepository,
UserService userService,
MeldungRepository meldungRepository,
FeedbackRepository feedbackRepository,
SupportUserService supportUserService,
AufgabenGruppeRepository aufgabenGruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
FinisherRepository finisherRepository,
GruppenAboRepository gruppenAboRepository,
ToyRepository toyRepository,
TTLockConfigRepository ttLockConfigRepository,
UserSubscriptionRepository userSubscriptionRepository) {
this.adminRepository = adminRepository;
this.userRepository = userRepository;
this.userService = userService;
this.meldungRepository = meldungRepository;
this.feedbackRepository = feedbackRepository;
this.supportUserService = supportUserService;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.finisherRepository = finisherRepository;
this.gruppenAboRepository = gruppenAboRepository;
this.toyRepository = toyRepository;
this.ttLockConfigRepository = ttLockConfigRepository;
this.userSubscriptionRepository = userSubscriptionRepository;
}
// ── DTOs ─────────────────────────────────────────────────────────────────
record AdminDto(UUID adminId, UUID userId, String userName, AdminRolle rolle, LocalDateTime createdAt) {}
record TtlockConfigDto(String clientId, String clientSecret, String baseUrl) {}
record TtlockConfigRequest(String clientId, String clientSecret, String baseUrl) {}
record MeldungDto(UUID meldungId, UUID melderId, String melderName,
de.oaa.xxx.meldung.MeldungZielTyp zielTyp, UUID zielId,
String grund, LocalDateTime gemeldetAt,
MeldungStatus status, UUID bearbeitetVon, LocalDateTime bearbeitetAt) {}
record CreateAdminRequest(UUID userId, AdminRolle rolle) {}
record StatusRequest(MeldungStatus status) {}
record UserSearchDto(UUID userId, String name) {}
record GiftSubscriptionRequest(UUID userId) {}
record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType,
LocalDate subscribedAt, LocalDate validUntil) {}
record FeedbackDto(UUID feedbackId, String name, String seite, String grund,
String text, LocalDateTime eingegangen, FeedbackStatus status,
String inArbeitVonName) {}
record FeedbackAntwortRequest(String text) {}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private AdminEntity requireAdmin(Principal principal) {
var user = userService.requireUser(principal);
return adminRepository.findByUserId(user.getUserId())
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.FORBIDDEN, "Kein Admin"));
}
private AdminEntity requireSuperAdmin(Principal principal) {
AdminEntity admin = requireAdmin(principal);
if (admin.getRolle() != AdminRolle.SUPERADMIN) {
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.FORBIDDEN, "Kein Superadmin");
}
return admin;
}
private AdminDto toDto(AdminEntity e) {
String name = userRepository.findById(e.getUserId()).map(UserEntity::getName).orElse("?");
return new AdminDto(e.getAdminId(), e.getUserId(), name, e.getRolle(), e.getCreatedAt());
}
private MeldungDto toMeldungDto(MeldungEntity e) {
String melderName = userRepository.findById(e.getMelderId()).map(UserEntity::getName).orElse("?");
return new MeldungDto(e.getMeldungId(), e.getMelderId(), melderName,
e.getZielTyp(), e.getZielId(), e.getGrund(), e.getGemeldetAt(),
e.getStatus(), e.getBearbeitetVon(), e.getBearbeitetAt());
}
// ── /admin/me ────────────────────────────────────────────────────────────
@GetMapping("/me")
public ResponseEntity<AdminDto> me(Principal principal) {
var user = userService.requireUser(principal);
return adminRepository.findByUserId(user.getUserId())
.map(a -> ResponseEntity.ok(toDto(a)))
.orElse(ResponseEntity.status(403).build());
}
// ── Meldungen ────────────────────────────────────────────────────────────
@GetMapping("/meldungen")
public ResponseEntity<List<MeldungDto>> getMeldungen(
@RequestParam(name = "status", required = false) MeldungStatus status,
Principal principal) {
requireAdmin(principal);
List<MeldungEntity> list = status != null
? meldungRepository.findByStatusOrderByGemeldetAtDesc(status)
: meldungRepository.findAllByOrderByGemeldetAtDesc();
return ResponseEntity.ok(list.stream().map(this::toMeldungDto).toList());
}
@PutMapping("/meldungen/{id}")
public ResponseEntity<Void> updateMeldung(@PathVariable("id") UUID id,
@RequestBody StatusRequest body,
Principal principal) {
requireAdmin(principal);
var user = userService.requireUser(principal);
MeldungEntity meldung = meldungRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
meldung.setStatus(body.status());
meldung.setBearbeitetVon(user.getUserId());
meldung.setBearbeitetAt(LocalDateTime.now());
return ResponseEntity.noContent().build();
}
// ── Aufgabengruppen ──────────────────────────────────────────────────────
@GetMapping("/aufgabengruppen")
public ResponseEntity<List<AufgabenGruppe>> getAufgabengruppen(Principal principal) {
requireAdmin(principal);
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository
.findByUserIdIsNull(PageRequest.of(0, 1000)).getContent();
return ResponseEntity.ok(list.stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
}
@PostMapping("/aufgabengruppen")
public ResponseEntity<AufgabenGruppeDisplay> createAufgabengruppe(
@RequestBody AufgabenGruppe gruppe, Principal principal) {
requireAdmin(principal);
gruppe.setUserId(null);
gruppe.setPrivateGruppe(false);
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
aufgabenGruppeRepository.save(entity);
return ResponseEntity.status(201).body(entity.toAufgabenGruppeDisplay());
}
@PutMapping("/aufgabengruppen/{id}")
public ResponseEntity<Void> updateAufgabengruppe(@PathVariable("id") UUID id,
@RequestBody AufgabenGruppe gruppe,
Principal principal) {
requireAdmin(principal);
AufgabenGruppeEntity entity = aufgabenGruppeRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
entity.setName(gruppe.getName());
entity.setBeschreibung(gruppe.getBeschreibung());
entity.setVon(gruppe.getVon());
if (gruppe.getBild() != null) {
entity.setBild(java.util.Base64.getDecoder().decode(gruppe.getBild()));
}
return ResponseEntity.noContent().build();
}
@DeleteMapping("/aufgabengruppen/{id}")
public ResponseEntity<Void> deleteAufgabengruppe(@PathVariable("id") UUID id, Principal principal) {
requireAdmin(principal);
AufgabenGruppeEntity entity = aufgabenGruppeRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
if (entity.getUserId() != null) {
return ResponseEntity.status(403).build(); // Nur System-Gruppen
}
gruppenAboRepository.deleteByAufgabenGruppe(entity);
aufgabeRepository.deleteAll(entity.getAufgaben());
strafeRepository.deleteAll(entity.getStrafen());
sperreRepository.deleteAll(entity.getSperren());
finisherRepository.deleteAll(entity.getFinisher());
aufgabenGruppeRepository.delete(entity);
return ResponseEntity.noContent().build();
}
// ── Item verschieben ─────────────────────────────────────────────────────
@PutMapping("/aufgabengruppen/items/{kind}/{itemId}/move")
public ResponseEntity<Void> moveItem(
@PathVariable("kind") String kind,
@PathVariable("itemId") UUID itemId,
@RequestParam("targetGruppeId") UUID targetGruppeId,
Principal principal) {
requireAdmin(principal);
AufgabenGruppeEntity targetGruppe = aufgabenGruppeRepository.findById(targetGruppeId)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND, "Zielgruppe nicht gefunden"));
switch (kind) {
case "aufgabe" -> aufgabeRepository.findById(itemId).ifPresent(e -> {
e.setAufgabenGruppe(targetGruppe);
aufgabeRepository.save(e);
});
case "strafe" -> strafeRepository.findById(itemId).ifPresent(e -> {
e.setAufgabenGruppe(targetGruppe);
strafeRepository.save(e);
});
case "zeitstrafe" -> sperreRepository.findById(itemId).ifPresent(e -> {
e.setAufgabenGruppe(targetGruppe);
sperreRepository.save(e);
});
case "finisher" -> finisherRepository.findById(itemId).ifPresent(e -> {
e.setAufgabenGruppe(targetGruppe);
finisherRepository.save(e);
});
default -> { return ResponseEntity.badRequest().build(); }
}
return ResponseEntity.noContent().build();
}
// ── Toys ─────────────────────────────────────────────────────────────────
@GetMapping("/toys")
public ResponseEntity<List<Toy>> getToys(Principal principal) {
requireAdmin(principal);
List<ToyEntity> list = toyRepository.findByUserIdIsNull(PageRequest.of(0, 1000, Sort.by(Sort.Direction.ASC, "name"))).getContent();
return ResponseEntity.ok(list.stream().map(ToyEntity::toToy).toList());
}
@PostMapping("/toys")
public ResponseEntity<Toy> createToy(@RequestBody Toy toy, Principal principal) {
requireAdmin(principal);
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNull(toy.getName())) {
return ResponseEntity.status(409).build();
}
toy.setUserId(null);
ToyEntity entity = ToyEntity.create(toy);
toyRepository.save(entity);
return ResponseEntity.status(201).body(entity.toToy());
}
@PutMapping("/toys/{id}")
public ResponseEntity<Void> updateToy(@PathVariable("id") UUID id,
@RequestBody Toy toy, Principal principal) {
requireAdmin(principal);
ToyEntity entity = toyRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(toy.getName(), id)) {
return ResponseEntity.status(409).build();
}
entity.setName(toy.getName());
entity.setBeschreibung(toy.getBeschreibung());
if (toy.getBild() != null) {
entity.setBild(java.util.Base64.getDecoder().decode(toy.getBild()));
}
return ResponseEntity.noContent().build();
}
@DeleteMapping("/toys/{id}")
public ResponseEntity<Void> deleteToy(@PathVariable("id") UUID id, Principal principal) {
requireAdmin(principal);
ToyEntity entity = toyRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
long usage = toyRepository.countAufgabeUsage(id)
+ toyRepository.countStrafeUsage(id)
+ toyRepository.countSperreUsage(id);
if (usage > 0) {
return ResponseEntity.status(409).build();
}
toyRepository.delete(entity);
return ResponseEntity.noContent().build();
}
// ── Benutzer-Suche (nur SUPERADMIN) ──────────────────────────────────────
@GetMapping("/users/search")
public ResponseEntity<List<UserSearchDto>> searchUsers(
@RequestParam String q, Principal principal) {
requireSuperAdmin(principal);
if (q == null || q.isBlank()) return ResponseEntity.ok(List.of());
List<UserEntity> users = userRepository.findByNameContainingIgnoreCase(q.trim());
return ResponseEntity.ok(users.stream()
.filter(u -> !adminRepository.existsByUserId(u.getUserId()))
.limit(20)
.map(u -> new UserSearchDto(u.getUserId(), u.getName()))
.toList());
}
@GetMapping("/users/search/all")
public ResponseEntity<List<UserSearchDto>> searchAllUsers(
@RequestParam String q, Principal principal) {
requireSuperAdmin(principal);
if (q == null || q.isBlank()) return ResponseEntity.ok(List.of());
List<UserEntity> users = userRepository.findByNameContainingIgnoreCase(q.trim());
return ResponseEntity.ok(users.stream()
.limit(20)
.map(u -> new UserSearchDto(u.getUserId(), u.getName()))
.toList());
}
// ── Admin-Verwaltung (nur SUPERADMIN) ────────────────────────────────────
@GetMapping("/admins")
public ResponseEntity<List<AdminDto>> getAdmins(Principal principal) {
requireSuperAdmin(principal);
return ResponseEntity.ok(adminRepository.findAll().stream().map(this::toDto).toList());
}
@PostMapping("/admins")
public ResponseEntity<AdminDto> createAdmin(@RequestBody CreateAdminRequest request, Principal principal) {
requireSuperAdmin(principal);
if (!userRepository.existsById(request.userId())) {
return ResponseEntity.status(404).build();
}
if (adminRepository.existsByUserId(request.userId())) {
return ResponseEntity.status(409).build();
}
AdminEntity entity = AdminEntity.create(request.userId(), request.rolle());
adminRepository.save(entity);
return ResponseEntity.status(201).body(toDto(entity));
}
@DeleteMapping("/admins/{id}")
public ResponseEntity<Void> deleteAdmin(@PathVariable("id") UUID id, Principal principal) {
var requestingUser = userService.requireUser(principal);
requireSuperAdmin(principal);
AdminEntity entity = adminRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
if (entity.getUserId().equals(requestingUser.getUserId())) {
return ResponseEntity.status(400).build(); // Selbstlöschung verhindern
}
adminRepository.delete(entity);
return ResponseEntity.noContent().build();
}
// ── Abonnement verschenken (nur SUPERADMIN) ──────────────────────────────
@GetMapping("/subscriptions")
public ResponseEntity<List<SubscriptionStatusDto>> getAllSubscriptions(Principal principal) {
requireSuperAdmin(principal);
var activeSubscriptions = userSubscriptionRepository
.findByValidUntilGreaterThanEqualOrderByValidUntilDesc(LocalDate.now());
return ResponseEntity.ok(activeSubscriptions.stream().map(sub -> {
String name = userRepository.findById(sub.getUserId()).map(UserEntity::getName).orElse("?");
return new SubscriptionStatusDto(sub.getUserId(), name,
sub.getSubscriptionType().name(), sub.getSubscribedAt(), sub.getValidUntil());
}).toList());
}
@GetMapping("/subscriptions/user/{userId}")
public ResponseEntity<SubscriptionStatusDto> getSubscriptionStatus(
@PathVariable UUID userId, Principal principal) {
requireSuperAdmin(principal);
UserEntity user = userRepository.findById(userId).orElse(null);
if (user == null) return ResponseEntity.notFound().build();
var sub = userSubscriptionRepository
.findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(userId, LocalDate.now())
.orElse(null);
return ResponseEntity.ok(new SubscriptionStatusDto(
userId, user.getName(),
sub != null ? sub.getSubscriptionType().name() : "STANDARD",
sub != null ? sub.getSubscribedAt() : null,
sub != null ? sub.getValidUntil() : null
));
}
@PostMapping("/subscriptions/gift")
public ResponseEntity<SubscriptionStatusDto> giftSubscription(
@RequestBody GiftSubscriptionRequest request, Principal principal) {
requireSuperAdmin(principal);
UserEntity user = userRepository.findById(request.userId()).orElse(null);
if (user == null) return ResponseEntity.notFound().build();
LocalDate today = LocalDate.now();
var existing = userSubscriptionRepository
.findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(request.userId(), today)
.orElse(null);
UserSubscriptionEntity sub = new UserSubscriptionEntity();
sub.setUserId(request.userId());
sub.setSubscriptionType(SubscriptionType.PREMIUM);
sub.setSubscribedAt(today);
// Hat der User bereits ein aktives Abo: Laufzeit um 1 Monat verlängern
sub.setValidUntil(existing != null
? existing.getValidUntil().plusMonths(1)
: today.plusMonths(1));
sub.setCancellableFrom(null); // Geschenk, kein Vertrag
userSubscriptionRepository.save(sub);
return ResponseEntity.ok(new SubscriptionStatusDto(
request.userId(), user.getName(),
sub.getSubscriptionType().name(),
sub.getSubscribedAt(), sub.getValidUntil()
));
}
// ── Feedback ─────────────────────────────────────────────────────────────
private FeedbackDto toFeedbackDto(FeedbackEntity e) {
String inArbeitName = null;
if (e.getInArbeitVon() != null) {
inArbeitName = userRepository.findById(e.getInArbeitVon())
.map(UserEntity::getName).orElse("?");
}
return new FeedbackDto(e.getFeedbackId(), e.getName(), e.getSeite(), e.getGrund(),
e.getText(), e.getEingegangen(), e.getStatus(), inArbeitName);
}
@GetMapping("/feedback")
public ResponseEntity<java.util.Map<String, List<FeedbackDto>>> getFeedback(Principal principal) {
requireAdmin(principal);
List<FeedbackDto> ungelesen = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.UNGELESEN)
.stream().map(this::toFeedbackDto).toList();
List<FeedbackDto> inArbeit = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.IN_ARBEIT)
.stream().map(this::toFeedbackDto).toList();
List<FeedbackDto> beantwortet = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.BEANTWORTET)
.stream().map(this::toFeedbackDto).toList();
return ResponseEntity.ok(java.util.Map.of(
"ungelesen", ungelesen,
"inArbeit", inArbeit,
"beantwortet", beantwortet));
}
@PutMapping("/feedback/{id}/annehmen")
public ResponseEntity<Void> feedbackAnnehmen(@PathVariable("id") UUID id, Principal principal) {
AdminEntity admin = requireAdmin(principal);
FeedbackEntity f = feedbackRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
if (f.getStatus() == FeedbackStatus.IN_ARBEIT) {
// Bereits von jemand anderem in Arbeit Konflikt
return ResponseEntity.status(409).build();
}
f.setStatus(FeedbackStatus.IN_ARBEIT);
f.setInArbeitVon(admin.getUserId());
feedbackRepository.save(f);
return ResponseEntity.noContent().build();
}
@PostMapping("/feedback/{id}/antworten")
public ResponseEntity<Void> feedbackAntworten(@PathVariable("id") UUID id,
@RequestBody FeedbackAntwortRequest body,
Principal principal) {
requireAdmin(principal);
if (body.text() == null || body.text().isBlank()) {
return ResponseEntity.badRequest().build();
}
FeedbackEntity f = feedbackRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
f.setStatus(FeedbackStatus.BEANTWORTET);
feedbackRepository.save(f);
// DM an den Nutzer senden, falls er eingeloggt war
if (f.getUserId() != null) {
String dm = "Ursprüngliche Nachricht\n" + f.getText() + "\n\nAntwort\n" + body.text();
supportUserService.sendDm(f.getUserId(), dm);
}
return ResponseEntity.noContent().build();
}
// ── TTLock-Konfiguration (nur SUPERADMIN) ─────────────────────────────────
@GetMapping("/ttlock")
public ResponseEntity<TtlockConfigDto> getTtlockConfig(Principal principal) {
requireSuperAdmin(principal);
TTLockConfigEntity cfg = ttLockConfigRepository.findById(1L)
.orElse(new TTLockConfigEntity());
return ResponseEntity.ok(new TtlockConfigDto(
cfg.getClientId(),
cfg.getClientSecret(),
cfg.getBaseUrl()
));
}
@PutMapping("/ttlock")
public ResponseEntity<Void> saveTtlockConfig(@RequestBody TtlockConfigRequest body, Principal principal) {
requireSuperAdmin(principal);
TTLockConfigEntity cfg = ttLockConfigRepository.findById(1L)
.orElseGet(TTLockConfigEntity::new);
cfg.setClientId(body.clientId());
cfg.setClientSecret(body.clientSecret());
cfg.setBaseUrl(body.baseUrl());
ttLockConfigRepository.save(cfg);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,38 @@
package de.oaa.xxx.admin;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "admin")
public class AdminEntity {
@Id
@Column
private UUID adminId;
@Column(nullable = false)
private UUID userId;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
private AdminRolle rolle;
@Column(nullable = false)
private LocalDateTime createdAt;
public static AdminEntity create(UUID userId, AdminRolle rolle) {
AdminEntity entity = new AdminEntity();
entity.setAdminId(UUID.randomUUID());
entity.setUserId(userId);
entity.setRolle(rolle);
entity.setCreatedAt(LocalDateTime.now());
return entity;
}
}

View File

@@ -0,0 +1,13 @@
package de.oaa.xxx.admin;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface AdminRepository extends JpaRepository<AdminEntity, UUID> {
Optional<AdminEntity> findByUserId(UUID userId);
boolean existsByUserId(UUID userId);
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.admin;
public enum AdminRolle {
ADMIN, SUPERADMIN
}

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
package de.oaa.xxx.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
public class SchemaMigration implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(SchemaMigration.class);
private final JdbcTemplate jdbc;
public SchemaMigration(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public void run(ApplicationArguments args) {
try {
String columnType = jdbc.queryForObject(
"SELECT DATA_TYPE FROM information_schema.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'verification' AND COLUMN_NAME = 'image'",
String.class);
if ("blob".equalsIgnoreCase(columnType)) {
log.info("Migrating verification.image from BLOB to MEDIUMBLOB");
jdbc.execute("ALTER TABLE verification MODIFY COLUMN image MEDIUMBLOB");
log.info("Migration complete");
}
} catch (Exception e) {
log.warn("Schema migration check failed: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,119 @@
package de.oaa.xxx.config;
import jakarta.servlet.DispatcherType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) ->
response.sendRedirect("/login.html")))
.authorizeHttpRequests(auth -> auth
.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR).permitAll()
.requestMatchers("/").permitAll()
.requestMatchers("/error").permitAll()
.requestMatchers("/api").permitAll()
.requestMatchers("/userhome.html").authenticated()
.requestMatchers("/games/chastity/toys.html").authenticated()
.requestMatchers("/games/bdsm/aufgaben.html").authenticated()
.requestMatchers("/games/chastity/entdecken.html").authenticated()
.requestMatchers("/konto/profile.html").authenticated()
.requestMatchers("/games/vanilla/infovanilla.html").authenticated()
.requestMatchers("/games/bdsm/infobdsm.html").authenticated()
.requestMatchers("/games/chastity/infochastity.html").authenticated()
.requestMatchers("/games/vanilla/sessionvanilla.html").authenticated()
.requestMatchers("/sessionbdsm.html").authenticated()
.requestMatchers("/games/chastity/sessionchastity.html").authenticated()
.requestMatchers("/games/chastity/neulock.html").authenticated()
.requestMatchers("/games/chastity/activelock.html").authenticated()
.requestMatchers("/sessionbdsmtoys.html").authenticated()
.requestMatchers("/sessionbdsmingame.html").authenticated()
.requestMatchers("/games/bdsm/neubdsm.html").authenticated()
.requestMatchers("/games/bdsm/bdsmingame.html").authenticated()
.requestMatchers("/community/personen-suchen.html").authenticated()
.requestMatchers("/community/freunde.html").authenticated()
.requestMatchers("/community/nachrichten.html").authenticated()
.requestMatchers("/community/benutzer.html").authenticated()
.requestMatchers("/community/gruppen.html").authenticated()
.requestMatchers("/community/gruppe.html").authenticated()
.requestMatchers("/community/feed.html").authenticated()
.requestMatchers("/admin/admin.html").authenticated()
.requestMatchers("/games/chastity/communityvotes.html").authenticated()
.requestMatchers("/games/chastity/keyholder.html").authenticated()
.requestMatchers("/games/chastity/keyholder-finden.html").authenticated()
.requestMatchers("/games/chastity/meine-locks.html").authenticated()
.requestMatchers("/games/chastity/entdecken-vorlagen.html").authenticated()
.requestMatchers("/games/chastity/unlock-history.html").authenticated()
.requestMatchers("/games/common/einladungen.html").authenticated()
.requestMatchers("/games/chastity/joinlock.html").authenticated()
.requestMatchers("/community/benachrichtigungen.html").authenticated()
.requestMatchers("/community/abonnements.html").authenticated()
.requestMatchers("/gruppen/**").authenticated()
.requestMatchers("/feed/**").authenticated()
.requestMatchers("/notifications/**").authenticated()
.requestMatchers("/events/**").authenticated()
.requestMatchers("/*.html").permitAll()
.requestMatchers("/**/*.html").permitAll()
.requestMatchers("/help/*.html").permitAll()
.requestMatchers("/css/**").permitAll()
.requestMatchers("/js/**").permitAll()
.requestMatchers("/images/**").permitAll()
.requestMatchers("/img/**").permitAll()
.requestMatchers("/favicon.ico").permitAll()
.requestMatchers("/audio/**").permitAll()
.requestMatchers("/*.png").permitAll()
.requestMatchers("/*.jpg").permitAll()
.requestMatchers("/*.svg").permitAll()
.requestMatchers("/*.webp").permitAll()
.requestMatchers(HttpMethod.GET, "/login").permitAll()
.requestMatchers(HttpMethod.GET, "/ttlock").permitAll()
.requestMatchers(HttpMethod.POST, "/login").permitAll()
.requestMatchers(HttpMethod.GET, "/login/publickey").permitAll()
.requestMatchers(HttpMethod.GET, "/login/logout").permitAll()
.requestMatchers(HttpMethod.POST, "/user").permitAll()
.requestMatchers(HttpMethod.GET, "/registration").permitAll()
.requestMatchers(HttpMethod.POST, "/registration").permitAll()
.requestMatchers(HttpMethod.GET, "/activation").permitAll()
.requestMatchers(HttpMethod.GET, "/activation/**").permitAll()
.requestMatchers(HttpMethod.POST, "/password-reset/request").permitAll()
.requestMatchers(HttpMethod.POST, "/password-reset/confirm").permitAll()
.requestMatchers(HttpMethod.GET, "/email-change/**").permitAll()
.requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/feedback").permitAll()
.requestMatchers(HttpMethod.POST, "/filler").permitAll()
.requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll()
.requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,38 @@
package de.oaa.xxx.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.List;
@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<String> list) {
if (list == null || list.isEmpty()) return null;
try {
return mapper.writeValueAsString(list);
} catch (Exception e) {
return null;
}
}
@Override
public List<String> convertToEntityAttribute(String json) {
if (json == null || json.isBlank()) return List.of();
try {
if (!json.startsWith("[")) {
// Legacy: single base64 string
return List.of(json);
}
return mapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) {
return List.of();
}
}
}

View File

@@ -0,0 +1,54 @@
package de.oaa.xxx.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Serves /css/variables.css dynamically from application.properties theme settings.
* All HTML pages load this first, so changing app.theme.* immediately updates the whole UI.
*/
@RestController
public class ThemeController {
@Value("${app.theme.color-bg:#1a1a2e}")
private String colorBg;
@Value("${app.theme.color-card:#16213e}")
private String colorCard;
@Value("${app.theme.color-primary:#e94560}")
private String colorPrimary;
@Value("${app.theme.color-secondary:#0f3460}")
private String colorSecondary;
@Value("${app.theme.color-text:#eeeeee}")
private String colorText;
@Value("${app.theme.color-muted:#888888}")
private String colorMuted;
@Value("${app.theme.color-success:#2ecc71}")
private String colorSuccess;
/** Mobile breakpoint in px (unitless integer). Used by sidebar.js and lightbox layout. */
@Value("${app.theme.breakpoint-mobile:768}")
private int breakpointMobile;
@GetMapping(value = "/css/variables.css", produces = "text/css")
public String variables() {
return """
:root {
--color-bg: %s;
--color-card: %s;
--color-primary: %s;
--color-secondary: %s;
--color-text: %s;
--color-muted: %s;
--color-success: %s;
--breakpoint-mobile: %d;
}
""".formatted(colorBg, colorCard, colorPrimary, colorSecondary, colorText, colorMuted, colorSuccess, breakpointMobile);
}
}

View File

@@ -0,0 +1,125 @@
package de.oaa.xxx.emailchange;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService;
import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.user.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.security.Principal;
import java.util.UUID;
import java.util.regex.Pattern;
@RestController
@RequestMapping("/email-change")
public class EmailChangeController {
private static final Logger LOGGER = LoggerFactory.getLogger(EmailChangeController.class);
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
private final EmailChangeRepository emailChangeRepository;
private final UserRepository userRepository;
private final RegistrationRepository registrationRepository;
private final MailService mailService;
private final MailTemplateService mailTemplateService;
public EmailChangeController(EmailChangeRepository emailChangeRepository,
UserRepository userRepository,
RegistrationRepository registrationRepository,
MailService mailService,
MailTemplateService mailTemplateService) {
this.emailChangeRepository = emailChangeRepository;
this.userRepository = userRepository;
this.registrationRepository = registrationRepository;
this.mailService = mailService;
this.mailTemplateService = mailTemplateService;
}
record EmailChangeRequest(String newEmail) {}
@PostMapping
public ResponseEntity<Void> requestChange(@RequestBody EmailChangeRequest request, Principal principal) {
String currentEmail = principal.getName();
String newEmail = request.newEmail();
if (newEmail == null || newEmail.isBlank() || !EMAIL_PATTERN.matcher(newEmail).matches()) {
return ResponseEntity.badRequest().build();
}
if (userRepository.findByEmail(newEmail).isPresent()
|| registrationRepository.findByEmail(newEmail).isPresent()) {
return ResponseEntity.status(409).build();
}
// Remove any pending request for this user
emailChangeRepository.findByUserEmail(currentEmail)
.ifPresent(emailChangeRepository::delete);
var user = userRepository.findByEmail(currentEmail);
if (user.isEmpty()) return ResponseEntity.status(401).build();
EmailChangeEntity entity = EmailChangeEntity.create(currentEmail, newEmail);
emailChangeRepository.save(entity);
Email email = new Email();
email.setTitel("Bitte bestätige deine neue E-Mail-Adresse");
email.setEmailAdresse(newEmail);
String confirmLink = baseUrl + "/email-change/" + entity.getTokenId().toString();
email.setText(mailTemplateService.buildEmailChangeMail(user.get().getName(), confirmLink, newEmail));
if (!mailService.send(email)) {
emailChangeRepository.delete(entity);
return ResponseEntity.internalServerError().build();
}
return ResponseEntity.status(202).build();
}
@GetMapping("/{token}")
public void confirm(@PathVariable String token, HttpServletResponse response) throws IOException {
UUID tokenId;
try {
tokenId = UUID.fromString(token);
} catch (IllegalArgumentException e) {
response.sendRedirect("/login.html");
return;
}
var entity = emailChangeRepository.findById(tokenId);
if (entity.isEmpty()) {
response.sendRedirect("/login.html");
return;
}
var user = userRepository.findByEmail(entity.get().getUserEmail());
if (user.isPresent()) {
user.get().setEmail(entity.get().getNewEmail());
userRepository.save(user.get());
LOGGER.info("E-Mail geändert von {} zu {}", entity.get().getUserEmail(), entity.get().getNewEmail());
}
emailChangeRepository.delete(entity.get());
// Clear JWT cookie so user must log in with new email
ResponseCookie cookie = ResponseCookie.from("jwt", "")
.httpOnly(true)
.sameSite("Strict")
.path("/")
.maxAge(0)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
response.sendRedirect("/login.html?emailChanged=1");
}
}

View File

@@ -0,0 +1,45 @@
package de.oaa.xxx.emailchange;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "email_change")
public class EmailChangeEntity {
@Id
@Column
private UUID tokenId;
@Column
private String userEmail;
@Column
private String newEmail;
@Column
private LocalDateTime createdAt;
@Override
public String toString() {
return "EmailChangeEntity[tokenId=" + tokenId + ", userEmail=" + userEmail + ", newEmail=" + newEmail + ", createdAt=" + createdAt + "]";
}
public static EmailChangeEntity create(String userEmail, String newEmail) {
EmailChangeEntity entity = new EmailChangeEntity();
entity.setTokenId(UUID.randomUUID());
entity.setUserEmail(userEmail);
entity.setNewEmail(newEmail);
entity.setCreatedAt(LocalDateTime.now());
return entity;
}
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.emailchange;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface EmailChangeRepository extends JpaRepository<EmailChangeEntity, UUID> {
Optional<EmailChangeEntity> findByUserEmail(String userEmail);
}

View File

@@ -0,0 +1,408 @@
package de.oaa.xxx.feed;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.feed.dto.FeedItemDto;
import de.oaa.xxx.feed.dto.FeedPostRequest;
import de.oaa.xxx.feed.entity.FeedPostEntity;
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
import de.oaa.xxx.feed.entity.FeedPostVoteEntity;
import de.oaa.xxx.feed.repository.FeedPostLikeRepository;
import de.oaa.xxx.feed.repository.FeedPostOptionRepository;
import de.oaa.xxx.feed.repository.FeedPostRepository;
import de.oaa.xxx.feed.repository.FeedPostVoteRepository;
import de.oaa.xxx.gruppe.BeitragTyp;
import de.oaa.xxx.gruppe.dto.UmfrageOptionDto;
import de.oaa.xxx.gruppe.entity.GruppenbeitragEntity;
import de.oaa.xxx.gruppe.entity.UmfrageStimmeEntity;
import de.oaa.xxx.gruppe.repository.GruppeRepository;
import de.oaa.xxx.gruppe.repository.GruppenbeitragLikeRepository;
import de.oaa.xxx.gruppe.repository.GruppenbeitragRepository;
import de.oaa.xxx.gruppe.repository.GruppenmitgliedRepository;
import de.oaa.xxx.gruppe.repository.UmfrageOptionRepository;
import de.oaa.xxx.gruppe.repository.UmfrageStimmeRepository;
import de.oaa.xxx.social.LikeService;
import de.oaa.xxx.social.entity.FriendshipEntity;
import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/feed")
public class FeedController {
private static final Logger LOGGER = LoggerFactory.getLogger(FeedController.class);
private final FeedPostRepository feedPostRepository;
private final FeedPostLikeRepository feedPostLikeRepository;
private final FeedPostOptionRepository feedPostOptionRepository;
private final FeedPostVoteRepository feedPostVoteRepository;
private final FriendshipRepository friendshipRepository;
private final GruppenmitgliedRepository mitgliedRepository;
private final GruppenbeitragRepository gruppenbeitragRepository;
private final UmfrageOptionRepository umfrageOptionRepository;
private final UmfrageStimmeRepository umfrageStimmeRepository;
private final GruppenbeitragLikeRepository gruppenbeitragLikeRepository;
private final GruppeRepository gruppeRepository;
private final KommentarRepository kommentarRepository;
private final UserRepository userRepository;
private final UserService userService;
private final LikeService likeService;
public FeedController(FeedPostRepository feedPostRepository,
FeedPostLikeRepository feedPostLikeRepository,
FeedPostOptionRepository feedPostOptionRepository,
FeedPostVoteRepository feedPostVoteRepository,
FriendshipRepository friendshipRepository,
GruppenmitgliedRepository mitgliedRepository,
GruppenbeitragRepository gruppenbeitragRepository,
UmfrageOptionRepository umfrageOptionRepository,
UmfrageStimmeRepository umfrageStimmeRepository,
GruppenbeitragLikeRepository gruppenbeitragLikeRepository,
GruppeRepository gruppeRepository,
KommentarRepository kommentarRepository,
UserRepository userRepository,
UserService userService,
LikeService likeService) {
this.feedPostRepository = feedPostRepository;
this.feedPostLikeRepository = feedPostLikeRepository;
this.feedPostOptionRepository = feedPostOptionRepository;
this.feedPostVoteRepository = feedPostVoteRepository;
this.friendshipRepository = friendshipRepository;
this.mitgliedRepository = mitgliedRepository;
this.gruppenbeitragRepository = gruppenbeitragRepository;
this.umfrageOptionRepository = umfrageOptionRepository;
this.umfrageStimmeRepository = umfrageStimmeRepository;
this.gruppenbeitragLikeRepository = gruppenbeitragLikeRepository;
this.gruppeRepository = gruppeRepository;
this.kommentarRepository = kommentarRepository;
this.userRepository = userRepository;
this.userService = userService;
this.likeService = likeService;
}
record FeedPage(List<FeedItemDto> posts, boolean hasMore) {}
record VoteRequest(UUID optionId) {}
// ── POST /feed/posts ──
@PostMapping("/posts")
public ResponseEntity<FeedItemDto> createPost(@RequestBody FeedPostRequest req, Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
BeitragTyp typ;
try {
typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
FeedPostEntity post = new FeedPostEntity();
post.setPostId(UUID.randomUUID());
post.setAuthorId(myId);
post.setText(req.text().trim());
post.setBeitragTyp(typ);
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
post.setBilder(req.bilder() != null ? req.bilder() : List.of());
post.setPublic(req.isPublic());
post.setCreatedAt(LocalDateTime.now());
feedPostRepository.save(post);
LOGGER.info("User {} hat Feed-Post {} erstellt (Typ: {}, public: {})", myId, post.getPostId(), typ, post.isPublic());
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
for (int i = 0; i < req.optionen().size(); i++) {
String optText = req.optionen().get(i);
if (optText == null || optText.isBlank()) continue;
FeedPostOptionEntity opt = new FeedPostOptionEntity();
opt.setOptionId(UUID.randomUUID());
opt.setPostId(post.getPostId());
opt.setText(optText.trim());
opt.setReihenfolge(i);
feedPostOptionRepository.save(opt);
}
}
return ResponseEntity.status(201).body(toFeedItemDtoFromPost(post, myId));
}
// ── GET /feed/mine ──
@GetMapping("/mine")
public ResponseEntity<FeedPage> getMyFeed(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
// Collect friend IDs
List<UUID> friendIds = friendshipRepository
.findFriends(myId, FriendshipEntity.Status.ACCEPTED)
.stream()
.map(f -> f.getSenderId().equals(myId) ? f.getReceiverId() : f.getSenderId())
.toList();
List<UUID> authorIds = new ArrayList<>(friendIds);
authorIds.add(myId);
// Collect group IDs
List<UUID> gruppeIds = mitgliedRepository.findByUserId(myId)
.stream()
.map(m -> m.getGruppeId())
.toList();
LocalDateTime since = LocalDateTime.now().minusDays(90);
// Fetch feed posts from friends + self
List<FeedPostEntity> feedPosts = feedPostRepository
.findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(authorIds, since);
// Fetch gruppe posts
List<GruppenbeitragEntity> gruppePosts = gruppeIds.isEmpty() ? List.of() :
gruppenbeitragRepository.findByGruppeIdInAndCreatedAtAfterOrderByCreatedAtDesc(gruppeIds, since);
// Merge, convert, sort
List<FeedItemDto> merged = Stream.concat(
feedPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId)),
gruppePosts.stream().map(b -> toFeedItemDtoFromGruppe(b, myId))
).sorted(Comparator.comparing(FeedItemDto::createdAt).reversed()).toList();
int from = page * size;
int to = Math.min(from + size, merged.size());
List<FeedItemDto> items = from < merged.size() ? merged.subList(from, to) : List.of();
boolean hasMore = to < merged.size();
return ResponseEntity.ok(new FeedPage(items, hasMore));
}
// ── GET /feed/public ──
@GetMapping("/public")
public ResponseEntity<FeedPage> getPublicFeed(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
Slice<FeedPostEntity> slice = feedPostRepository
.findByIsPublicTrueOrderByCreatedAtDesc(PageRequest.of(page, size));
List<FeedItemDto> items = slice.getContent().stream()
.map(p -> toFeedItemDtoFromPost(p, myId))
.toList();
return ResponseEntity.ok(new FeedPage(items, slice.hasNext()));
}
// ── GET /feed/user/{userId} ──
@GetMapping("/user/{userId}")
public ResponseEntity<FeedPage> getUserPosts(@PathVariable UUID userId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
PageRequest pageable = PageRequest.of(page, size);
List<FeedPostEntity> posts;
if (myId.equals(userId)) {
posts = feedPostRepository.findByAuthorIdOrderByCreatedAtDesc(userId, pageable);
} else {
posts = feedPostRepository.findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(userId, pageable);
}
// Check if there's a next page
PageRequest nextPageable = PageRequest.of(page + 1, size);
List<FeedPostEntity> nextPage;
if (myId.equals(userId)) {
nextPage = feedPostRepository.findByAuthorIdOrderByCreatedAtDesc(userId, nextPageable);
} else {
nextPage = feedPostRepository.findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(userId, nextPageable);
}
List<FeedItemDto> items = posts.stream()
.map(p -> toFeedItemDtoFromPost(p, myId))
.toList();
return ResponseEntity.ok(new FeedPage(items, !nextPage.isEmpty()));
}
// ── POST /feed/posts/{id}/like ──
@PostMapping("/posts/{id}/like")
public ResponseEntity<Void> toggleLike(@PathVariable UUID id, Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
if (feedPostRepository.findById(id).isEmpty()) return ResponseEntity.notFound().build();
likeService.toggleFeedPostLike(id, myId);
return ResponseEntity.ok().build();
}
// ── POST /feed/posts/{id}/vote ──
@PostMapping("/posts/{id}/vote")
public ResponseEntity<Void> vote(@PathVariable UUID id,
@RequestBody VoteRequest req,
Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
var postOpt = feedPostRepository.findById(id);
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
FeedPostEntity post = postOpt.get();
var optOpt = feedPostOptionRepository.findById(req.optionId());
if (optOpt.isEmpty() || !optOpt.get().getPostId().equals(id))
return ResponseEntity.badRequest().build();
boolean isMultiChoice = Boolean.TRUE.equals(post.getMultiChoice());
var existingVote = feedPostVoteRepository.findByOptionIdAndUserId(req.optionId(), myId);
if (existingVote.isPresent()) {
feedPostVoteRepository.delete(existingVote.get());
return ResponseEntity.ok().build();
}
if (!isMultiChoice) {
List<FeedPostVoteEntity> existing = feedPostVoteRepository.findByPostIdAndUserId(id, myId);
feedPostVoteRepository.deleteAll(existing);
}
FeedPostVoteEntity vote = new FeedPostVoteEntity();
vote.setStimmeId(UUID.randomUUID());
vote.setOptionId(req.optionId());
vote.setPostId(id);
vote.setUserId(myId);
feedPostVoteRepository.save(vote);
LOGGER.debug("User {} hat für Option {} in Feed-Post {} gestimmt", myId, req.optionId(), id);
return ResponseEntity.ok().build();
}
// ── DELETE /feed/posts/{id} ──
@DeleteMapping("/posts/{id}")
public ResponseEntity<Void> deletePost(@PathVariable UUID id, Principal principal) {
UUID myId = resolveMyId(principal);
if (myId == null) return ResponseEntity.status(401).build();
var postOpt = feedPostRepository.findById(id);
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
FeedPostEntity post = postOpt.get();
if (!post.getAuthorId().equals(myId)) return ResponseEntity.status(403).build();
feedPostVoteRepository.deleteByPostId(id);
feedPostOptionRepository.deleteByPostId(id);
feedPostLikeRepository.deleteByPostId(id);
var kommentare = kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("FEED_POST", id);
kommentarRepository.deleteAll(kommentare);
feedPostRepository.delete(post);
LOGGER.info("User {} hat Feed-Post {} gelöscht", myId, id);
return ResponseEntity.noContent().build();
}
// ── Helpers ──
private UUID resolveMyId(Principal principal) {
if (principal == null) return null;
return userService.requireUser(principal).getUserId();
}
private FeedItemDto toFeedItemDtoFromPost(FeedPostEntity p, UUID myId) {
UserEntity author = userRepository.findById(p.getAuthorId()).orElse(null);
long likeCount = feedPostLikeRepository.countByPostId(p.getPostId());
boolean likedByMe = feedPostLikeRepository.findByPostIdAndUserId(p.getPostId(), myId).isPresent();
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("FEED_POST", p.getPostId());
List<UmfrageOptionDto> optionen = List.of();
List<UUID> myVoteOptionIds = List.of();
if (p.getBeitragTyp() == BeitragTyp.UMFRAGE) {
optionen = feedPostOptionRepository.findByPostIdOrderByReihenfolge(p.getPostId())
.stream()
.map(o -> new UmfrageOptionDto(o.getOptionId(), o.getText(), o.getReihenfolge(),
feedPostVoteRepository.countByOptionId(o.getOptionId())))
.toList();
myVoteOptionIds = feedPostVoteRepository.findByPostIdAndUserId(p.getPostId(), myId)
.stream()
.map(FeedPostVoteEntity::getOptionId)
.toList();
}
return new FeedItemDto(
p.getPostId(), "FEED",
null, null,
p.getAuthorId(),
author != null ? author.getName() : "Unbekannt",
author != null ? author.getProfilePicture() : null,
p.getBeitragTyp().name(), p.getText(), p.getMultiChoice(), p.getBilder(),
p.getCreatedAt(),
likeCount, likedByMe, kommentarCount,
optionen, myVoteOptionIds,
p.isPublic()
);
}
private FeedItemDto toFeedItemDtoFromGruppe(GruppenbeitragEntity b, UUID myId) {
UserEntity author = userRepository.findById(b.getAuthorId()).orElse(null);
long likeCount = gruppenbeitragLikeRepository.countByBeitragId(b.getBeitragId());
boolean likedByMe = gruppenbeitragLikeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId).isPresent();
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("GROUP_POST", b.getBeitragId());
String gruppeName = gruppeRepository.findById(b.getGruppeId())
.map(g -> g.getName())
.orElse("Gruppe");
List<UmfrageOptionDto> optionen = List.of();
List<UUID> myVoteOptionIds = List.of();
if (b.getBeitragTyp() == BeitragTyp.UMFRAGE) {
optionen = umfrageOptionRepository.findByBeitragIdOrderByReihenfolge(b.getBeitragId())
.stream()
.map(o -> new UmfrageOptionDto(o.getOptionId(), o.getText(), o.getReihenfolge(),
umfrageStimmeRepository.countByOptionId(o.getOptionId())))
.toList();
myVoteOptionIds = umfrageStimmeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId)
.stream()
.map(UmfrageStimmeEntity::getOptionId)
.toList();
}
return new FeedItemDto(
b.getBeitragId(), "GROUP",
b.getGruppeId(), gruppeName,
b.getAuthorId(),
author != null ? author.getName() : "Unbekannt",
author != null ? author.getProfilePicture() : null,
b.getBeitragTyp().name(), b.getText(), b.getMultiChoice(), b.getBilder(),
b.getCreatedAt(),
likeCount, likedByMe, kommentarCount,
optionen, myVoteOptionIds,
false
);
}
}

View File

@@ -0,0 +1,28 @@
package de.oaa.xxx.feed.dto;
import de.oaa.xxx.gruppe.dto.UmfrageOptionDto;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public record FeedItemDto(
UUID postId,
String postType, // "FEED" | "GROUP"
UUID gruppeId,
String gruppeName,
UUID authorId,
String authorName,
String authorPicture,
String beitragTyp,
String text,
Boolean multiChoice,
List<String> bilder,
LocalDateTime createdAt,
long likeCount,
boolean likedByMe,
long kommentarCount,
List<UmfrageOptionDto> optionen,
List<UUID> myVoteOptionIds,
boolean isPublic
) {}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.feed.dto;
import java.util.List;
public record FeedPostRequest(
String beitragTyp,
String text,
Boolean multiChoice,
List<String> optionen,
List<String> bilder,
boolean isPublic
) {}

View File

@@ -0,0 +1,45 @@
package de.oaa.xxx.feed.entity;
import de.oaa.xxx.config.StringListConverter;
import de.oaa.xxx.gruppe.BeitragTyp;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "feed_post")
public class FeedPostEntity {
@Id
@Column
private UUID postId;
@Column(nullable = false)
private UUID authorId;
@Column(columnDefinition = "TEXT")
private String text;
@Convert(converter = StringListConverter.class)
@Column(name = "bild", columnDefinition = "MEDIUMTEXT")
private List<String> bilder;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private BeitragTyp beitragTyp;
@Column
private Boolean multiChoice;
@Column(nullable = false)
private boolean isPublic;
@Column(nullable = false)
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.feed.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "feed_post_like", uniqueConstraints = {
@UniqueConstraint(columnNames = {"postId", "userId"})
})
public class FeedPostLikeEntity {
@Id
@Column
private UUID likeId;
@Column(nullable = false)
private UUID postId;
@Column(nullable = false)
private UUID userId;
@Column(nullable = false)
private LocalDateTime likedAt;
}

View File

@@ -0,0 +1,27 @@
package de.oaa.xxx.feed.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "feed_post_option")
public class FeedPostOptionEntity {
@Id
@Column
private UUID optionId;
@Column(nullable = false)
private UUID postId;
@Column(nullable = false)
private String text;
@Column(nullable = false)
private int reihenfolge;
}

View File

@@ -0,0 +1,27 @@
package de.oaa.xxx.feed.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "feed_post_vote")
public class FeedPostVoteEntity {
@Id
@Column
private UUID stimmeId;
@Column(nullable = false)
private UUID optionId;
@Column(nullable = false)
private UUID postId;
@Column(nullable = false)
private UUID userId;
}

View File

@@ -0,0 +1,18 @@
package de.oaa.xxx.feed.repository;
import de.oaa.xxx.feed.entity.FeedPostLikeEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.UUID;
public interface FeedPostLikeRepository extends JpaRepository<FeedPostLikeEntity, UUID> {
Optional<FeedPostLikeEntity> findByPostIdAndUserId(UUID postId, UUID userId);
long countByPostId(UUID postId);
@Transactional
void deleteByPostId(UUID postId);
}

View File

@@ -0,0 +1,16 @@
package de.oaa.xxx.feed.repository;
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
public interface FeedPostOptionRepository extends JpaRepository<FeedPostOptionEntity, UUID> {
List<FeedPostOptionEntity> findByPostIdOrderByReihenfolge(UUID postId);
@Transactional
void deleteByPostId(UUID postId);
}

View File

@@ -0,0 +1,25 @@
package de.oaa.xxx.feed.repository;
import de.oaa.xxx.feed.entity.FeedPostEntity;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public interface FeedPostRepository extends JpaRepository<FeedPostEntity, UUID> {
Slice<FeedPostEntity> findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);
List<FeedPostEntity> findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(List<UUID> authorIds, LocalDateTime since);
List<FeedPostEntity> findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(UUID authorId, Pageable pageable);
List<FeedPostEntity> findByAuthorIdOrderByCreatedAtDesc(UUID authorId, Pageable pageable);
@Transactional
void deleteByAuthorId(UUID authorId);
}

View File

@@ -0,0 +1,21 @@
package de.oaa.xxx.feed.repository;
import de.oaa.xxx.feed.entity.FeedPostVoteEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface FeedPostVoteRepository extends JpaRepository<FeedPostVoteEntity, UUID> {
List<FeedPostVoteEntity> findByPostIdAndUserId(UUID postId, UUID userId);
Optional<FeedPostVoteEntity> findByOptionIdAndUserId(UUID optionId, UUID userId);
long countByOptionId(UUID optionId);
@Transactional
void deleteByPostId(UUID postId);
}

View File

@@ -0,0 +1,89 @@
package de.oaa.xxx.feedback;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
@RestController
@RequestMapping("/api/feedback")
public class FeedbackController {
private final MailService mailService;
private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository;
private final SupportUserService supportUserService;
public FeedbackController(MailService mailService,
FeedbackRepository feedbackRepository,
UserRepository userRepository,
SupportUserService supportUserService) {
this.mailService = mailService;
this.feedbackRepository = feedbackRepository;
this.userRepository = userRepository;
this.supportUserService = supportUserService;
}
record FeedbackRequest(String name, String seite, String grund, String text) {}
@PostMapping
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req, Principal principal) {
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
return ResponseEntity.badRequest().build();
}
// Eingeloggten User ermitteln (optional)
UUID userId = null;
if (principal != null) {
userId = userRepository.findByEmail(principal.getName())
.map(u -> u.getUserId()).orElse(null);
}
FeedbackEntity entity = new FeedbackEntity();
entity.setUserId(userId);
entity.setName(req.name());
entity.setSeite(req.seite());
entity.setGrund(req.grund());
entity.setText(req.text());
entity.setEingegangen(LocalDateTime.now());
entity.setStatus(FeedbackStatus.UNGELESEN);
feedbackRepository.save(entity);
// Bestätigungs-DM an eingeloggten Nutzer
if (userId != null) {
supportUserService.sendDm(userId,
"Vielen Dank für dein Feedback! ✉️\n\n" +
"Wir haben deine Nachricht erhalten und werden uns so schnell wie möglich darum kümmern.\n\n" +
"Bitte antworte nicht auf diese Nachricht du kannst uns jederzeit über " +
"Kontakt & Feedback erneut erreichen.");
}
try {
Email email = new Email();
email.setEmailAdresse("kontakt@xxx-sphere.de");
email.setTitel("[xXx Sphere] " + esc(req.grund()));
email.setText(
"<b>Von:</b> " + esc(req.name()) + "<br>" +
"<b>Seite:</b> " + esc(req.seite()) + "<br>" +
"<b>Grund:</b> " + esc(req.grund()) + "<br><br>" +
"<b>Nachricht:</b><br>" + esc(req.text()).replace("\n", "<br>")
);
mailService.send(email);
} catch (Exception e) {
// Mail-Server nicht erreichbar Eintrag ist bereits gespeichert
}
return ResponseEntity.ok().build();
}
private String esc(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
}

View File

@@ -0,0 +1,38 @@
package de.oaa.xxx.feedback;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "feedback")
@Getter
@Setter
public class FeedbackEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID feedbackId;
/** Eingeloggter Nutzer null wenn Gast */
private UUID userId;
private String name;
private String seite;
private String grund;
@Column(columnDefinition = "TEXT")
private String text;
private LocalDateTime eingegangen;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private FeedbackStatus status = FeedbackStatus.UNGELESEN;
/** Admin-UserId der den Eintrag in Arbeit genommen hat */
private UUID inArbeitVon;
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.feedback;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface FeedbackRepository extends JpaRepository<FeedbackEntity, UUID> {
List<FeedbackEntity> findByStatusOrderByEingegangenDesc(FeedbackStatus status);
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.feedback;
public enum FeedbackStatus {
UNGELESEN,
IN_ARBEIT,
BEANTWORTET
}

View File

@@ -0,0 +1,28 @@
package de.oaa.xxx.games.bdsm;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AktiveSperre {
private UUID aktiveSperreId;
private BdsmMitspieler mitspieler;
private Integer minuten;
private LocalDateTime startzeit;
private LocalDateTime endzeit;
private List<Werkzeug> fuer;
private String releaseText;
@Override
public String toString() {
return "AktiveSperre[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null)
+ ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]";
}
}

View File

@@ -0,0 +1,25 @@
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
public class AufgabeAnzeige {
private String nameAktiverMitspieler;
private String aufgabeText;
private Integer timer;
private Callback callback;
private Integer level;
private UUID mitspielerId;
private boolean eigenesGeraet;
@Override
public String toString() {
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
}
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.games.bdsm;
public enum AufgabeArt {
AUFGABE,
STRAFE,
SPERRE;
}

View File

@@ -0,0 +1,32 @@
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
public class BdsmGame {
private UUID sessionId;
private UUID userId;
private UUID setupId;
private Integer wahrscheinlichkeitSperre;
private Integer wahrscheinlichkeitStrafe;
private Integer aufgabenProLevel;
private Double zeitfaktorZeitstrafen;
private Integer level;
private Integer aufgabenAufAktuellemLevel;
private LocalDateTime startZeit;
private LocalDateTime letzteAktivitaet;
@Override
public String toString() {
return "Session[sessionId=" + sessionId + ", userId=" + userId
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + "]";
}
}

View File

@@ -0,0 +1,282 @@
package de.oaa.xxx.games.bdsm;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.common.aufgaben.Aufgabe;
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.Sperre;
import de.oaa.xxx.games.common.aufgaben.Strafe;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
public class BdsmGameDurchfuehren {
private final AufgabenList aufgabenList;
private final List<BdsmMitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel;
private int level;
private int aufgabenAufAktuellemLevel;
public BdsmGameDurchfuehren(BdsmGameEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
}
public AufgabeAnzeige getNext() {
checkLevel();
if (level == 6) {
return null;
}
int nextInt = new Random().nextInt(1, 100);
// Sonderfälle: bleiben wie bisher (inkl. eigener interner Fallbacks)
if (nextInt == 1) {
AufgabeAnzeige anzeige = findUltimativeStrafe();
if (anzeige != null) return anzeige;
} else if (nextInt == 2) {
AufgabeAnzeige anzeige = findSperreVerlaengern();
if (anzeige != null) return anzeige;
} else {
// Reihenfolge der Kategorien: gewürfelte zuerst, dann die anderen
List<Supplier<AufgabeAnzeige>> reihenfolge;
if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
reihenfolge = List.of(this::findeAufgabe, this::findeStrafe, this::findeSperre);
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
reihenfolge = List.of(this::findeStrafe, this::findeAufgabe, this::findeSperre);
} else {
reihenfolge = List.of(this::findeSperre, this::findeStrafe, this::findeAufgabe);
}
for (Supplier<AufgabeAnzeige> finder : reihenfolge) {
AufgabeAnzeige anzeige = finder.get();
if (anzeige != null) return anzeige;
}
}
// Echtes Fallback: nur wenn wirklich keine Kategorie eine Aufgabe liefert
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
BdsmMitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
anzeige.setTimer(120);
return anzeige;
}
public void backToLvl5() {
this.level = 5;
this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>();
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
var finishers = aufgabenList.getFinisher().stream()
.filter(finisher -> geschlecht == finisher.getGeschlecht() && finisher.isAufgabePassend(partner, cumming))
.toList();
if (!finishers.isEmpty()) {
var aufgabe = finishers.get(new Random().nextInt(finishers.size()));
var anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
setMitspielerInfo(anzeige, cumming);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
cumming.getName(), partner != null ? partner.getName() : ""));
list.add(anzeige);
} else {
var anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
anzeige.setAufgabeText(cumming.getName() + "geht heute leider leer aus...");
list.add(anzeige);
}
});
});
return list;
}
private void checkLevel() {
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0;
level++;
}
}
private void setMitspielerInfo(AufgabeAnzeige anzeige, BdsmMitspieler aktiv) {
if (aktiv != null) {
anzeige.setMitspielerId(aktiv.getId());
anzeige.setEigenesGeraet(aktiv.isEigenesGeraet());
}
}
private AufgabeAnzeige findUltimativeStrafe() {
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
BdsmMitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
String text = "{AKTIV}, verschnüre {PASSIV} fachmännisch inkl. KG, Plugs, Knebel, Augenbinde und was dir sonst einfällt. Nutze die Ruhe für was auch immer du möchtest.";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200));
return anzeige;
}
}
return findeStrafe();
}
private AufgabeAnzeige findSperreVerlaengern() {
if (!aktiveSperren.isEmpty()) {
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
BdsmMitspieler passiv = sperre.getMitspieler();
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
if (aktiv != null) {
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
callback.setFaktor(new Random().nextInt(2, 4));
callback.setSpielerId(passiv.getId());
anzeige.setCallback(callback);
return anzeige;
}
}
return findeSperre();
}
private AufgabeAnzeige findeAufgabe() {
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aktiv != null) {
BdsmMitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
if (passiv != null) {
List<Aufgabe> list = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
} else {
anzeige.setTimer(aufgabe.getSekundenVon());
}
}
return anzeige;
}
}
}
return null;
}
private AufgabeAnzeige findeStrafe() {
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
BdsmMitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Strafe> list = aufgabenList.getStrafen().stream()
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Strafe strafe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
if (strafe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
} else {
anzeige.setTimer(strafe.getSekundenVon());
}
}
return anzeige;
}
}
}
return null;
}
private AufgabeAnzeige findeSperre() {
BdsmMitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
BdsmMitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Sperre> list = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.isAufgabePassend(passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Sperre sperre = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
callback.setSpielerId(passiv.getId());
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
anzeige.setCallback(callback);
return anzeige;
}
}
}
return null;
}
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
}
private BdsmMitspieler findeMitspielerMitRolle(RolleEnum rolle) {
List<BdsmMitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
private BdsmMitspieler findeMitspielerMitRolle(RolleEnum rolle, BdsmMitspieler gegenspieler) {
if (gegenspieler == null) return findeMitspielerMitRolle(rolle);
List<BdsmMitspieler> list = mitspieler.stream()
.filter(m -> m != gegenspieler)
.filter(m -> m.isPassenderSpielpartner(gegenspieler))
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
public int getAufgabenAufAktuellemLevel() {
return aufgabenAufAktuellemLevel;
}
public int getLevel() {
return level;
}
}

View File

@@ -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.setTaskMode(template.getTaskMode());
int codeLines = template.getUnlockCodeLength() != null ? template.getUnlockCodeLength() : 5;
newLock.setUnlockCodeLength(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.",
"/games/chastity/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);
}
}

View File

@@ -0,0 +1,44 @@
package de.oaa.xxx.games.bdsm;
import java.util.List;
import java.util.UUID;
import de.oaa.xxx.games.common.aufgaben.CommonMitspieler;
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BdsmMitspieler implements CommonMitspieler {
private UUID id;
private UUID userId;
private boolean eigenesGeraet;
private boolean sperrenVorFinaleAufloesen = true;
private String name;
private GeschlechtEnum geschlecht;
private List<GeschlechtEnum> spieltMit;
private List<RolleEnum> rollen;
private List<Werkzeug> verfuegbareWerkzeuge;
public boolean isVerfuegbar(Werkzeug werkzeug) {
return verfuegbareWerkzeuge.contains(werkzeug);
}
@Override
public String toString() {
return "Mitspieler[id=" + id + ", name=" + name + ", geschlecht=" + geschlecht
+ ", rollen=" + rollen + ", werkzeuge=" + verfuegbareWerkzeuge + "]";
}
public boolean isPassenderSpielpartner(BdsmMitspieler other) {
if (!spieltMit.contains(other.getGeschlecht())) {
return false;
}
if (!other.spieltMit.contains(geschlecht)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,16 @@
package de.oaa.xxx.games.bdsm;
import java.util.UUID;
public abstract class Callback {
private UUID sessionId;
public UUID getSessionId() { return sessionId; }
public void setSessionId(UUID sessionId) { this.sessionId = sessionId; }
@Override
public String toString() {
return getClass().getSimpleName() + "[sessionId=" + sessionId + "]";
}
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.games.bdsm;
public enum GeschlechtEnum {
WEIBLICH,
DIVERS,
MAENNLICH;
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.games.bdsm;
public enum RolleEnum {
BESTRAFUNG_AKTIV,
BESTRAFUNG_PASSIV,
AUFGABE_AKTIV,
AUFGABE_PASSIV;
}

View File

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

View File

@@ -0,0 +1,126 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.common.aufgaben.Aufgabe;
import de.oaa.xxx.games.common.aufgaben.Toy;
import de.oaa.xxx.games.common.entity.AufgabeEntity;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabeRepository;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.ToyRepository;
import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/aufgabe")
@Transactional
public class AufgabeController {
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabeController.class);
private final AufgabeRepository aufgabeRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
private final UserService userService;
private final SubscriptionLimitService limitService;
public AufgabeController(AufgabeRepository aufgabeRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository,
UserService userService,
SubscriptionLimitService limitService) {
this.aufgabeRepository = aufgabeRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
this.userService = userService;
this.limitService = limitService;
}
@GetMapping("/{aufgabeId}")
public ResponseEntity<Aufgabe> get(@PathVariable UUID aufgabeId) {
return aufgabeRepository.findById(aufgabeId)
.map(entity -> ResponseEntity.ok(entity.toAufgabe()))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Aufgabe aufgabe, Principal principal) {
if (aufgabe.getKurzText() == null || aufgabe.getText() == null || aufgabe.getLevel() == null || aufgabe.getGruppeId() == null) {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(aufgabe.getGruppeId()).orElse(null);
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
int limit = limitService.maxTasksPerGroup(userService.requireUser(principal).getUserId());
if (gruppeEntity.getAufgaben().size() >= limit) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(aufgabe.getBenoetigteToys());
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity, toys);
aufgabeRepository.save(entity);
LOGGER.debug("Aufgabe {} '{}' in Gruppe {} erstellt", entity.getAufgabeId(), entity.getKurzText(), aufgabe.getGruppeId());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getAufgabeId()).toUri()
).build();
}
@PutMapping("/{aufgabeId}")
public ResponseEntity<Void> update(@PathVariable UUID aufgabeId, @RequestBody Aufgabe aufgabe) {
if (aufgabe.getKurzText() == null || aufgabe.getText() == null || aufgabe.getLevel() == null) {
return ResponseEntity.badRequest().build();
}
AufgabeEntity entity = aufgabeRepository.findById(aufgabeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(aufgabe.getKurzText());
entity.setText(aufgabe.getText());
entity.setLevel(aufgabe.getLevel());
entity.setSekundenVon(aufgabe.getSekundenVon());
entity.setSekundenBis(aufgabe.getSekundenBis());
entity.setBenoetigtAktiv(aufgabe.getBenoetigtAktiv());
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(aufgabe.getBenoetigteToys()));
aufgabeRepository.save(entity);
LOGGER.debug("Aufgabe {} aktualisiert", aufgabeId);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Aufgabe aufgabe) {
try {
aufgabeRepository.findById(aufgabe.getAufgabeId()).ifPresent(aufgabeRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -0,0 +1,257 @@
package de.oaa.xxx.games.bdsm.controller;
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;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeList;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppeService;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.repository.AufgabeRepository;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.FinisherRepository;
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
import de.oaa.xxx.games.common.repository.SperreRepository;
import de.oaa.xxx.games.common.repository.StrafeRepository;
import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserService;
@RestController
@RequestMapping("/gruppe")
@Transactional
public class AufgabenGruppeController {
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeController.class);
private static final int DEFAULT_PAGE_SIZE = 5;
private final AufgabenGruppeRepository gruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final FinisherRepository finisherRepository;
private final GruppenAboRepository aboRepository;
private final AufgabenGruppeService aufgabenGruppeService;
private final SubscriptionLimitService limitService;
private final UserService userService;
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
FinisherRepository finisherRepository,
GruppenAboRepository aboRepository,
AufgabenGruppeService aufgabenGruppeService,
SubscriptionLimitService limitService,
UserService userService) {
this.gruppeRepository = gruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.finisherRepository = finisherRepository;
this.aboRepository = aboRepository;
this.aufgabenGruppeService = aufgabenGruppeService;
this.limitService = limitService;
this.userService = userService;
}
// ── Paginierte Listen ──
@GetMapping("/list/user")
public ResponseEntity<AufgabenGruppePage> listUser(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserId(
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toGruppePage(result, true));
}
@GetMapping("/list/system")
public ResponseEntity<AufgabenGruppePage> listSystem(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserIdIsNull(
PageRequest.of(page, size, Sort.by("name")));
return ResponseEntity.ok(toGruppePage(result));
}
// ── Bestehende Endpunkte ──
@GetMapping("/all")
public ResponseEntity<AufgabenGruppeList> getAll(@RequestParam(required = false) String search, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
String searchPattern = search != null ? "%" + search + "%" : null;
AufgabenGruppeList list = new AufgabenGruppeList();
list.setGruppen(gruppeRepository.listWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
return ResponseEntity.ok(list);
}
@GetMapping("/own")
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam UUID userId) {
AufgabenGruppeList list = new AufgabenGruppeList();
list.setGruppen(gruppeRepository.findByUserId(userId)
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
return ResponseEntity.ok(list);
}
@GetMapping("/{gruppeId}")
public ResponseEntity<AufgabenGruppe> get(@PathVariable UUID gruppeId) {
return gruppeRepository.findById(gruppeId)
.map(entity -> ResponseEntity.ok(entity.toAufgabenGruppe()))
.orElse(ResponseEntity.noContent().build());
}
// ── Anlegen ──
@PostMapping
public ResponseEntity<Void> create(@RequestBody AufgabenGruppe gruppe, Principal principal) {
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
if (gruppeRepository.countByUserId(user.getUserId()) >= limitService.maxTaskGroups(user.getUserId())) {
return ResponseEntity.status(409).build();
}
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
entity.setUserId(user.getUserId());
entity.setPrivateGruppe(true);
gruppeRepository.save(entity);
LOGGER.debug("User {} hat AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
).build();
}
// ── Bearbeiten ──
@PutMapping("/{gruppeId}")
public ResponseEntity<Void> update(@PathVariable UUID gruppeId,
@RequestBody AufgabenGruppe gruppe,
Principal principal) {
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
return ResponseEntity.badRequest().build();
}
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
entity.setName(gruppe.getName().trim());
entity.setBeschreibung(gruppe.getBeschreibung());
entity.setVon(gruppe.getVon());
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
if (gruppe.getBild() != null) {
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
}
gruppeRepository.save(entity);
LOGGER.debug("User {} hat AufgabenGruppe {} aktualisiert", user.getUserId(), gruppeId);
return ResponseEntity.ok().build();
}
// ── Kopieren (Systemgruppe → eigene) ──
@PostMapping("/copy/{gruppeId}")
public ResponseEntity<Void> copy(@PathVariable UUID gruppeId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
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();
}
}
// ── Löschen ──
@DeleteMapping("/{gruppeId}")
public ResponseEntity<Void> deleteById(@PathVariable UUID gruppeId, Principal principal) {
UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build();
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
if (entity == null) return ResponseEntity.noContent().build();
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
try {
aboRepository.deleteByAufgabenGruppe(entity);
aufgabeRepository.deleteAll(entity.getAufgaben());
strafeRepository.deleteAll(entity.getStrafen());
sperreRepository.deleteAll(entity.getSperren());
finisherRepository.deleteAll(entity.getFinisher());
gruppeRepository.delete(entity);
return ResponseEntity.accepted().build();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
try {
gruppeRepository.findById(gruppe.getGruppenId()).ifPresent(gruppeRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
// ── Hilfsmethoden ──
private UserEntity resolveUser(Principal principal) {
if (principal == null) return null;
return userService.requireUser(principal);
}
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page) {
return toGruppePage(page, false);
}
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page, boolean withSubscriberCount) {
AufgabenGruppePage result = new AufgabenGruppePage();
result.setContent(page.getContent().stream().map(entity -> {
AufgabenGruppe g = entity.toAufgabenGruppe();
if (withSubscriberCount) g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
return g;
}).toList());
result.setCurrentPage(page.getNumber());
result.setTotalPages(page.getTotalPages());
result.setTotalElements(page.getTotalElements());
return result;
}
}

View File

@@ -0,0 +1,238 @@
package de.oaa.xxx.games.bdsm.controller;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService;
@RestController
@RequestMapping("/bdsm/einladung")
@Transactional
public class BdsmEinladungController {
private final BdsmEinladungRepository einladungRepository;
private final UserRepository userRepository;
private final FriendshipRepository friendshipRepository;
private final SystemMessageService systemMessageService;
private final UserService userService;
public BdsmEinladungController(BdsmEinladungRepository einladungRepository,
UserRepository userRepository,
FriendshipRepository friendshipRepository,
SystemMessageService systemMessageService,
UserService userService) {
this.einladungRepository = einladungRepository;
this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository;
this.systemMessageService = systemMessageService;
this.userService = userService;
}
record EinladungRequest(UUID setupId, int slotIndex, UUID inviteeId) {}
record AntwortRequest(boolean accepted, String mode) {} // mode: OWN_DEVICE | HOST_DEVICE
record SpielerDatenRequest(String spielerDatenJson) {}
private UUID currentUserId(Principal principal) {
return userService.requireUser(principal).getUserId();
}
@PostMapping
public ResponseEntity<Map<String, Object>> sendEinladung(@RequestBody EinladungRequest req, Principal principal) {
UUID inviterId = currentUserId(principal);
if (inviterId == null) return ResponseEntity.status(401).build();
// Freundschaft prüfen
var friendship = friendshipRepository.findExisting(inviterId, req.inviteeId());
if (friendship.isEmpty() || friendship.get().getStatus() != de.oaa.xxx.social.entity.FriendshipEntity.Status.ACCEPTED) {
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())
&& (e.getStatus() == Status.PENDING
|| e.getStatus() == Status.ACCEPTED_OWN
|| e.getStatus() == Status.ACCEPTED_HOST));
if (alreadyInvited) {
return ResponseEntity.status(409).build();
}
// Alte Einladung für diesen Slot canceln
einladungRepository.findBySetupId(req.setupId()).stream()
.filter(e -> e.getSlotIndex() == req.slotIndex() && e.getStatus() == Status.PENDING)
.forEach(e -> e.setStatus(Status.CANCELLED));
BdsmEinladungEntity entity = new BdsmEinladungEntity();
entity.setEinladungId(UUID.randomUUID());
entity.setSetupId(req.setupId());
entity.setInviterId(inviterId);
entity.setInviteeId(req.inviteeId());
entity.setSlotIndex(req.slotIndex());
entity.setStatus(Status.PENDING);
entity.setCreatedAt(LocalDateTime.now());
einladungRepository.save(entity);
systemMessageService.pushInvitationUpdate(req.inviteeId());
Map<String, Object> result = new LinkedHashMap<>();
result.put("einladungId", entity.getEinladungId());
return ResponseEntity.ok(result);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> cancelEinladung(@PathVariable UUID id, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
e.setStatus(Status.CANCELLED);
systemMessageService.pushInvitationUpdate(e.getInviteeId());
return ResponseEntity.accepted().build();
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> getBySetupId(@RequestParam UUID setupId, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findBySetupId(setupId).stream()
.map(this::toMap).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/meine-aktive")
public ResponseEntity<Map<String, Object>> getAktive(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
return einladungRepository.findByInviteeIdAndStatus(userId, Status.ACCEPTED_OWN)
.stream().findFirst()
.map(e -> ResponseEntity.ok(toMap(e)))
.orElse(ResponseEntity.noContent().build());
}
@GetMapping("/pending/count")
public ResponseEntity<Integer> getPendingCount(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
return ResponseEntity.ok(einladungRepository.findByInviteeIdAndStatus(userId, Status.PENDING).size());
}
@GetMapping("/pending")
public ResponseEntity<List<Map<String, Object>>> getPending(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviteeIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/sent")
public ResponseEntity<List<Map<String, Object>>> getSent(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviterIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviteeId()).ifPresent(u -> {
m.put("inviteeName", u.getName());
m.put("inviteeAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/{id}")
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);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId) && !e.getInviterId().equals(userId)) {
return ResponseEntity.status(403).build();
}
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return ResponseEntity.ok(m);
}
@PutMapping("/{id}/spielerdaten")
public ResponseEntity<Void> spielerDatenEinreichen(@PathVariable UUID id, @RequestBody SpielerDatenRequest req, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId)) return ResponseEntity.status(403).build();
if (e.getStatus() != Status.ACCEPTED_OWN) return ResponseEntity.badRequest().build();
e.setSpielerDatenJson(req.spielerDatenJson());
e.setBereit(true);
return ResponseEntity.accepted().build();
}
@PutMapping("/{id}/antwort")
public ResponseEntity<Void> antwort(@PathVariable UUID id, @RequestBody AntwortRequest req, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId)) return ResponseEntity.status(403).build();
if (e.getStatus() != Status.PENDING) return ResponseEntity.badRequest().build();
if (!req.accepted()) {
e.setStatus(Status.DECLINED);
} else if ("OWN_DEVICE".equals(req.mode())) {
e.setStatus(Status.ACCEPTED_OWN);
} else {
e.setStatus(Status.ACCEPTED_HOST);
}
return ResponseEntity.accepted().build();
}
private Map<String, Object> toMap(BdsmEinladungEntity e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("einladungId", e.getEinladungId());
m.put("setupId", e.getSetupId());
m.put("slotIndex", e.getSlotIndex());
m.put("inviteeId", e.getInviteeId());
m.put("inviterId", e.getInviterId());
m.put("status", e.getStatus().name());
m.put("sessionId", e.getSessionId());
m.put("bereit", e.isBereit());
m.put("spielerDatenJson", e.getSpielerDatenJson());
userRepository.findById(e.getInviteeId()).ifPresent(u -> m.put("inviteeName", u.getName()));
return m;
}
}

View File

@@ -0,0 +1,624 @@
package de.oaa.xxx.games.bdsm.controller;
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;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import 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.BdsmMitspieler;
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
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;
import de.oaa.xxx.user.UserService;
@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 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;
private final UserService userService;
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
BdsmEinladungRepository einladungRepository, ObjectMapper objectMapper,
SystemMessageService systemMessageService, CardlockRepository cardlockRepository,
BdsmGameService bdsmGameService, UserService userService) {
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;
this.userService = userService;
}
@GetMapping("/{sessionId}")
public ResponseEntity<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
return sessionRepository.findById(sessionId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@GetMapping
public ResponseEntity<BdsmGame> getByUserId(@RequestParam UUID userId) {
return sessionRepository.findByUserId(userId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody BdsmGame session, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var existingOpt = sessionRepository.findByUserId(userId);
if (existingOpt.isPresent()) {
BdsmGameEntity existing = existingOpt.get();
if (existing.getAufgaben() != null) return ResponseEntity.status(409).build();
// Unvollständige Session (aufgaben=null) bereinigen
aktiveSperreRepository.deleteAll(existing.getAktiveSperren());
mitspielerRepository.deleteAll(existing.getMitspieler());
sessionRepository.delete(existing);
}
BdsmGameEntity entity = new BdsmGameEntity();
entity.setSessionId(UUID.randomUUID());
entity.setUserId(userId);
entity.setAufgabenAufAktuellemLevel(0);
entity.setAufgabenProLevel(session.getAufgabenProLevel() != null ? session.getAufgabenProLevel() : 5);
LocalDateTime now = LocalDateTime.now();
entity.setLetzteAktivitaet(now);
entity.setStartZeit(now);
entity.setWahrscheinlichkeitSperre(session.getWahrscheinlichkeitSperre() != null ? session.getWahrscheinlichkeitSperre() : 10);
entity.setWahrscheinlichkeitStrafe(session.getWahrscheinlichkeitStrafe() != null ? session.getWahrscheinlichkeitStrafe() : 10);
entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0);
entity.setLevel(1);
entity.setSetupId(session.getSetupId());
sessionRepository.save(entity);
LOGGER.debug("BdsmGame gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(),
entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
entity.getZeitfaktorZeitstrafen());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSessionId()).toUri()
).build();
}
@DeleteMapping
public ResponseEntity<Void> deleteSession(@RequestBody BdsmGame session) {
return sessionRepository.findById(session.getSessionId())
.map(entity -> {
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
mitspielerRepository.deleteAll(entity.getMitspieler());
sessionRepository.delete(entity);
return ResponseEntity.accepted().<Void>build();
})
.orElse(ResponseEntity.noContent().build());
}
@PostMapping("/{sessionId}/abgeschlossen")
public ResponseEntity<Void> spielAbgeschlossen(@PathVariable UUID sessionId) {
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
ORDENTLICH_BEENDET.add(sessionId);
bdsmGameService.spielAbschliessen(entity);
return ResponseEntity.accepted().build();
}
/** Prüft ob eine Session ordentlich (nicht abgebrochen) beendet wurde. */
@GetMapping("/{sessionId}/beendet")
public ResponseEntity<Void> istBeendet(@PathVariable UUID sessionId) {
if (ORDENTLICH_BEENDET.remove(sessionId)) return ResponseEntity.ok().build();
return ResponseEntity.notFound().build();
}
@DeleteMapping("/{sessionId}/verlassen")
public ResponseEntity<Void> verlasseSpiel(@PathVariable UUID sessionId, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
MitspielerEntity self = session.getMitspieler().stream()
.filter(m -> userId.equals(m.getUserId()))
.findFirst().orElse(null);
if (self == null) return ResponseEntity.status(403).build();
String name = self.getName();
String nachricht = name + " hat das BDSM-Spiel verlassen. Das Spiel wurde abgebrochen.";
systemMessageService.send(userId, session.getUserId(), nachricht, "/userhome.html", MessageCause.GAME_STATE);
session.getMitspieler().stream()
.filter(m -> m.isEigenesGeraet() && m.getUserId() != null && !userId.equals(m.getUserId()))
.forEach(m -> systemMessageService.send(userId, m.getUserId(), nachricht, "/userhome.html", MessageCause.GAME_STATE));
aktiveSperreRepository.deleteAll(session.getAktiveSperren());
mitspielerRepository.deleteAll(session.getMitspieler());
sessionRepository.delete(session);
return ResponseEntity.accepted().build();
}
@PostMapping("/{sessionId}/aufgaben")
public ResponseEntity<Void> setAufgaben(@RequestBody AufgabenList list, @PathVariable UUID sessionId) {
try {
if (list.size() > 1000) {
return ResponseEntity.badRequest().build();
}
String aufgaben = objectMapper.writeValueAsString(list);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
session.setAufgaben(aufgaben);
sessionRepository.save(session);
// Erst jetzt Einladungen mit der Session verknüpfen Gäste werden nur weitergeleitet wenn aufgaben bereit sind
if (session.getSetupId() != null) {
einladungRepository.findBySetupId(session.getSetupId()).stream()
.filter(e -> e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_OWN
|| e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_HOST)
.forEach(e -> e.setSessionId(session.getSessionId()));
}
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/{sessionId}/aufgaben/next")
public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) {
try {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null || session.getAufgaben() == null) {
return ResponseEntity.badRequest().build();
}
session.setLetzteAktivitaet(LocalDateTime.now());
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
AufgabeAnzeige next = durchfuehren.getNext();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
if (next == null) {
return ResponseEntity.noContent().build();
}
next.setLevel(durchfuehren.getLevel());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Neue Aufgabe [sessionId={}, level={}, aufgaben={}/{}, aktiveSperren={}]",
sessionId, session.getLevel(), session.getAufgabenAufAktuellemLevel(),
session.getAufgabenProLevel(), session.getAktiveSperren().size());
session.getAktiveSperren().forEach(s ->
LOGGER.debug(" Sperre [mitspieler={}, {}min, ende={}]",
s.getMitspieler().getName(), s.getMinuten(), s.getEndzeit()));
}
return ResponseEntity.ok(next);
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/{sessionId}/mitspieler")
public ResponseEntity<Void> addMitspieler(@RequestBody BdsmMitspieler mitspieler, @PathVariable UUID sessionId) {
if (mitspieler.getName() == null || mitspieler.getGeschlecht() == null || mitspieler.getRollen() == null
|| mitspieler.getRollen().isEmpty() || mitspieler.getSpieltMit() == null || mitspieler.getSpieltMit().isEmpty()
|| mitspieler.getVerfuegbareWerkzeuge() == null || mitspieler.getVerfuegbareWerkzeuge().isEmpty()) {
return ResponseEntity.badRequest().build();
}
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
MitspielerEntity entity = new MitspielerEntity();
entity.setMitspielerId(UUID.randomUUID());
entity.setGeschlecht(mitspieler.getGeschlecht());
entity.setName(mitspieler.getName());
entity.setRollen(mitspieler.getRollen());
entity.setSpieltMit(mitspieler.getSpieltMit());
entity.setWerkzeuge(new ArrayList<>(mitspieler.getVerfuegbareWerkzeuge()));
entity.setUserId(mitspieler.getUserId());
entity.setEigenesGeraet(mitspieler.isEigenesGeraet());
entity.setSperrenVorFinaleAufloesen(mitspieler.isSperrenVorFinaleAufloesen());
entity.setSession(session);
mitspielerRepository.save(entity);
// Aktive Chastity-Lockees: 365-Tage-Zeitstrafe auf das gesperrte Körperteil
if (mitspieler.getUserId() != null
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(mitspieler.getUserId())) {
List<Werkzeug> locked = new ArrayList<>();
if (mitspieler.getGeschlecht() == GeschlechtEnum.WEIBLICH) locked.add(Werkzeug.VAGINA);
else if (mitspieler.getGeschlecht() == GeschlechtEnum.MAENNLICH) locked.add(Werkzeug.PENIS);
else { locked.add(Werkzeug.VAGINA); locked.add(Werkzeug.PENIS); }
if (!locked.isEmpty()) {
// Gesperrte Werkzeuge force-hinzufügen (auch wenn Checkbox nicht angekreuzt)
locked.forEach(w -> { if (!entity.getWerkzeuge().contains(w)) entity.getWerkzeuge().add(w); });
LocalDateTime now = LocalDateTime.now();
AktiveSperreEntity chastitySperre = new AktiveSperreEntity();
chastitySperre.setAktiveSperreId(UUID.randomUUID());
chastitySperre.setMitspieler(entity);
chastitySperre.setSession(session);
chastitySperre.setFuer(locked);
chastitySperre.setMinuten(1440);
chastitySperre.setStartzeit(now);
chastitySperre.setEndzeit(now.plusHours(24));
chastitySperre.setReleaseText(entity.getName() + " hat die Keuschheit durchgehalten das Schloss ist ab sofort offen.");
aktiveSperreRepository.save(chastitySperre);
// Werkzeug für die Spieldauer durch die Zeitstrafe sperren
locked.forEach(entity.getWerkzeuge()::remove);
mitspielerRepository.save(entity);
}
}
return ResponseEntity.accepted().build();
}
@GetMapping("/{sessionId}/finisher")
public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) {
try {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
return ResponseEntity.ok(durchfuehren.getFinisher());
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/{sessionId}/backToLevel5")
public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) {
try {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
durchfuehren.backToLvl5();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
sessionRepository.save(session);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/{sessionId}/mitspieler/me")
public ResponseEntity<Map<String, Object>> getMeinMitspieler(@PathVariable UUID sessionId, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
return session.getMitspieler().stream()
.filter(m -> userId.equals(m.getUserId()))
.findFirst()
.map(m -> {
Map<String, Object> result = new LinkedHashMap<>();
result.put("mitspielerId", m.getMitspielerId());
result.put("name", m.getName());
result.put("eigenesGeraet", m.isEigenesGeraet());
return ResponseEntity.ok(result);
})
.orElse(ResponseEntity.noContent().build());
}
record AbschliessenRequest(boolean sperreAnwenden) {}
record SperreFreigabe(String text, UUID mitspielerId, boolean eigenesGeraet) {}
record AbschliessenResponse(List<SperreFreigabe> abgelaufeneSperren) {}
@PostMapping("/{sessionId}/active-task/abschliessen")
public ResponseEntity<AbschliessenResponse> activeTaskAbschliessen(
@PathVariable UUID sessionId, @RequestBody AbschliessenRequest req) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
SperreVerarbeiten sperreVerarbeiten = new SperreVerarbeiten();
if (req.sperreAnwenden() && session.getActiveTaskJson() != null) {
try {
JsonNode task = objectMapper.readTree(session.getActiveTaskJson());
JsonNode cb = task.get("callback");
if (cb != null && !cb.isNull()) {
if (cb.has("sperreId") && !cb.get("sperreId").isNull()) {
SperreCallback callback = objectMapper.treeToValue(cb, SperreCallback.class);
callback.setSessionId(sessionId);
sperreVerarbeiten.sperreAnwenden(callback, sessionRepository, mitspielerRepository, aktiveSperreRepository);
LOGGER.info("Zeitstrafe via abschliessen angewandt [session={}, spieler={}]", sessionId, callback.getSpielerId());
} else if (cb.has("faktor") && !cb.get("faktor").isNull()) {
SperrenVerlaengernCallback callback = objectMapper.treeToValue(cb, SperrenVerlaengernCallback.class);
List<AktiveSperreEntity> locks = aktiveSperreRepository.findAktiveLocks(callback.getSpielerId());
locks.forEach(lock -> sperreVerarbeiten.sperreVerlaengern(lock, callback.getFaktor(), aktiveSperreRepository));
LOGGER.info("Sperren via abschliessen verlängert [session={}, spieler={}, faktor={}]", sessionId, callback.getSpielerId(), callback.getFaktor());
}
}
} catch (Exception e) {
LOGGER.error("Fehler beim Verarbeiten des Callbacks beim Abschließen: {}", e.getMessage(), e);
}
}
session.setActiveTaskJson(null);
session.setTaskStartedAt(null);
sessionRepository.save(session);
List<SperreFreigabe> freigaben = new ArrayList<>();
aktiveSperreRepository.findAbgelaufene(sessionId, LocalDateTime.now()).forEach(s -> {
UUID mitspielerId = s.getMitspieler().getMitspielerId();
boolean eigenesGeraet = s.getMitspieler().isEigenesGeraet();
String t = sperreVerarbeiten.sperreAufheben(s, aktiveSperreRepository, mitspielerRepository);
if (t != null && !t.isBlank()) freigaben.add(new SperreFreigabe(t, mitspielerId, eigenesGeraet));
});
return ResponseEntity.ok(new AbschliessenResponse(freigaben));
}
record ActiveTaskRequest(String taskJson, LocalDateTime timerStartedAt) {}
record ActiveTaskResponse(String taskJson, Long elapsedSeconds) {}
@PutMapping("/{sessionId}/active-task")
public ResponseEntity<Void> setActiveTask(@PathVariable UUID sessionId, @RequestBody ActiveTaskRequest req) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(req.taskJson());
session.setTaskStartedAt(req.timerStartedAt());
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@DeleteMapping("/{sessionId}/active-task")
public ResponseEntity<Void> clearActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(null);
session.setTaskStartedAt(null);
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@GetMapping("/{sessionId}/active-task")
public ResponseEntity<ActiveTaskResponse> getActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
if (session.getActiveTaskJson() == null) return ResponseEntity.noContent().build();
Long elapsed = null;
if (session.getTaskStartedAt() != null) {
elapsed = Duration.between(session.getTaskStartedAt(), LocalDateTime.now()).getSeconds();
}
return ResponseEntity.ok(new ActiveTaskResponse(session.getActiveTaskJson(), elapsed));
}
// ── Keyholder-Angebot: prüft ob am Ende eine VAGINA/PENIS-Sperre vorliegt ──
@GetMapping("/{sessionId}/keyholder-angebot")
public ResponseEntity<Map<String, Object>> keyholderAngebot(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
// Alle noch in der DB vorhandenen VAGINA/PENIS-Sperren auch abgelaufene,
// da im Finale-Flow bereits abgelaufene Sperren noch nicht formal aufgehoben wurden.
List<AktiveSperreEntity> relevantesSperren = session.getAktiveSperren().stream()
.filter(s -> s.getFuer().contains(Werkzeug.VAGINA) || s.getFuer().contains(Werkzeug.PENIS))
.toList();
for (AktiveSperreEntity sperre : relevantesSperren) {
MitspielerEntity lockee = sperre.getMitspieler();
if (lockee == null || lockee.getUserId() == null || lockee.getGeschlecht() == null) continue;
// Kein Angebot wenn Lockee bereits aktiv in einem Chastity-Game gesperrt ist
if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockee.getUserId())) continue;
for (MitspielerEntity kandidat : session.getMitspieler()) {
if (kandidat.getMitspielerId().equals(lockee.getMitspielerId())) continue;
if (kandidat.getUserId() == null) continue;
if (!kandidat.getSpieltMit().contains(lockee.getGeschlecht())) continue;
List<CardLockEntity> locks = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(kandidat.getUserId());
if (locks.isEmpty()) continue;
Map<String, Object> result = new LinkedHashMap<>();
result.put("lockeeId", lockee.getMitspielerId());
result.put("lockeeName", lockee.getName());
result.put("lockeeUserId", lockee.getUserId());
result.put("keyholderMitspielerId", kandidat.getMitspielerId());
result.put("keyholderName", kandidat.getName());
result.put("keyholderUserId", kandidat.getUserId());
return ResponseEntity.ok(result);
}
}
return ResponseEntity.noContent().build();
}
@GetMapping("/{sessionId}/keyholder-locks")
public ResponseEntity<List<Map<String, Object>>> keyholderLocks(
@PathVariable UUID sessionId, @RequestParam UUID keyholderUserId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
List<Map<String, Object>> result = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(keyholderUserId).stream()
.map(l -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("lockId", l.getLockId());
item.put("name", l.getName() != null ? l.getName() : "Unbenanntes Lock");
item.put("pickEveryMinute", l.getPickEveryMinute());
item.put("totalCards", l.getInitialCards() != null ? l.getInitialCards().size() : 0);
item.put("active", l.getStartTime() != null && l.getUnlockTime() == null);
return item;
})
.toList();
if (result.isEmpty()) return ResponseEntity.noContent().build();
return ResponseEntity.ok(result);
}
record ZuChastityRequest(UUID lockId, UUID lockeeUserId, UUID keyholderUserId) {}
@PostMapping("/{sessionId}/zu-chastity")
public ResponseEntity<Map<String, Object>> zuChastity(
@PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) {
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();
}
}
/** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */
@GetMapping("/chastity-constraint")
public ResponseEntity<Map<String, Object>> chastityConstraint(@RequestParam UUID userId) {
Map<String, Object> result = new LinkedHashMap<>();
if (!cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(userId)) {
result.put("lockedWerkzeug", null);
return ResponseEntity.ok(result);
}
return userRepository.findById(userId).map(u -> {
String werkzeug = null;
if (u.getGeschlecht() != null) {
werkzeug = switch (u.getGeschlecht().name()) {
case "WEIBLICH" -> "VAGINA";
case "MAENNLICH" -> "PENIS";
default -> "BOTH";
};
}
result.put("lockedWerkzeug", werkzeug);
return ResponseEntity.ok(result);
}).orElseGet(() -> {
result.put("lockedWerkzeug", null);
return ResponseEntity.ok(result);
});
}
// ── Debug-Endpoint: vollständiger Entity-Zustand ──
@GetMapping("/{sessionId}/debug")
public ResponseEntity<Map<String, Object>> debug(@PathVariable UUID sessionId) {
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
Map<String, Object> session = new LinkedHashMap<>();
session.put("sessionId", entity.getSessionId());
session.put("userId", entity.getUserId());
session.put("setupId", entity.getSetupId());
session.put("startZeit", entity.getStartZeit());
session.put("letzteAktivitaet", entity.getLetzteAktivitaet());
session.put("level", entity.getLevel());
session.put("aufgabenAufAktuellemLevel", entity.getAufgabenAufAktuellemLevel());
session.put("aufgabenProLevel", entity.getAufgabenProLevel());
session.put("wahrscheinlichkeitSperre", entity.getWahrscheinlichkeitSperre());
session.put("wahrscheinlichkeitStrafe", entity.getWahrscheinlichkeitStrafe());
session.put("zeitfaktorZeitstrafen", entity.getZeitfaktorZeitstrafen());
session.put("taskStartedAt", entity.getTaskStartedAt());
session.put("hatAufgaben", entity.getAufgaben() != null);
session.put("hatActiveTask", entity.getActiveTaskJson() != null);
List<Map<String, Object>> mitspielerList = entity.getMitspieler().stream().map(m -> {
Map<String, Object> mp = new LinkedHashMap<>();
mp.put("mitspielerId", m.getMitspielerId());
mp.put("name", m.getName());
mp.put("userId", m.getUserId());
mp.put("geschlecht", m.getGeschlecht());
mp.put("rollen", m.getRollen());
mp.put("werkzeuge", m.getWerkzeuge());
mp.put("spieltMit", m.getSpieltMit());
mp.put("eigenesGeraet", m.isEigenesGeraet());
mp.put("sperrenVorFinaleAufloesen", m.isSperrenVorFinaleAufloesen());
return mp;
}).toList();
LocalDateTime now = LocalDateTime.now();
List<Map<String, Object>> sperrenList = entity.getAktiveSperren().stream().map(s -> {
Map<String, Object> sp = new LinkedHashMap<>();
sp.put("aktiveSperreId", s.getAktiveSperreId());
sp.put("mitspielerName", s.getMitspieler() != null ? s.getMitspieler().getName() : null);
sp.put("fuer", s.getFuer());
sp.put("minuten", s.getMinuten());
sp.put("startzeit", s.getStartzeit());
sp.put("endzeit", s.getEndzeit());
sp.put("abgelaufen", s.getEndzeit() != null && s.getEndzeit().isBefore(now));
sp.put("releaseText", s.getReleaseText());
return sp;
}).toList();
Map<String, Object> result = new LinkedHashMap<>();
result.put("session", session);
result.put("mitspieler", mitspielerList);
result.put("aktiveSperren", sperrenList);
return ResponseEntity.ok(result);
}
private BdsmGame toSession(BdsmGameEntity entity) {
BdsmGame session = new BdsmGame();
session.setSessionId(entity.getSessionId());
session.setUserId(entity.getUserId());
session.setAufgabenProLevel(entity.getAufgabenProLevel());
session.setWahrscheinlichkeitSperre(entity.getWahrscheinlichkeitSperre());
session.setWahrscheinlichkeitStrafe(entity.getWahrscheinlichkeitStrafe());
session.setZeitfaktorZeitstrafen(entity.getZeitfaktorZeitstrafen());
session.setLevel(entity.getLevel());
session.setAufgabenAufAktuellemLevel(entity.getAufgabenAufAktuellemLevel());
session.setStartZeit(entity.getStartZeit());
session.setLetzteAktivitaet(entity.getLetzteAktivitaet());
return session;
}
}

View File

@@ -0,0 +1,71 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.bdsm.entity.BdsmSetupDraftEntity;
import de.oaa.xxx.games.bdsm.repository.BdsmSetupDraftRepository;
import de.oaa.xxx.user.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/bdsm/setup-draft")
@Transactional
public class BdsmSetupDraftController {
private final BdsmSetupDraftRepository draftRepository;
private final UserService userService;
public BdsmSetupDraftController(BdsmSetupDraftRepository draftRepository, UserService userService) {
this.draftRepository = draftRepository;
this.userService = userService;
}
record DraftRequest(String setupId, String settingsJson, String setupJson, String gruppenJson) {}
@GetMapping
public ResponseEntity<Map<String, Object>> getDraft(
@RequestParam(required = false) String setupId,
Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
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());
m.put("settingsJson", d.getSettingsJson());
m.put("setupJson", d.getSetupJson());
m.put("gruppenJson", d.getGruppenJson());
return ResponseEntity.ok(m);
})
.orElse(ResponseEntity.noContent().build());
}
@PutMapping
public ResponseEntity<Void> saveDraft(@RequestBody DraftRequest req, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
BdsmSetupDraftEntity d = draftRepository.findByUserId(userId)
.orElseGet(() -> { BdsmSetupDraftEntity n = new BdsmSetupDraftEntity(); n.setUserId(userId); return n; });
if (req.setupId() != null) d.setSetupId(req.setupId());
if (req.settingsJson() != null) d.setSettingsJson(req.settingsJson());
if (req.setupJson() != null) d.setSetupJson(req.setupJson());
if (req.gruppenJson() != null) d.setGruppenJson(req.gruppenJson());
d.setUpdatedAt(LocalDateTime.now());
draftRepository.save(d);
return ResponseEntity.accepted().build();
}
@DeleteMapping
public ResponseEntity<Void> deleteDraft(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
draftRepository.findByUserId(userId).ifPresent(draftRepository::delete);
return ResponseEntity.accepted().build();
}
}

View File

@@ -0,0 +1,89 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.common.aufgaben.Favorit;
import de.oaa.xxx.games.common.aufgaben.FavoritList;
import de.oaa.xxx.games.common.entity.FavoritEntity;
import de.oaa.xxx.games.common.repository.FavoritRepository;
import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/favorit")
@Transactional
public class FavoritController {
private static final Logger LOGGER = LoggerFactory.getLogger(FavoritController.class);
private final FavoritRepository favoritRepository;
private final UserService userService;
public FavoritController(FavoritRepository favoritRepository, UserService userService) {
this.favoritRepository = favoritRepository;
this.userService = userService;
}
@GetMapping("/{favoritId}")
public ResponseEntity<Favorit> get(@PathVariable UUID favoritId) {
return favoritRepository.findById(favoritId)
.map(entity -> ResponseEntity.ok(entity.toFavorit()))
.orElse(ResponseEntity.noContent().build());
}
@GetMapping
public ResponseEntity<FavoritList> all(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
List<FavoritEntity> entities = favoritRepository.findByUserId(userId);
FavoritList result = new FavoritList();
result.setFavoriten(entities.stream().map(FavoritEntity::toFavorit).toList());
return ResponseEntity.ok(result);
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Favorit favorit, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
if (favorit.getAufgabenGruppeId() == null) {
return ResponseEntity.badRequest().build();
}
List<FavoritEntity> existing = favoritRepository.findByUserIdAndAufgabenGruppeId(userId, favorit.getAufgabenGruppeId());
FavoritEntity entity;
if (existing.isEmpty()) {
entity = FavoritEntity.fromFavorit(favorit, userId);
favoritRepository.save(entity);
LOGGER.debug("User {} hat AufgabenGruppe {} als Favorit gespeichert", userId, favorit.getAufgabenGruppeId());
} else {
entity = existing.get(0);
}
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getFavoritId()).toUri()
).build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Favorit favorit, Principal principal) {
try {
UUID userId = userService.requireUser(principal).getUserId();
favoritRepository.findByUserIdAndAufgabenGruppeId(userId, favorit.getAufgabenGruppeId())
.forEach(favoritRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
}

View File

@@ -0,0 +1,34 @@
package de.oaa.xxx.games.bdsm.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.common.aufgaben.DefaultFiller;
@RestController
@RequestMapping("/filler")
public class FillerController {
private static final Logger LOGGER = LoggerFactory.getLogger(FillerController.class);
private final DefaultFiller defaultFiller;
public FillerController(DefaultFiller defaultFiller) {
this.defaultFiller = defaultFiller;
}
@PostMapping
public ResponseEntity<Void> fill() {
try {
defaultFiller.fill();
return ResponseEntity.ok().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
}

View File

@@ -0,0 +1,116 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.common.aufgaben.Finisher;
import de.oaa.xxx.games.common.aufgaben.Toy;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.FinisherEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.FinisherRepository;
import de.oaa.xxx.games.common.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/finisher")
@Transactional
public class FinisherController {
private static final Logger LOGGER = LoggerFactory.getLogger(FinisherController.class);
private final FinisherRepository finisherRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public FinisherController(FinisherRepository finisherRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.finisherRepository = finisherRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{finisherId}")
public ResponseEntity<Finisher> get(@PathVariable UUID finisherId) {
return finisherRepository.findById(finisherId)
.map(entity -> ResponseEntity.ok(entity.toFinisher()))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Finisher finisher) {
if (finisher.getKurzText() == null || finisher.getText() == null
|| finisher.getGeschlecht() == null || finisher.getGruppeId() == null) {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(finisher.getGruppeId()).orElse(null);
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
if (gruppeEntity.getFinisher().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(finisher.getBenoetigteToys());
FinisherEntity entity = FinisherEntity.create(finisher, gruppeEntity, toys);
finisherRepository.save(entity);
LOGGER.debug("Finisher {} '{}' in Gruppe {} erstellt", entity.getFinisherId(), entity.getKurzText(), finisher.getGruppeId());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getFinisherId()).toUri()
).build();
}
@PutMapping("/{finisherId}")
public ResponseEntity<Void> update(@PathVariable UUID finisherId, @RequestBody Finisher finisher) {
if (finisher.getKurzText() == null || finisher.getText() == null || finisher.getGeschlecht() == null) {
return ResponseEntity.badRequest().build();
}
FinisherEntity entity = finisherRepository.findById(finisherId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(finisher.getKurzText());
entity.setText(finisher.getText());
entity.setGeschlecht(finisher.getGeschlecht());
entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv());
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys()));
finisherRepository.save(entity);
LOGGER.debug("Finisher {} aktualisiert", finisherId);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Finisher finisher) {
try {
finisherRepository.findById(finisher.getFinisherId()).ifPresent(finisherRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -0,0 +1,118 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.common.aufgaben.Sperre;
import de.oaa.xxx.games.common.aufgaben.Toy;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.SperreEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.SperreRepository;
import de.oaa.xxx.games.common.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController("aufgabenSperreController")
@RequestMapping("/sperre")
@Transactional
public class SperreController {
private static final Logger LOGGER = LoggerFactory.getLogger(SperreController.class);
private final SperreRepository sperreRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public SperreController(SperreRepository sperreRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.sperreRepository = sperreRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{sperreId}")
public ResponseEntity<Sperre> get(@PathVariable UUID sperreId) {
return sperreRepository.findById(sperreId)
.map(entity -> ResponseEntity.ok(entity.toSperre()))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Sperre sperre) {
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null
|| sperre.getGruppeId() == null || sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null);
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
if (gruppeEntity.getSperren().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(sperre.getBenoetigteToys());
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity, toys);
sperreRepository.save(entity);
LOGGER.debug("Sperre {} '{}' in Gruppe {} erstellt", entity.getSperreId(), entity.getKurzText(), sperre.getGruppeId());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSperreId()).toUri()
).build();
}
@PutMapping("/{sperreId}")
public ResponseEntity<Void> update(@PathVariable UUID sperreId, @RequestBody Sperre sperre) {
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null
|| sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) {
return ResponseEntity.badRequest().build();
}
SperreEntity entity = sperreRepository.findById(sperreId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(sperre.getKurzText());
entity.setText(sperre.getText());
entity.setReleaseText(sperre.getReleaseText());
entity.setMinutenVon(sperre.getMinutenVon());
entity.setMinutenBis(sperre.getMinutenBis());
entity.setSperreFuer(sperre.getSperreFuer());
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
sperreRepository.save(entity);
LOGGER.debug("Sperre {} aktualisiert", sperreId);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Sperre sperre) {
try {
sperreRepository.findById(sperre.getSperreId()).ifPresent(sperreRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

@@ -0,0 +1,117 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.common.aufgaben.Strafe;
import de.oaa.xxx.games.common.aufgaben.Toy;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.StrafeEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.StrafeRepository;
import de.oaa.xxx.games.common.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/strafe")
@Transactional
public class StrafeController {
private static final Logger LOGGER = LoggerFactory.getLogger(StrafeController.class);
private final StrafeRepository strafeRepository;
private final AufgabenGruppeRepository gruppeRepository;
private final ToyRepository toyRepository;
public StrafeController(StrafeRepository strafeRepository,
AufgabenGruppeRepository gruppeRepository,
ToyRepository toyRepository) {
this.strafeRepository = strafeRepository;
this.gruppeRepository = gruppeRepository;
this.toyRepository = toyRepository;
}
@GetMapping("/{strafeId}")
public ResponseEntity<Strafe> get(@PathVariable UUID strafeId) {
return strafeRepository.findById(strafeId)
.map(entity -> ResponseEntity.ok(entity.toStrafe()))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Strafe strafe) {
if (strafe.getKurzText() == null || strafe.getText() == null || strafe.getLevel() == null || strafe.getGruppeId() == null) {
return ResponseEntity.badRequest().build();
}
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(strafe.getGruppeId()).orElse(null);
if (gruppeEntity == null) {
return ResponseEntity.badRequest().build();
}
if (gruppeEntity.getStrafen().size() >= 100) {
return ResponseEntity.status(409).build();
}
List<ToyEntity> toys = resolveToys(strafe.getBenoetigteToys());
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity, toys);
strafeRepository.save(entity);
LOGGER.debug("Strafe {} '{}' in Gruppe {} erstellt", entity.getStrafeId(), entity.getKurzText(), strafe.getGruppeId());
return ResponseEntity.created(
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getStrafeId()).toUri()
).build();
}
@PutMapping("/{strafeId}")
public ResponseEntity<Void> update(@PathVariable UUID strafeId, @RequestBody Strafe strafe) {
if (strafe.getKurzText() == null || strafe.getText() == null || strafe.getLevel() == null) {
return ResponseEntity.badRequest().build();
}
StrafeEntity entity = strafeRepository.findById(strafeId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build();
entity.setKurzText(strafe.getKurzText());
entity.setText(strafe.getText());
entity.setLevel(strafe.getLevel());
entity.setSekundenVon(strafe.getSekundenVon());
entity.setSekundenBis(strafe.getSekundenBis());
entity.setBenoetigtAktiv(strafe.getBenoetigtAktiv());
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(strafe.getBenoetigteToys()));
strafeRepository.save(entity);
LOGGER.debug("Strafe {} aktualisiert", strafeId);
return ResponseEntity.ok().build();
}
@DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Strafe strafe) {
try {
strafeRepository.findById(strafe.getStrafeId()).ifPresent(strafeRepository::delete);
return ResponseEntity.accepted().build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
return ResponseEntity.internalServerError().build();
}
}
private List<ToyEntity> resolveToys(List<Toy> toys) {
if (toys == null || toys.isEmpty()) return new ArrayList<>();
List<UUID> ids = toys.stream()
.filter(t -> t.getToyId() != null)
.map(Toy::getToyId)
.toList();
if (ids.isEmpty()) return new ArrayList<>();
return toyRepository.findAllById(ids);
}
}

View File

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

View File

@@ -0,0 +1,77 @@
package de.oaa.xxx.games.bdsm.entity;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.games.bdsm.BdsmMitspieler;
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "aktiveSperre")
public class AktiveSperreEntity {
@Id
@Column
private UUID aktiveSperreId;
@ManyToOne
@JoinColumn(name = "mitspielerId", nullable = false)
private MitspielerEntity mitspieler;
@Column
private Integer minuten;
@Column
private LocalDateTime startzeit;
@Column
private LocalDateTime endzeit;
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
@CollectionTable(name = "aktiveSperre_fuer", joinColumns = @JoinColumn(name = "aktiveSperreId"))
@Column(name = "werkzeug")
private List<Werkzeug> fuer = new ArrayList<>();
@Column
private String releaseText;
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private BdsmGameEntity session;
public AktiveSperre toSperre(List<BdsmMitspieler> mitspielerList) {
AktiveSperre sperre = new AktiveSperre();
sperre.setAktiveSperreId(aktiveSperreId);
sperre.setEndzeit(endzeit);
sperre.setFuer(fuer);
sperre.setMinuten(minuten);
sperre.setMitspieler(getMitspielerFromList(mitspielerList, mitspieler.getMitspielerId()));
sperre.setReleaseText(releaseText);
sperre.setStartzeit(startzeit);
return sperre;
}
@Override
public String toString() {
return "AktiveSperreEntity[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null)
+ ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]";
}
private BdsmMitspieler getMitspielerFromList(List<BdsmMitspieler> mitspielerList, UUID id) {
Optional<BdsmMitspieler> first = mitspielerList.stream().filter(m -> m.getId().equals(id)).findFirst();
return first.orElse(null);
}
}

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_defaults")
public class BdsmDefaultsEntity {
@Id
@Column(name = "user_id")
private UUID userId;
@Column(length = 100)
private String spieltMit;
@Column(length = 200)
private String rollen;
@Column(length = 200)
private String werkzeuge;
}

View File

@@ -0,0 +1,56 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_einladung")
public class BdsmEinladungEntity {
public enum Status {
PENDING, ACCEPTED_OWN, ACCEPTED_HOST, DECLINED, CANCELLED
}
@Id
@Column
private UUID einladungId;
@Column(nullable = false)
private UUID setupId;
@Column
private UUID sessionId;
@Column(nullable = false)
private UUID inviterId;
@Column(nullable = false)
private UUID inviteeId;
@Column(nullable = false)
private int slotIndex;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private Status status;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(columnDefinition = "TEXT")
private String spielerDatenJson;
@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT false")
private boolean bereit;
}

View File

@@ -0,0 +1,64 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_game")
public class BdsmGameEntity {
@Id
@Column
private UUID sessionId;
@Column(unique = true)
private UUID userId;
@Column
private LocalDateTime startZeit;
@Column
private LocalDateTime letzteAktivitaet;
@OneToMany(mappedBy = "session", fetch = FetchType.EAGER)
private List<MitspielerEntity> mitspieler = new ArrayList<>();
@OneToMany(mappedBy = "session", fetch = FetchType.EAGER)
private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>();
@Column
private Integer wahrscheinlichkeitSperre;
@Column
private Integer wahrscheinlichkeitStrafe;
@Column
private Integer aufgabenProLevel;
@Column
private Integer level;
@Column
private Integer aufgabenAufAktuellemLevel;
@Column(columnDefinition = "TEXT")
private String aufgaben;
@Column
private Double zeitfaktorZeitstrafen;
@Column(columnDefinition = "TEXT")
private String activeTaskJson;
@Column
private LocalDateTime taskStartedAt;
@Column
private UUID setupId;
@Override
public String toString() {
return "BdsmGameEntity[sessionId=" + sessionId + ", userId=" + userId
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]";
}
}

View File

@@ -0,0 +1,37 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_setup_draft")
public class BdsmSetupDraftEntity {
@Id
@Column(name = "user_id")
private UUID userId;
@Column(length = 36)
private String setupId;
@Column(columnDefinition = "TEXT")
private String settingsJson;
@Column(columnDefinition = "TEXT")
private String setupJson;
@Column(columnDefinition = "TEXT")
private String gruppenJson;
@Column
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,86 @@
package de.oaa.xxx.games.bdsm.entity;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.BdsmMitspieler;
import de.oaa.xxx.games.bdsm.RolleEnum;
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "mitspieler")
public class MitspielerEntity {
@Id
@Column
private UUID mitspielerId;
@Column
private UUID userId;
@Column
private boolean eigenesGeraet;
@Column
private String name;
@Enumerated(EnumType.STRING)
@Column
private GeschlechtEnum geschlecht;
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_werkzeuge", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "werkzeug")
private List<Werkzeug> werkzeuge = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = GeschlechtEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_spieltMit", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "geschlecht")
private List<GeschlechtEnum> spieltMit = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = RolleEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "rolle")
private List<RolleEnum> rollen = new ArrayList<>();
@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT true")
private boolean sperrenVorFinaleAufloesen = true;
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private BdsmGameEntity session;
@OneToMany(mappedBy = "mitspieler", fetch = FetchType.EAGER)
private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>();
@Override
public String toString() {
return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name
+ ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]";
}
public BdsmMitspieler toMitspieler() {
BdsmMitspieler mitspieler = new BdsmMitspieler();
mitspieler.setGeschlecht(geschlecht);
mitspieler.setId(mitspielerId);
mitspieler.setUserId(userId);
mitspieler.setEigenesGeraet(eigenesGeraet);
mitspieler.setName(name);
mitspieler.setRollen(rollen);
mitspieler.setSpieltMit(spieltMit);
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge));
mitspieler.setSperrenVorFinaleAufloesen(sperrenVorFinaleAufloesen);
return mitspieler;
}
}

View File

@@ -0,0 +1,19 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public interface AktiveSperreRepository extends JpaRepository<AktiveSperreEntity, UUID> {
@Query("select a from AktiveSperreEntity a join a.session s where a.endzeit < :now and s.sessionId = :sessionId")
List<AktiveSperreEntity> findAbgelaufene(@Param("sessionId") UUID sessionId, @Param("now") LocalDateTime now);
@Query("select a from AktiveSperreEntity a join a.mitspieler m where m.mitspielerId = :mitspielerId")
List<AktiveSperreEntity> findAktiveLocks(@Param("mitspielerId") UUID mitspielerId);
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmDefaultsRepository extends JpaRepository<BdsmDefaultsEntity, UUID> {
Optional<BdsmDefaultsEntity> findByUserId(UUID userId);
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface BdsmEinladungRepository extends JpaRepository<BdsmEinladungEntity, UUID> {
List<BdsmEinladungEntity> findBySetupId(UUID setupId);
List<BdsmEinladungEntity> findByInviteeIdAndStatus(UUID inviteeId, Status status);
List<BdsmEinladungEntity> findByInviterIdAndStatus(UUID inviterId, Status status);
}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmGameRepository extends JpaRepository<BdsmGameEntity, UUID> {
Optional<BdsmGameEntity> findByUserId(UUID userId);
}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmSetupDraftEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> {
Optional<BdsmSetupDraftEntity> findByUserId(UUID userId);
Optional<BdsmSetupDraftEntity> findBySetupId(String setupId);
}

View File

@@ -0,0 +1,9 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface MitspielerRepository extends JpaRepository<MitspielerEntity, UUID> {
}

View File

@@ -0,0 +1,26 @@
package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID;
public class SperreCallback extends Callback {
private UUID sperreId;
private UUID spielerId;
private String releaseText;
public UUID getSperreId() { return sperreId; }
public void setSperreId(UUID sperreId) { this.sperreId = sperreId; }
public UUID getSpielerId() { return spielerId; }
public void setSpielerId(UUID spielerId) { this.spielerId = spielerId; }
public String getReleaseText() { return releaseText; }
public void setReleaseText(String releaseText) { this.releaseText = releaseText; }
@Override
public String toString() {
return "SperreCallback[sessionId=" + getSessionId() + ", sperreId=" + sperreId + ", spielerId=" + spielerId + "]";
}
}

View File

@@ -0,0 +1,91 @@
package de.oaa.xxx.games.bdsm.sperre;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.Sperre;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
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.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
public class SperreVerarbeiten {
private final ObjectMapper objectMapper = new ObjectMapper();
public void sperreAnwenden(SperreCallback callback, BdsmGameRepository sessionRepository,
MitspielerRepository mitspielerRepository, AktiveSperreRepository sperreRepository) throws Exception {
BdsmGameEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
MitspielerEntity mitspieler = mitspielerRepository.findById(callback.getSpielerId()).orElse(null);
if (session != null) {
AufgabenList aufgaben = objectMapper.readValue(session.getAufgaben(), AufgabenList.class);
Optional<Sperre> first = aufgaben.getSperren().stream()
.filter(sperre -> sperre.getSperreId().equals(callback.getSperreId()))
.findFirst();
if (first.isPresent()) {
Sperre sperre = first.get();
AktiveSperreEntity aktiv = new AktiveSperreEntity();
fill(callback, session, mitspieler, sperre, aktiv);
sperreRepository.save(aktiv);
sperre.getSperreFuer().forEach(mitspieler.getWerkzeuge()::remove);
mitspielerRepository.save(mitspieler);
}
}
}
public String sperreAufheben(AktiveSperreEntity aufzuheben, AktiveSperreRepository sperreRepository,
MitspielerRepository mitspielerRepository) {
MitspielerEntity mitspieler = aufzuheben.getMitspieler();
aufzuheben.getFuer().forEach(mitspieler.getWerkzeuge()::add);
mitspielerRepository.save(mitspieler);
String releaseText = aufzuheben.getReleaseText();
sperreRepository.delete(aufzuheben);
return releaseText;
}
public void sperreVerlaengern(AktiveSperreEntity verlaengern, Integer faktor, AktiveSperreRepository sperreRepository) {
Integer neueDauer = verlaengern.getMinuten() * faktor;
verlaengern.setEndzeit(verlaengern.getStartzeit().plusMinutes(neueDauer));
verlaengern.setMinuten(neueDauer);
sperreRepository.save(verlaengern);
}
private void fill(SperreCallback callback, BdsmGameEntity session, MitspielerEntity mitspieler,
Sperre sperre, AktiveSperreEntity aktiv) {
aktiv.setAktiveSperreId(UUID.randomUUID());
LocalDateTime now = LocalDateTime.now();
Integer minuten = berechneDauer(session, sperre);
aktiv.setStartzeit(now);
aktiv.setEndzeit(now.plusMinutes(minuten));
aktiv.setMinuten(minuten);
aktiv.setMitspieler(mitspieler);
aktiv.setSession(session);
aktiv.setFuer(sperre.getSperreFuer());
aktiv.setReleaseText(callback.getReleaseText());
}
private Integer berechneDauer(BdsmGameEntity session, Sperre sperre) {
Integer minuten = 30;
if (sperre.getMinutenVon() != null) {
if (sperre.getMinutenBis() != null) {
minuten = new Random().nextInt(sperre.getMinutenVon(), sperre.getMinutenBis());
} else {
minuten = sperre.getMinutenVon();
}
}
if (session.getZeitfaktorZeitstrafen() != null) {
minuten = (int) (minuten * session.getZeitfaktorZeitstrafen());
}
if (minuten == 0) {
minuten = 1;
}
return minuten;
}
}

View File

@@ -0,0 +1,22 @@
package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID;
public class SperrenVerlaengernCallback extends Callback {
private UUID spielerId;
private Integer faktor;
public UUID getSpielerId() { return spielerId; }
public void setSpielerId(UUID spielerId) { this.spielerId = spielerId; }
public Integer getFaktor() { return faktor; }
public void setFaktor(Integer faktor) { this.faktor = faktor; }
@Override
public String toString() {
return "SperrenVerlaengernCallback[sessionId=" + getSessionId() + ", spielerId=" + spielerId + ", faktor=" + faktor + "]";
}
}

View File

@@ -0,0 +1,6 @@
package de.oaa.xxx.games.chastity.cardlock;
public interface Card {
public CardDTO processCard(CardLockService lock);
}

View File

@@ -0,0 +1,35 @@
package de.oaa.xxx.games.chastity.cardlock;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.LinkedHashMap;
import java.util.Map;
@Converter
public class CardCountMapConverter implements AttributeConverter<Map<String, Integer>, String> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Map<String, Integer> map) {
if (map == null || map.isEmpty()) return null;
try {
return mapper.writeValueAsString(map);
} catch (Exception e) {
return null;
}
}
@Override
public Map<String, Integer> convertToEntityAttribute(String json) {
if (json == null || json.isBlank()) return new LinkedHashMap<>();
try {
return mapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) {
return new LinkedHashMap<>();
}
}
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock;
public record CardDTO (CardEnum card, String unlockCode){
}

View File

@@ -0,0 +1,63 @@
package de.oaa.xxx.games.chastity.cardlock;
public enum CardEnum {
RED {
@Override
public Card get() {
return new RedCard();
}
},
GREEN {
@Override
public Card get() {
return new GreenCard();
}
},
YELLOW {
@Override
public Card get() {
return new YellowCard();
}
},
TASK {
@Override
public Card get() {
return new TaskCard();
}
},
FREEZE {
@Override
public Card get() {
return new FreezeCard();
}
},
RESET {
@Override
public Card get() {
return new ResetCard();
}
},
DOUBLE_UP {
@Override
public Card get() {
return new DoubleUpCard();
}
},
CUM {
@Override
public Card get() {
return new CumCard();
}
},
CUM_IN_CAGE {
@Override
public Card get() {
return new CumInCageCard();
}
};
public abstract Card get();
}

View File

@@ -0,0 +1,36 @@
package de.oaa.xxx.games.chastity.cardlock;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.ArrayList;
import java.util.List;
@Converter
public class CardEnumListConverter implements AttributeConverter<List<CardEnum>, String> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<CardEnum> list) {
if (list == null || list.isEmpty()) return null;
try {
return mapper.writeValueAsString(list.stream().map(Enum::name).toList());
} catch (Exception e) {
return null;
}
}
@Override
public List<CardEnum> convertToEntityAttribute(String json) {
if (json == null || json.isBlank()) return new ArrayList<>();
try {
List<String> names = mapper.readValue(json, new TypeReference<>() {});
return new ArrayList<>(names.stream().map(CardEnum::valueOf).toList());
} catch (Exception e) {
return new ArrayList<>();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.time.LocalDateTime;
import java.util.List;
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@DiscriminatorValue("CARDLOCK")
public class CardLockEntity extends BaseLockEntity {
@Convert(converter = CardEnumListConverter.class)
@Column(columnDefinition = "TEXT")
private List<CardEnum> initialCards;
@Column
private Integer pickEveryMinute;
@Column
private boolean accumulatePicks;
@Column
private boolean showRemainingCards;
@Column
private LocalDateTime latestOpeningtime;
// State
@Column
private LocalDateTime nextCardIn;
@Column
private Integer openPicks;
@Convert(converter = CardEnumListConverter.class)
@Column(columnDefinition = "TEXT")
private List<CardEnum> availableCards;
@Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Task> tasksInQueue;
}

View File

@@ -0,0 +1,14 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
public interface CardLockRepository extends JpaRepository<CardLockEntity, UUID> {
@Modifying
@Query("DELETE FROM CardLockEntity c WHERE c.lockId = :lockId")
void deleteByLockId(UUID lockId);
}

View File

@@ -0,0 +1,275 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
import de.oaa.xxx.games.chastity.common.BaseLockService;
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.history.GameType;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
public class CardLockService extends BaseLockService implements LockControlCallback {
private static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class);
private final CardLockEntity lock;
private final CardLockRepository cardLockRepository;
private String pendingTaskMode;
public CardLockService(
CardLockEntity lock,
CommunityVerificationVoteRepository communityVerificationVoteRepository,
CommunityVerificationRepository communityVerificationRepository,
KeyholderVerificationRepository keyholderVerificationRepository,
GameHistoryRepository gameHistoryRepository,
UserRepository userRepository,
KeyholderNotificationRepository keyholderNotificationRepository,
SystemMessageService systemMessageService,
UnlockCodeHistoryService unlockCodeHistoryService,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
CardLockRepository cardLockRepository,
LockControlFactory lockControlFactory) {
super(communityVerificationVoteRepository, communityVerificationRepository, keyholderVerificationRepository,
gameHistoryRepository, userRepository, keyholderNotificationRepository, systemMessageService,
unlockCodeHistoryService, keyholderTaskChoiceRepository, communityTaskVoteRepository);
this.lock = lock;
this.cardLockRepository = cardLockRepository;
// lockControl aus Entity-Typ wiederherstellen (für bereits laufende Locks)
if (lock.getControllType() != null) {
this.lockControl = lockControlFactory.create(lock.getControllType(), this, lock.getLockee());
}
}
// ── LockControl Setup ─────────────────────────────────────────────────────
/** Wird von CardLockServiceFactory gesetzt (package-private). */
void initLockControl(de.oaa.xxx.games.chastity.lockcontroll.LockControl lc) {
this.lockControl = lc;
}
// ── LockControlCallback ───────────────────────────────────────────────────
@Override
public void setUnlockCode(String code) {
lock.setUnlockCode(code);
cardLockRepository.save(lock);
}
@Override
public int getUnlockcodeLenght() {
return lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5;
}
// ── Abstract method implementations ──────────────────────────────────────
@Override
protected BaseLockEntity getLock() {
return lock;
}
@Override
protected void saveLock() {
cardLockRepository.save(lock);
}
@Override
protected GameType getGameType() {
return GameType.CARDLOCK;
}
@Override
protected void applyHygieneOvertime(Long overtime) {
LOGGER.debug("Apply {} Minutes Overtime");
if (lock.getFrozenUntil() != null) {
lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4));
} else {
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4));
}
LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
}
// ── Card drawing ──────────────────────────────────────────────────────────
public CardDTO getNextCard() {
LOGGER.debug("New Card requested by user {}", lock.getLockee());
CardDTO card = null;
if (lock.isKeyholderRequestedUnlock()
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
card = getGreenCard();
} else if (lock.isAccumulatePicks()) {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1);
}
if (lock.getOpenPicks() != null && lock.getOpenPicks() > 0) {
lock.setOpenPicks(lock.getOpenPicks() - 1);
card = getRandomCard();
}
} else {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute()));
card = getRandomCard();
}
}
cardLockRepository.save(lock);
return card;
}
private CardDTO getRandomCard() {
var cards = lock.getAvailableCards();
if (!cards.isEmpty()) {
var card = cards.get(new Random().nextInt(cards.size()));
LOGGER.debug("Card drafted: {}", card);
lock.getAvailableCards().remove(card);
return card.get().processCard(this);
}
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
return getGreenCard();
}
private CardDTO getGreenCard() {
return new CardDTO(CardEnum.GREEN, lock.getUnlockCode());
}
// ── Card effects ──────────────────────────────────────────────────────────
public String doubleUp() {
var cards = lock.getAvailableCards();
LOGGER.debug("Double up {} cards", cards.size());
lock.getAvailableCards().addAll(cards);
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
return "";
}
public String reset() {
LOGGER.debug("Reset to initial cards");
lock.setAvailableCards(lock.getInitialCards());
return "";
}
public String green() {
LOGGER.debug("Green Card drafted");
return lock.getUnlockCode();
}
public String freeze() {
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
freeze(multiplier);
return "";
}
private String freeze(double multiplier) {
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
lock.setFrozenUntil(frozenTill);
lock.setNextCardIn(frozenTill);
LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
return "";
}
/** Called by TaskCard. Dispatches based on TaskMode and stores result for controller. */
public String task() {
switch (lock.getTaskMode()) {
case RANDOM -> applyRandomTask();
case KEYHOLDER -> {
if (lock.isTestLock()) applyRandomTask();
else startKeyholderVote();
}
case COMMUNITY -> {
if (lock.isTestLock()) applyRandomTask();
else startCommunityVote();
}
}
pendingTaskMode = lock.getTaskMode().name();
return "";
}
/** Returns the TaskMode that was triggered by the last task() call, or null if no task card was drawn. */
public String getPendingTaskMode() {
return pendingTaskMode;
}
public String redCard() {
return "";
}
public String yellowCard() {
Random random = new Random();
if (random.nextBoolean()) {
for (int i = 0; i < random.nextInt(1, 3); i++) {
LOGGER.debug("Adding Red card");
lock.getAvailableCards().add(CardEnum.RED);
}
} else {
for (int i = 0; i < random.nextInt(1, 3); i++) {
LOGGER.debug("Removing Red card if possible");
lock.getAvailableCards().remove(CardEnum.RED);
}
}
return "";
}
public void putBackGreen() {
LOGGER.debug("Green Card was put Back");
lock.getAvailableCards().add(CardEnum.GREEN);
cardLockRepository.save(lock);
}
// ── Hygiene opening ───────────────────────────────────────────────────────
@Override
protected void afterHygieneClosing() {
if (lockControl != null) lockControl.lock();
}
public void startHygieneOpening() {
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
}
// ── Cum cards ─────────────────────────────────────────────────────────────
public String cum(boolean tempUnlock) {
if (tempUnlock) {
startTempOpening(TempOpeningReason.CARD, 0);
}
return lock.getUnlockCode();
}
// ── Assigned task penalty ─────────────────────────────────────────────────
public void applyAssignedTaskPenalty(AssignedTaskEntity task) {
if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) {
LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes());
if (lock.getFrozenUntil() == null || until.isAfter(lock.getFrozenUntil())) {
lock.setFrozenUntil(until);
lock.setNextCardIn(until);
}
}
if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) {
List<CardEnum> cards = new ArrayList<>(
lock.getAvailableCards() != null ? lock.getAvailableCards() : List.of());
for (int i = 0; i < task.getPenaltyRedCards(); i++) {
cards.add(CardEnum.RED);
}
lock.setAvailableCards(cards);
}
cardLockRepository.save(lock);
}
}

View File

@@ -0,0 +1,101 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.util.Optional;
import java.util.UUID;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.common.BaseLockService;
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.social.SystemMessageService;
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 CardLockRepository cardLockRepository;
private final CommunityVerificationRepository communityVerificationRepository;
private final CommunityVerificationVoteRepository communityVerificationVoteRepository;
private final GameHistoryRepository gameHistoryRepository;
private final UserRepository userRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService;
private final KeyholderNotificationRepository keyholderNotificationRepository;
private final KeyholderVerificationRepository keyholderVerificationRepository;
private final SystemMessageService systemMessageService;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final LockControlFactory lockControlFactory;
private final CardlockRepository cardlockRepository;
private final TimeLockRepository timeLockRepository;
public CardLockServiceFactory(
CommunityVerificationRepository communityVerificationRepository,
CommunityVerificationVoteRepository communityVerificationVoteRepository,
CardLockRepository cardLockRepository,
CardlockRepository cardlockRepository,
GameHistoryRepository gameHistoryRepository,
UserRepository userRepository,
KeyholderNotificationRepository keyholderNotificationRepository,
KeyholderVerificationRepository keyholderVerificationRepository,
UnlockCodeHistoryService unlockCodeHistoryService,
SystemMessageService systemMessageService,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
LockControlFactory lockControlFactory,
TimeLockRepository timeLockRepository) {
this.cardLockRepository = cardLockRepository;
this.cardlockRepository = cardlockRepository;
this.communityVerificationRepository = communityVerificationRepository;
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
this.keyholderNotificationRepository = keyholderNotificationRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.keyholderVerificationRepository = keyholderVerificationRepository;
this.systemMessageService = systemMessageService;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.lockControlFactory = lockControlFactory;
this.timeLockRepository = timeLockRepository;
}
public boolean hasActiveLock(UUID lockeeId) {
return BaseLockService.hasActiveLock(lockeeId, cardlockRepository, timeLockRepository);
}
public Optional<UUID> findActiveLockId(UUID lockeeId) {
var cardLock = cardlockRepository.findByLockee(lockeeId).stream()
.filter(l -> l.getUnlockTime() == null).findFirst();
if (cardLock.isPresent()) return Optional.of(cardLock.get().getLockId());
return timeLockRepository.findFirstByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId)
.map(l -> l.getLockId());
}
/**
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
* Setzt den lockControl anhand des gespeicherten controllType.
*/
public CardLockService create(CardLockEntity lock) {
CardLockService service = new CardLockService(lock, communityVerificationVoteRepository,
communityVerificationRepository, keyholderVerificationRepository, gameHistoryRepository,
userRepository, keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService,
keyholderTaskChoiceRepository, communityTaskVoteRepository, cardLockRepository, lockControlFactory);
return service;
}
}

View File

@@ -0,0 +1,13 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CardlockRepository extends JpaRepository<CardLockEntity, UUID> {
List<CardLockEntity> findByLockee(UUID lockee);
List<CardLockEntity> findByKeyholderAndUnlockTimeIsNull(UUID keyholder);
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
}

View File

@@ -0,0 +1,141 @@
package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.tasks.TaskMode;
import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateRepository;
import java.security.Principal;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/cardlock/templates")
public class CardlockTemplateController {
private final CardlockTemplateRepository templateRepository;
private final UserService userService;
private final TimeLockTemplateRepository timeLockTemplateRepository;
private final SubscriptionLimitService limitService;
public CardlockTemplateController(CardlockTemplateRepository templateRepository,
UserService userService,
TimeLockTemplateRepository timeLockTemplateRepository,
SubscriptionLimitService limitService) {
this.templateRepository = templateRepository;
this.userService = userService;
this.timeLockTemplateRepository = timeLockTemplateRepository;
this.limitService = limitService;
}
record TemplateRequest(
String name,
Map<String, Integer> cardCountsMin,
Map<String, Integer> cardCountsMax,
Integer pickEveryMinute,
boolean accumulatePicks,
boolean showRemainingCards,
Integer hygineOpeningDurationMinutes,
Integer hygineOpeningEveryMinites,
List<Task> tasks,
boolean requiresVerification,
TaskMode taskMode
) {}
private Map<String, Object> toDto(CardlockTemplateEntity t) {
Map<String, Object> dto = new LinkedHashMap<>();
dto.put("templateId", t.getTemplateId());
dto.put("name", t.getName());
dto.put("cardCountsMin", t.getCardCountsMin() != null ? t.getCardCountsMin() : Map.of());
dto.put("cardCountsMax", t.getCardCountsMax() != null ? t.getCardCountsMax() : Map.of());
dto.put("pickEveryMinute", t.getPickEveryMinute());
dto.put("accumulatePicks", t.isAccumulatePicks());
dto.put("showRemainingCards", t.isShowRemainingCards());
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
dto.put("requiresVerification", t.isRequiresVerification());
dto.put("taskCardMode", t.getTaskCardMode());
return dto;
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> list(Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
List<Map<String, Object>> result = templateRepository.findByOwner(myId)
.stream().map(this::toDto).collect(Collectors.toList());
return ResponseEntity.ok(result);
}
@PostMapping
public ResponseEntity<Map<String, Object>> create(@RequestBody TemplateRequest req, Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
if (req.pickEveryMinute() == null || req.pickEveryMinute() < 1)
return ResponseEntity.badRequest().build();
if (req.cardCountsMin() == null || req.cardCountsMin().isEmpty())
return ResponseEntity.badRequest().build();
long totalTemplates = templateRepository.countByOwner(myId)
+ timeLockTemplateRepository.countByOwner(myId);
if (totalTemplates >= limitService.maxLockTemplates(myId))
return ResponseEntity.status(409).header("X-Error", "limit-reached").build();
CardlockTemplateEntity t = new CardlockTemplateEntity();
t.setOwner(myId);
applyRequest(t, req);
templateRepository.save(t);
return ResponseEntity.ok(toDto(t));
}
@PutMapping("/{id}")
public ResponseEntity<Map<String, Object>> update(@PathVariable UUID id,
@RequestBody TemplateRequest req,
Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
var opt = templateRepository.findById(id);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
CardlockTemplateEntity t = opt.get();
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
if (req.pickEveryMinute() == null || req.pickEveryMinute() < 1)
return ResponseEntity.badRequest().build();
if (req.cardCountsMin() == null || req.cardCountsMin().isEmpty())
return ResponseEntity.badRequest().build();
applyRequest(t, req);
templateRepository.save(t);
return ResponseEntity.ok(toDto(t));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable UUID id, Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
var opt = templateRepository.findById(id);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
if (!opt.get().getOwner().equals(myId)) return ResponseEntity.status(403).build();
templateRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
private void applyRequest(CardlockTemplateEntity t, TemplateRequest req) {
t.setName(req.name());
t.setCardCountsMin(req.cardCountsMin());
t.setCardCountsMax(req.cardCountsMax() != null ? req.cardCountsMax() : req.cardCountsMin());
t.setPickEveryMinute(req.pickEveryMinute());
t.setAccumulatePicks(req.accumulatePicks());
t.setShowRemainingCards(req.showRemainingCards());
t.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites());
t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
t.setTasks(req.tasks() != null ? req.tasks() : List.of());
t.setRequiresVerification(req.requiresVerification());
t.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
}
}

View File

@@ -0,0 +1,34 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.util.Map;
import de.oaa.xxx.games.chastity.common.BaseLockTemplateEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@DiscriminatorValue("CARDLOCK")
public class CardlockTemplateEntity extends BaseLockTemplateEntity {
@Convert(converter = CardCountMapConverter.class)
@Column(columnDefinition = "TEXT")
private Map<String, Integer> cardCountsMin;
@Convert(converter = CardCountMapConverter.class)
@Column(columnDefinition = "TEXT")
private Map<String, Integer> cardCountsMax;
@Column
private Integer pickEveryMinute;
@Column
private boolean accumulatePicks;
@Column
private boolean showRemainingCards;
@Column
private boolean requiresVerification;
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.chastity.cardlock;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface CardlockTemplateRepository extends JpaRepository<CardlockTemplateEntity, UUID> {
List<CardlockTemplateEntity> findByOwner(UUID owner);
long countByOwner(UUID owner);
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class CumCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.CUM, lock.cum(true));
}
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class CumInCageCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.CUM_IN_CAGE, lock.cum(false));
}
}

View File

@@ -0,0 +1,9 @@
package de.oaa.xxx.games.chastity.cardlock;
public class DoubleUpCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.DOUBLE_UP, lock.doubleUp());
}
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class FreezeCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.FREEZE, lock.freeze());
}
}

View File

@@ -0,0 +1,9 @@
package de.oaa.xxx.games.chastity.cardlock;
public class GreenCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.GREEN, lock.green());
}
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class RedCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.RED, lock.redCard());
}
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class ResetCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.RESET, lock.reset());
}
}

View File

@@ -0,0 +1,10 @@
package de.oaa.xxx.games.chastity.cardlock;
public class TaskCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.TASK, lock.task());
}
}

View File

@@ -0,0 +1,9 @@
package de.oaa.xxx.games.chastity.cardlock;
public class YellowCard implements Card {
@Override
public CardDTO processCard(CardLockService lock) {
return new CardDTO(CardEnum.YELLOW, lock.yellowCard());
}
}

View File

@@ -0,0 +1,22 @@
package de.oaa.xxx.games.chastity.common;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.chastity.tasks.Task;
@RestController
@RequestMapping("games/chastity/lock/")
public class BaseLockController {
@GetMapping("/{lockId}/tasks")
public ResponseEntity<List<Task>> getTasks() {
return null;
}
}

View File

@@ -0,0 +1,105 @@
package de.oaa.xxx.games.chastity.common;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import de.oaa.xxx.games.chastity.lockcontroll.LockControllType;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
import de.oaa.xxx.games.chastity.tasks.TaskMode;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "active_lock")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "lock_type", discriminatorType = DiscriminatorType.STRING)
public class BaseLockEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID lockId;
@Column(nullable = false)
private String name;
// --- Gemeinsame Settings ---
@Column(nullable = false)
private UUID lockee;
@Column
private UUID keyholder;
@Column(nullable = false)
private boolean testLock;
@Column
private boolean requiresVerification;
@Column
private Integer unlockCodeLength;
@Column
private String unlockCode;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private LockControllType controllType;
// --- Timing & Hygiene ---
@Column
private LocalDateTime startTime;
@Column
private LocalDateTime unlockTime;
@Column
private LocalDateTime lastHygineOpening;
@Column
private Integer hygineOpeningDurationMinutes;
@Column
private Integer hygineOpeningEveryMinites;
@Column
private LocalDateTime tempOpeningTime; // If null, not while hygine opening
@Column
private Integer tempOpeningDuration;
@Column
private TempOpeningReason tempOpeningReason;
@Column
private LocalDateTime frozenUntil;
// --- Aufgaben-System (Basis) ---
@Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Task> tasks;
@Column
private String currentTask;
@Column(columnDefinition = "TEXT")
private String currentTaskDescription;
@Column
private LocalDateTime taskUntil;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TaskMode taskMode = TaskMode.RANDOM;
// --- Notfall- & Keyholder-Status ---
@Column(nullable = false)
private boolean keyholderRequestedUnlock = false;
@Column
private LocalDateTime emergencyUnlockRequestedAt;
@Column(nullable = false)
private boolean emergencyAutoUnlocked = false;
// Getter & Setter
public TaskMode getTaskMode() {
return taskMode != null ? taskMode : TaskMode.RANDOM;
}
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.chastity.common;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BaseLockRepository extends JpaRepository<BaseLockEntity, UUID>{
Optional<BaseLockEntity> findByLockee(UUID userId);
}

View File

@@ -0,0 +1,310 @@
package de.oaa.xxx.games.chastity.common;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity;
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
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.user.UserRepository;
public abstract class BaseLockService {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseLockService.class);
protected final CommunityVerificationVoteRepository communityVerificationVoteRepository;
protected final CommunityVerificationRepository communityVerificationRepository;
protected final KeyholderVerificationRepository keyholderVerificationRepository;
protected final GameHistoryRepository gameHistoryRepository;
protected final UserRepository userRepository;
protected final KeyholderNotificationRepository keyholderNotificationRepository;
protected final SystemMessageService systemMessageService;
protected final UnlockCodeHistoryService unlockCodeHistoryService;
protected final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
protected final CommunityTaskVoteRepository communityTaskVoteRepository;
/** Wird von Subklassen gesetzt; steuert wie das physische Schloss (neu) verriegelt wird. */
protected de.oaa.xxx.games.chastity.lockcontroll.LockControl lockControl;
public de.oaa.xxx.games.chastity.lockcontroll.LockControl getLockControl() {
return lockControl;
}
// ── Abstrakte Methoden ────────────────────────────────────────────────────
protected abstract BaseLockEntity getLock();
protected abstract void saveLock();
protected abstract GameType getGameType();
/** Wie wird Überschreitung der Hygiene-Öffnung bestraft? CardLock friert ein, TimeLock verlängert die Zeit. */
protected abstract void applyHygieneOvertime(Long overtime);
// ── Hook-Methoden (Standard: No-Op) ───────────────────────────────────────
/** TimeLock: lockControl.unlock() vor dem finalen Entsperren aufrufen. */
protected void beforePhysicalUnlock() {}
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
protected void afterHygieneClosing() {}
public BaseLockService(
CommunityVerificationVoteRepository communityVerificationVoteRepository,
CommunityVerificationRepository communityVerificationRepository,
KeyholderVerificationRepository keyholderVerificationRepository,
GameHistoryRepository gameHistoryRepository,
UserRepository userRepository,
KeyholderNotificationRepository keyholderNotificationRepository,
SystemMessageService systemMessageService,
UnlockCodeHistoryService unlockCodeHistoryService,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository) {
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
this.communityVerificationRepository = communityVerificationRepository;
this.keyholderVerificationRepository = keyholderVerificationRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
this.keyholderNotificationRepository = keyholderNotificationRepository;
this.systemMessageService = systemMessageService;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
}
// ── Lockee-Prüfung ────────────────────────────────────────────────────────
/**
* Prüft ob der Anwender bereits ein aktives Lock (CardLock oder TimeLock) als Lockee hat.
* Ein Lock gilt als aktiv wenn startTime gesetzt und unlockTime null ist.
*/
public static boolean hasActiveLock(UUID lockeeId, CardlockRepository cardlockRepo,
TimeLockRepository timelockRepo) {
return cardlockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId)
|| timelockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId);
}
// ── Gemeinsame Hilfsmethoden ──────────────────────────────────────────────
protected Long calcOvertime() {
LocalDateTime now = LocalDateTime.now();
BaseLockEntity lock = getLock();
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
if (now.isAfter(dueTime)) {
return ChronoUnit.MINUTES.between(dueTime, now);
}
}
return null;
}
protected void reportKeyholder(Long overtime) {
BaseLockEntity lock = getLock();
KeyholderNotificationEntity notification = new KeyholderNotificationEntity();
notification.setLockId(lock.getLockId());
notification.setLockeeId(lock.getLockee());
notification.setKeyholderUserId(lock.getKeyholder());
notification.setViolationTime(LocalDateTime.now());
notification.setOvertimeMinutes(overtime);
notification.setOpeningReason(de.oaa.xxx.games.chastity.unlock.TempOpeningReason.HYGIENE);
keyholderNotificationRepository.save(notification);
userRepository.findById(lock.getKeyholder()).ifPresent(kh ->
sendMessage(lock.getLockee(), kh.getUserId(),
"Deine Lockee hat die Hygiene-Öffnung um " + overtime + " Minuten überschritten.",
"/games/chastity/keyholder.html?lockId=" + lock.getLockId(),
de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
}
protected void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl,
de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
}
// ── Aufgaben ──────────────────────────────────────────────────────────────
public void task(Task task) {
BaseLockEntity lock = getLock();
LOGGER.debug("Apply task {}", task);
lock.setCurrentTask(task.getTitle());
lock.setCurrentTaskDescription(task.getDescription());
if (task.getMinutes() != null && task.getMinutes() > 0) {
lock.setTaskUntil(LocalDateTime.now().plusMinutes(task.getMinutes()));
}
}
public String clearTask() {
BaseLockEntity lock = getLock();
LOGGER.debug("Clear task");
lock.setCurrentTask(null);
lock.setCurrentTaskDescription(null);
lock.setTaskUntil(null);
return "";
}
protected void applyRandomTask() {
LOGGER.debug("Apply random task");
var tasks = getLock().getTasks();
if (tasks != null && !tasks.isEmpty()) {
task(tasks.get(new Random().nextInt(tasks.size())));
}
}
protected void startKeyholderVote() {
BaseLockEntity lock = getLock();
KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity();
choice.setLockId(lock.getLockId());
choice.setCreatedAt(LocalDateTime.now());
choice.setActive(true);
choice.setExpiresAt(LocalDateTime.now().plusHours(1));
keyholderTaskChoiceRepository.save(choice);
userRepository.findById(lock.getKeyholder())
.ifPresent(kh -> sendMessage(lock.getLockee(), kh.getUserId(),
"Deine Lockee hat eine Aufgaben-Karte gezogen wähle eine Aufgabe aus.",
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
}
protected void startCommunityVote() {
BaseLockEntity lock = getLock();
CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity();
vote.setLockId(lock.getLockId());
vote.setCreatedAt(LocalDateTime.now());
vote.setExpiresAt(LocalDateTime.now().plusHours(1));
vote.setActive(true);
communityTaskVoteRepository.save(vote);
}
// ── Temporäre Öffnung ─────────────────────────────────────────────────────
protected void startTempOpening(TempOpeningReason reason, Integer duration) {
BaseLockEntity lock = getLock();
assert duration != null;
lock.setTempOpeningReason(reason);
lock.setTempOpeningTime(LocalDateTime.now());
lock.setTempOpeningDuration(duration);
saveLock();
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
}
public String endTempOpening() {
var lock = getLock();
var now = LocalDateTime.now();
var overtime = calcOvertime();
if (overtime != null) {
if (lock.getKeyholder() != null) {
reportKeyholder(overtime);
}
applyHygieneOvertime(overtime);
}
afterHygieneClosing();
if (TempOpeningReason.HYGIENE == lock.getTempOpeningReason()) {
lock.setLastHygineOpening(now);
}
lock.setTempOpeningReason(null);
lock.setTempOpeningDuration(null);
lock.setTempOpeningTime(null);
if (lockControl != null
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
lockControl.lock();
saveLock();
return lock.getUnlockCode() != null ? lock.getUnlockCode() : "";
}
var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5);
lock.setUnlockCode(code);
saveLock();
return code;
}
// ── Entsperren ────────────────────────────────────────────────────────────
public void unlock(String unlockCode) {
BaseLockEntity lock = getLock();
beforePhysicalUnlock();
lock.setUnlockTime(LocalDateTime.now());
boolean valid = true;
if (lock.isEmergencyAutoUnlocked()) {
valid = false;
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
} else if (lock.isTestLock()) {
valid = false;
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications;
if (lock.getKeyholder() != null) {
verifications = keyholderVerificationRepository.findByLockId(lock.getLockId()).stream()
.filter(v -> v.isValid())
.map(v -> v.getVerificationDate())
.collect(Collectors.toSet());
} else {
verifications = communityVerificationRepository.findByLockId(lock.getLockId()).stream()
.filter(v -> v.isValid())
.map(v -> v.getVerificationDate())
.collect(Collectors.toSet());
}
LocalDate current = lock.getStartTime().toLocalDate();
LocalDate last = lock.getUnlockTime().toLocalDate().minusDays(1);
while (!current.isAfter(last)) {
if (!verifications.contains(current)) {
valid = false;
LOGGER.debug("Lock invalid - no daily verification on {}", current);
break;
}
current = current.plusDays(1);
}
}
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
saveLock();
if (lockControl != null) {
lockControl.cleanup();
}
if (valid) {
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
GameHistoryEntity entry = new GameHistoryEntity();
entry.setGameType(getGameType());
entry.setGameName(lock.getName());
entry.setStartTime(lock.getStartTime());
entry.setEndTime(lock.getUnlockTime());
entry.setDurationMinutes(durationMinutes);
entry.addParticipant(lock.getLockee(), GameRole.LOCKEE);
if (lock.getKeyholder() != null) {
entry.addParticipant(lock.getKeyholder(), GameRole.KEYHOLDER);
}
gameHistoryRepository.save(entry);
int minutes = (int) durationMinutes;
userRepository.findById(lock.getLockee()).ifPresent(u -> {
u.setLockeeXp(u.getLockeeXp() + minutes);
userRepository.save(u);
});
if (lock.getKeyholder() != null) {
userRepository.findById(lock.getKeyholder()).ifPresent(u -> {
u.setKeyholderXp(u.getKeyholderXp() + minutes);
userRepository.save(u);
});
}
}
}
}

View File

@@ -0,0 +1,116 @@
package de.oaa.xxx.games.chastity.common;
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateEntity;
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity;
import de.oaa.xxx.user.UserService;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.*;
@RestController
@RequestMapping("/templates")
public class BaseLockTemplateController {
private final BaseLockTemplateRepository templateRepository;
private final UserService userService;
public BaseLockTemplateController(BaseLockTemplateRepository templateRepository,
UserService userService) {
this.templateRepository = templateRepository;
this.userService = userService;
}
@GetMapping
public ResponseEntity<Map<String, Object>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
var pageable = PageRequest.of(page, Math.min(size, 50), Sort.by("name"));
var pageResult = templateRepository.findByOwner(myId, pageable);
var content = pageResult.getContent().stream().map(t -> {
Map<String, Object> dto = new LinkedHashMap<>();
dto.put("templateId", t.getTemplateId());
dto.put("name", t.getName());
dto.put("lockType", t instanceof CardlockTemplateEntity ? "CARDLOCK" : "TIMELOCK");
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
dto.put("requiresVerification", t.isRequiresVerification());
dto.put("published", t.isPublished());
dto.put("showAuthor", t.isShowAuthor());
return dto;
}).toList();
Map<String, Object> response = new LinkedHashMap<>();
response.put("content", content);
response.put("page", pageResult.getNumber());
response.put("totalPages", pageResult.getTotalPages());
response.put("last", pageResult.isLast());
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
var opt = templateRepository.findById(id);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
var t = opt.get();
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
if (t instanceof CardlockTemplateEntity c) {
return ResponseEntity.ok(toCardlockDto(c));
} else if (t instanceof TimeLockTemplateEntity tl) {
return ResponseEntity.ok(toTimelockDto(tl));
}
return ResponseEntity.notFound().build();
}
private Map<String, Object> toCardlockDto(CardlockTemplateEntity t) {
Map<String, Object> dto = new LinkedHashMap<>();
dto.put("_type", "CARDLOCK");
dto.put("templateId", t.getTemplateId());
dto.put("name", t.getName());
dto.put("cardCountsMin", t.getCardCountsMin() != null ? t.getCardCountsMin() : Map.of());
dto.put("cardCountsMax", t.getCardCountsMax() != null ? t.getCardCountsMax() : Map.of());
dto.put("pickEveryMinute", t.getPickEveryMinute());
dto.put("accumulatePicks", t.isAccumulatePicks());
dto.put("showRemainingCards", t.isShowRemainingCards());
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
dto.put("requiresVerification", t.isRequiresVerification());
dto.put("taskCardMode", t.getTaskCardMode());
return dto;
}
private Map<String, Object> toTimelockDto(TimeLockTemplateEntity t) {
Map<String, Object> dto = new LinkedHashMap<>();
dto.put("_type", "TIMELOCK");
dto.put("templateId", t.getTemplateId());
dto.put("name", t.getName());
dto.put("minTimeInMinutes", t.getMinTimeInMinutes());
dto.put("maxTimeInMinutes", t.getMaxTimeInMinutes());
dto.put("endTimeVisible", t.isEndTimeVisible());
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
dto.put("taskEveryMinutes", t.getTaskEveryMinutes());
dto.put("minTasksPerDay", t.getMinTasksPerDay());
dto.put("spinningWheelEntries", t.getSpinningWheelEntries() != null ? t.getSpinningWheelEntries() : List.of());
dto.put("spinsEveryMinutes", t.getSpinsEveryMinutes());
dto.put("minSpinsPerDay", t.getMinSpinsPerDay());
dto.put("requiresVerification", t.isRequiresVerification());
dto.put("taskMode", t.getTaskCardMode());
dto.put("penaltyType", t.getPenaltyType());
dto.put("penaltyValue", t.getPenaltyValue());
return dto;
}
}

View File

@@ -0,0 +1,63 @@
package de.oaa.xxx.games.chastity.common;
import java.util.List;
import java.util.UUID;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
import de.oaa.xxx.games.chastity.tasks.TaskMode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "lock_template")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "lock_type", discriminatorType = DiscriminatorType.STRING)
public class BaseLockTemplateEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID templateId;
@Column(nullable = false)
private UUID owner;
@Column
private String name;
@Column
private Integer hygineOpeningDurationMinutes;
@Column
private Integer hygineOpeningEveryMinites;
@Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Task> tasks;
@Column
private boolean requiresVerification;
@Column(nullable = false)
private TaskMode taskMode = TaskMode.RANDOM;
@Column(nullable = false)
private boolean published = false;
@Column(nullable = false)
private boolean showAuthor = false;
@Column(nullable = false)
private long subscriberCount = 0;
public TaskMode getTaskCardMode() {
return taskMode != null ? taskMode : TaskMode.RANDOM;
}
}

View File

@@ -0,0 +1,16 @@
package de.oaa.xxx.games.chastity.common;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BaseLockTemplateRepository extends JpaRepository<BaseLockTemplateEntity, UUID> {
List<BaseLockTemplateEntity> findByOwner(UUID owner);
List<BaseLockTemplateEntity> findByOwnerAndPublishedTrue(UUID owner);
Page<BaseLockTemplateEntity> findByOwner(UUID owner, Pageable pageable);
Page<BaseLockTemplateEntity> findByPublishedTrue(Pageable pageable);
Page<BaseLockTemplateEntity> findByPublishedTrueAndNameContainingIgnoreCase(String name, Pageable pageable);
}

View File

@@ -0,0 +1,25 @@
package de.oaa.xxx.games.chastity.common;
import java.util.Random;
public class CodeCreator {
private static final String CHARS_AN = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
private static final String CHARS_N = "0123456789";
public static String createNumeric(int digits) {
return create(digits, CHARS_N);
}
public static String createAlphanumeric(int digits) {
return create(digits, CHARS_AN);
}
private static String create(int digits, String chars) {
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < digits; i++) {
sb.append(chars.charAt(new Random().nextInt(chars.length())));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,6 @@
package de.oaa.xxx.games.chastity.common;
public enum LockType {
CARD, TIMED;
}

View File

@@ -0,0 +1,6 @@
package de.oaa.xxx.games.chastity.common;
public enum PenaltyType {
ADD, FREEZE, PILLORY;
}

Some files were not shown because too many files have changed in this diff Show More