Verschiebung nach anderem RePo - nun pro Projekt getrennt
This commit is contained in:
14
src/main/java/de/oaa/xxx/XxxThegameApplication.java
Normal file
14
src/main/java/de/oaa/xxx/XxxThegameApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
563
src/main/java/de/oaa/xxx/admin/AdminController.java
Normal file
563
src/main/java/de/oaa/xxx/admin/AdminController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
38
src/main/java/de/oaa/xxx/admin/AdminEntity.java
Normal file
38
src/main/java/de/oaa/xxx/admin/AdminEntity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/main/java/de/oaa/xxx/admin/AdminRepository.java
Normal file
13
src/main/java/de/oaa/xxx/admin/AdminRepository.java
Normal 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);
|
||||
}
|
||||
5
src/main/java/de/oaa/xxx/admin/AdminRolle.java
Normal file
5
src/main/java/de/oaa/xxx/admin/AdminRolle.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.admin;
|
||||
|
||||
public enum AdminRolle {
|
||||
ADMIN, SUPERADMIN
|
||||
}
|
||||
48
src/main/java/de/oaa/xxx/config/JwtFilter.java
Normal file
48
src/main/java/de/oaa/xxx/config/JwtFilter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
49
src/main/java/de/oaa/xxx/config/JwtService.java
Normal file
49
src/main/java/de/oaa/xxx/config/JwtService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
36
src/main/java/de/oaa/xxx/config/SchemaMigration.java
Normal file
36
src/main/java/de/oaa/xxx/config/SchemaMigration.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/main/java/de/oaa/xxx/config/SecurityConfig.java
Normal file
119
src/main/java/de/oaa/xxx/config/SecurityConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
38
src/main/java/de/oaa/xxx/config/StringListConverter.java
Normal file
38
src/main/java/de/oaa/xxx/config/StringListConverter.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/main/java/de/oaa/xxx/config/ThemeController.java
Normal file
54
src/main/java/de/oaa/xxx/config/ThemeController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
125
src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java
Normal file
125
src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java
Normal 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");
|
||||
}
|
||||
}
|
||||
45
src/main/java/de/oaa/xxx/emailchange/EmailChangeEntity.java
Normal file
45
src/main/java/de/oaa/xxx/emailchange/EmailChangeEntity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
408
src/main/java/de/oaa/xxx/feed/FeedController.java
Normal file
408
src/main/java/de/oaa/xxx/feed/FeedController.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/main/java/de/oaa/xxx/feed/dto/FeedItemDto.java
Normal file
28
src/main/java/de/oaa/xxx/feed/dto/FeedItemDto.java
Normal 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
|
||||
) {}
|
||||
12
src/main/java/de/oaa/xxx/feed/dto/FeedPostRequest.java
Normal file
12
src/main/java/de/oaa/xxx/feed/dto/FeedPostRequest.java
Normal 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
|
||||
) {}
|
||||
45
src/main/java/de/oaa/xxx/feed/entity/FeedPostEntity.java
Normal file
45
src/main/java/de/oaa/xxx/feed/entity/FeedPostEntity.java
Normal 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;
|
||||
}
|
||||
30
src/main/java/de/oaa/xxx/feed/entity/FeedPostLikeEntity.java
Normal file
30
src/main/java/de/oaa/xxx/feed/entity/FeedPostLikeEntity.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
27
src/main/java/de/oaa/xxx/feed/entity/FeedPostVoteEntity.java
Normal file
27
src/main/java/de/oaa/xxx/feed/entity/FeedPostVoteEntity.java
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
89
src/main/java/de/oaa/xxx/feedback/FeedbackController.java
Normal file
89
src/main/java/de/oaa/xxx/feedback/FeedbackController.java
Normal 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("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
}
|
||||
38
src/main/java/de/oaa/xxx/feedback/FeedbackEntity.java
Normal file
38
src/main/java/de/oaa/xxx/feedback/FeedbackEntity.java
Normal 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;
|
||||
}
|
||||
11
src/main/java/de/oaa/xxx/feedback/FeedbackRepository.java
Normal file
11
src/main/java/de/oaa/xxx/feedback/FeedbackRepository.java
Normal 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);
|
||||
}
|
||||
7
src/main/java/de/oaa/xxx/feedback/FeedbackStatus.java
Normal file
7
src/main/java/de/oaa/xxx/feedback/FeedbackStatus.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.feedback;
|
||||
|
||||
public enum FeedbackStatus {
|
||||
UNGELESEN,
|
||||
IN_ARBEIT,
|
||||
BEANTWORTET
|
||||
}
|
||||
28
src/main/java/de/oaa/xxx/games/bdsm/AktiveSperre.java
Normal file
28
src/main/java/de/oaa/xxx/games/bdsm/AktiveSperre.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
||||
25
src/main/java/de/oaa/xxx/games/bdsm/AufgabeAnzeige.java
Normal file
25
src/main/java/de/oaa/xxx/games/bdsm/AufgabeAnzeige.java
Normal 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) + "]";
|
||||
}
|
||||
}
|
||||
7
src/main/java/de/oaa/xxx/games/bdsm/AufgabeArt.java
Normal file
7
src/main/java/de/oaa/xxx/games/bdsm/AufgabeArt.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.games.bdsm;
|
||||
|
||||
public enum AufgabeArt {
|
||||
AUFGABE,
|
||||
STRAFE,
|
||||
SPERRE;
|
||||
}
|
||||
32
src/main/java/de/oaa/xxx/games/bdsm/BdsmGame.java
Normal file
32
src/main/java/de/oaa/xxx/games/bdsm/BdsmGame.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
||||
282
src/main/java/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.java
Normal file
282
src/main/java/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.java
Normal 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;
|
||||
}
|
||||
}
|
||||
208
src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java
Normal file
208
src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
44
src/main/java/de/oaa/xxx/games/bdsm/BdsmMitspieler.java
Normal file
44
src/main/java/de/oaa/xxx/games/bdsm/BdsmMitspieler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/main/java/de/oaa/xxx/games/bdsm/Callback.java
Normal file
16
src/main/java/de/oaa/xxx/games/bdsm/Callback.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
||||
7
src/main/java/de/oaa/xxx/games/bdsm/GeschlechtEnum.java
Normal file
7
src/main/java/de/oaa/xxx/games/bdsm/GeschlechtEnum.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.games.bdsm;
|
||||
|
||||
public enum GeschlechtEnum {
|
||||
WEIBLICH,
|
||||
DIVERS,
|
||||
MAENNLICH;
|
||||
}
|
||||
8
src/main/java/de/oaa/xxx/games/bdsm/RolleEnum.java
Normal file
8
src/main/java/de/oaa/xxx/games/bdsm/RolleEnum.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package de.oaa.xxx.games.bdsm;
|
||||
|
||||
public enum RolleEnum {
|
||||
BESTRAFUNG_AKTIV,
|
||||
BESTRAFUNG_PASSIV,
|
||||
AUFGABE_AKTIV,
|
||||
AUFGABE_PASSIV;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
public interface Card {
|
||||
|
||||
public CardDTO processCard(CardLockService lock);
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
public record CardDTO (CardEnum card, String unlockCode){
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
public enum LockType {
|
||||
|
||||
CARD, TIMED;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
public enum PenaltyType {
|
||||
|
||||
ADD, FREEZE, PILLORY;
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateRepository;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
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.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/templates")
|
||||
public class TemplateExploreController {
|
||||
|
||||
private final BaseLockTemplateRepository templateRepository;
|
||||
private final TemplateSubscriptionRepository subscriptionRepository;
|
||||
private final TimeLockTemplateRepository timeLockTemplateRepository;
|
||||
private final CardlockTemplateRepository cardlockTemplateRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public TemplateExploreController(BaseLockTemplateRepository templateRepository,
|
||||
TemplateSubscriptionRepository subscriptionRepository,
|
||||
TimeLockTemplateRepository timeLockTemplateRepository,
|
||||
CardlockTemplateRepository cardlockTemplateRepository,
|
||||
UserRepository userRepository,
|
||||
UserService userService) {
|
||||
this.templateRepository = templateRepository;
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
this.timeLockTemplateRepository = timeLockTemplateRepository;
|
||||
this.cardlockTemplateRepository = cardlockTemplateRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
// ── Öffentliche Vorlagen entdecken ────────────────────────────────────────
|
||||
|
||||
@GetMapping("/public")
|
||||
public ResponseEntity<Map<String, Object>> getPublicTemplates(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "") String q,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
size = Math.min(size, 20);
|
||||
var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "subscriberCount"));
|
||||
|
||||
Page<BaseLockTemplateEntity> pageResult = q.isBlank()
|
||||
? templateRepository.findByPublishedTrue(pageable)
|
||||
: templateRepository.findByPublishedTrueAndNameContainingIgnoreCase(q.trim(), pageable);
|
||||
|
||||
Set<UUID> subscribedIds = subscriptionRepository.findByUserId(myId)
|
||||
.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||
|
||||
List<Map<String, Object>> content = pageResult.getContent().stream()
|
||||
.map(t -> toPublicDto(t, myId, subscribedIds))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"content", content,
|
||||
"page", pageResult.getNumber(),
|
||||
"hasMore", !pageResult.isLast()
|
||||
));
|
||||
}
|
||||
|
||||
// ── Einzelne Vorlage per ID (für Detail-Dialog) ──────────────────────────
|
||||
|
||||
@GetMapping("/{id}/public")
|
||||
public ResponseEntity<Map<String, Object>> getTemplate(
|
||||
@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var tOpt = templateRepository.findById(id);
|
||||
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var t = tOpt.get();
|
||||
|
||||
// Sichtbar wenn: eigene Vorlage ODER veröffentlicht
|
||||
if (!t.getOwner().equals(myId) && !t.isPublished()) return ResponseEntity.status(403).build();
|
||||
|
||||
Set<UUID> subscribedIds = subscriptionRepository.findByUserId(myId)
|
||||
.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||
return ResponseEntity.ok(toPublicDto(t, myId, subscribedIds));
|
||||
}
|
||||
|
||||
// ── Eigene Vorlagen (für Auswahl-Dropdown) ───────────────────────────────
|
||||
|
||||
@GetMapping("/mine")
|
||||
public ResponseEntity<List<Map<String, Object>>> getMyTemplates(Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
List<Map<String, Object>> result = templateRepository.findByOwner(myId).stream()
|
||||
.map(t -> toPublicDto(t, myId, Set.of()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Abonnierte Vorlagen ───────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/subscribed")
|
||||
public ResponseEntity<List<Map<String, Object>>> getSubscribedTemplates(Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
List<TemplateSubscriptionEntity> subs = subscriptionRepository.findByUserId(myId);
|
||||
Set<UUID> subscribedIds = subs.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (var sub : subs) {
|
||||
templateRepository.findById(sub.getTemplateId()).ifPresent(t -> {
|
||||
if (t.isPublished()) {
|
||||
result.add(toPublicDto(t, myId, subscribedIds));
|
||||
}
|
||||
});
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Abonnieren ────────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{id}/subscribe")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> subscribe(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var tOpt = templateRepository.findById(id);
|
||||
if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build();
|
||||
if (tOpt.get().getOwner().equals(myId)) return ResponseEntity.status(409).build();
|
||||
if (subscriptionRepository.findByUserIdAndTemplateId(myId, id).isPresent())
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
var sub = new TemplateSubscriptionEntity();
|
||||
sub.setUserId(myId);
|
||||
sub.setTemplateId(id);
|
||||
sub.setSubscribedAt(LocalDateTime.now());
|
||||
subscriptionRepository.save(sub);
|
||||
|
||||
var t = tOpt.get();
|
||||
t.setSubscriberCount(t.getSubscriberCount() + 1);
|
||||
templateRepository.save(t);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Abo kündigen ──────────────────────────────────────────────────────────
|
||||
|
||||
@DeleteMapping("/{id}/subscribe")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> unsubscribe(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var subOpt = subscriptionRepository.findByUserIdAndTemplateId(myId, id);
|
||||
if (subOpt.isEmpty()) return ResponseEntity.noContent().build();
|
||||
|
||||
subscriptionRepository.delete(subOpt.get());
|
||||
|
||||
templateRepository.findById(id).ifPresent(t -> {
|
||||
t.setSubscriberCount(Math.max(0, t.getSubscriberCount() - 1));
|
||||
templateRepository.save(t);
|
||||
});
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Kopie erstellen (Fork) ────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{id}/fork")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> fork(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var tOpt = templateRepository.findById(id);
|
||||
if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build();
|
||||
|
||||
var source = tOpt.get();
|
||||
String copyName = (source.getName() != null ? source.getName() : "Vorlage") + " (Kopie)";
|
||||
|
||||
UUID newId;
|
||||
if (source instanceof TimeLockTemplateEntity tl) {
|
||||
var copy = new TimeLockTemplateEntity();
|
||||
copy.setOwner(myId);
|
||||
copy.setName(copyName);
|
||||
copy.setMinTimeInMinutes(tl.getMinTimeInMinutes());
|
||||
copy.setMaxTimeInMinutes(tl.getMaxTimeInMinutes());
|
||||
copy.setEndTimeVisible(tl.isEndTimeVisible());
|
||||
copy.setHygineOpeningDurationMinutes(tl.getHygineOpeningDurationMinutes());
|
||||
copy.setHygineOpeningEveryMinites(tl.getHygineOpeningEveryMinites());
|
||||
copy.setTasks(tl.getTasks());
|
||||
copy.setTaskEveryMinutes(tl.getTaskEveryMinutes());
|
||||
copy.setMinTasksPerDay(tl.getMinTasksPerDay());
|
||||
copy.setSpinningWheelEntries(tl.getSpinningWheelEntries());
|
||||
copy.setSpinsEveryMinutes(tl.getSpinsEveryMinutes());
|
||||
copy.setMinSpinsPerDay(tl.getMinSpinsPerDay());
|
||||
copy.setRequiresVerification(tl.isRequiresVerification());
|
||||
copy.setTaskMode(tl.getTaskMode() != null ? tl.getTaskMode() : TaskMode.RANDOM);
|
||||
copy.setPenaltyType(tl.getPenaltyType());
|
||||
copy.setPenaltyValue(tl.getPenaltyValue());
|
||||
newId = timeLockTemplateRepository.save(copy).getTemplateId();
|
||||
} else if (source instanceof CardlockTemplateEntity cl) {
|
||||
var copy = new CardlockTemplateEntity();
|
||||
copy.setOwner(myId);
|
||||
copy.setName(copyName);
|
||||
copy.setCardCountsMin(cl.getCardCountsMin());
|
||||
copy.setCardCountsMax(cl.getCardCountsMax());
|
||||
copy.setPickEveryMinute(cl.getPickEveryMinute());
|
||||
copy.setAccumulatePicks(cl.isAccumulatePicks());
|
||||
copy.setShowRemainingCards(cl.isShowRemainingCards());
|
||||
copy.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes());
|
||||
copy.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites());
|
||||
copy.setTasks(cl.getTasks());
|
||||
copy.setRequiresVerification(cl.isRequiresVerification());
|
||||
copy.setTaskMode(cl.getTaskMode() != null ? cl.getTaskMode() : TaskMode.RANDOM);
|
||||
newId = cardlockTemplateRepository.save(copy).getTemplateId();
|
||||
} else {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("templateId", newId.toString()));
|
||||
}
|
||||
|
||||
// ── Veröffentlichen ───────────────────────────────────────────────────────
|
||||
|
||||
@PatchMapping("/{id}/publish")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> publish(@PathVariable UUID id, Principal principal) {
|
||||
var me = userService.requireUser(principal);
|
||||
|
||||
var tOpt = templateRepository.findById(id);
|
||||
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var t = tOpt.get();
|
||||
if (!t.getOwner().equals(me.getUserId())) return ResponseEntity.status(403).build();
|
||||
|
||||
t.setPublished(true);
|
||||
t.setShowAuthor(me.isProfilBeiVeroeffentlichungenSichtbar());
|
||||
templateRepository.save(t);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Veröffentlichung entfernen ────────────────────────────────────────────
|
||||
|
||||
@DeleteMapping("/{id}/publish")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> unpublish(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var tOpt = templateRepository.findById(id);
|
||||
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var t = tOpt.get();
|
||||
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
t.setPublished(false);
|
||||
t.setShowAuthor(false);
|
||||
t.setSubscriberCount(0);
|
||||
templateRepository.save(t);
|
||||
subscriptionRepository.deleteByTemplateId(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── DTO Helper ────────────────────────────────────────────────────────────
|
||||
|
||||
private Map<String, Object> toPublicDto(BaseLockTemplateEntity t, UUID myId, Set<UUID> subscribedIds) {
|
||||
boolean isOwn = t.getOwner().equals(myId);
|
||||
boolean isSubscribed = subscribedIds.contains(t.getTemplateId());
|
||||
|
||||
String authorName = null;
|
||||
String authorProfilePicture = null;
|
||||
if (t.isShowAuthor()) {
|
||||
var authorOpt = userRepository.findById(t.getOwner());
|
||||
authorName = authorOpt.map(u -> u.getName()).orElse(null);
|
||||
authorProfilePicture = authorOpt.map(u -> u.getProfilePicture()).orElse(null);
|
||||
}
|
||||
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("templateId", t.getTemplateId().toString());
|
||||
dto.put("lockType", t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK");
|
||||
dto.put("name", t.getName() != null ? t.getName() : "");
|
||||
dto.put("subscriberCount", t.getSubscriberCount());
|
||||
dto.put("authorName", authorName);
|
||||
dto.put("authorProfilePicture", authorProfilePicture);
|
||||
dto.put("isOwnTemplate", isOwn);
|
||||
dto.put("isSubscribed", isSubscribed);
|
||||
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
|
||||
dto.put("requiresVerification", t.isRequiresVerification());
|
||||
dto.put("hygieneEnabled", t.getHygineOpeningEveryMinites() != null);
|
||||
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
|
||||
dto.put("taskMode", t.getTaskMode() != null ? t.getTaskMode().name() : "RANDOM");
|
||||
|
||||
if (t instanceof TimeLockTemplateEntity tl) {
|
||||
dto.put("minTimeInMinutes", tl.getMinTimeInMinutes());
|
||||
dto.put("maxTimeInMinutes", tl.getMaxTimeInMinutes());
|
||||
dto.put("endTimeVisible", tl.isEndTimeVisible());
|
||||
dto.put("taskEveryMinutes", tl.getTaskEveryMinutes());
|
||||
dto.put("minTasksPerDay", tl.getMinTasksPerDay());
|
||||
dto.put("spinningWheelEntries", tl.getSpinningWheelEntries() != null ? tl.getSpinningWheelEntries() : List.of());
|
||||
dto.put("spinsEveryMinutes", tl.getSpinsEveryMinutes());
|
||||
dto.put("minSpinsPerDay", tl.getMinSpinsPerDay());
|
||||
dto.put("penaltyType", tl.getPenaltyType() != null ? tl.getPenaltyType().name() : null);
|
||||
dto.put("penaltyValue", tl.getPenaltyValue());
|
||||
} else if (t instanceof CardlockTemplateEntity cl) {
|
||||
dto.put("cardCountsMin", cl.getCardCountsMin() != null ? cl.getCardCountsMin() : Map.of());
|
||||
dto.put("cardCountsMax", cl.getCardCountsMax() != null ? cl.getCardCountsMax() : Map.of());
|
||||
dto.put("pickEveryMinute", cl.getPickEveryMinute());
|
||||
dto.put("accumulatePicks", cl.isAccumulatePicks());
|
||||
dto.put("showRemainingCards",cl.isShowRemainingCards());
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user