package de.oaa.xxx.user; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.time.LocalDate; import java.time.Period; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import de.oaa.xxx.config.CookieFactory; import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity; import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository; import de.oaa.xxx.games.chastity.common.BaseLockRepository; import de.oaa.xxx.games.chastity.common.BaseLockTemplateRepository; import de.oaa.xxx.games.chastity.common.CodeCreator; import de.oaa.xxx.games.chastity.ttlock.TTAuthService; import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; import de.oaa.xxx.games.chastity.ttlock.TTLockService; import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigEntity; import de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository; import de.oaa.xxx.registration.Registration; import de.oaa.xxx.registration.RegistrationRepository; import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.entity.NotificationPreferenceEntity; import de.oaa.xxx.social.repository.NotificationPreferenceRepository; @RestController @RequestMapping("/user") public class UserController { private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); private final UserRepository userRepository; private final RegistrationRepository registrationRepository; private final NotificationPreferenceRepository notificationPreferenceRepository; private final BdsmDefaultsRepository bdsmDefaultsRepository; private final UserService userService; private final TTLockUserConfigRepository ttLockUserConfigRepository; private final TTLockConfigRepository ttLockConfigRepository; private final TTAuthService ttAuthService; private final TTLockService ttLockService; private final BaseLockRepository baseLockRepository; private final BaseLockTemplateRepository baseLockTemplateRepository; private final CookieFactory cookieFactory; public UserController(UserRepository userRepository, RegistrationRepository registrationRepository, NotificationPreferenceRepository notificationPreferenceRepository, BdsmDefaultsRepository bdsmDefaultsRepository, UserService userService, TTLockUserConfigRepository ttLockUserConfigRepository, TTLockConfigRepository ttLockConfigRepository, TTAuthService ttAuthService, TTLockService ttLockService, BaseLockRepository baseLockRepository, BaseLockTemplateRepository baseLockTemplateRepository, CookieFactory cookieFactory) { this.userRepository = userRepository; this.registrationRepository = registrationRepository; this.notificationPreferenceRepository = notificationPreferenceRepository; this.bdsmDefaultsRepository = bdsmDefaultsRepository; this.userService = userService; this.ttLockUserConfigRepository = ttLockUserConfigRepository; this.ttLockConfigRepository = ttLockConfigRepository; this.ttAuthService = ttAuthService; this.ttLockService = ttLockService; this.baseLockRepository = baseLockRepository; this.baseLockTemplateRepository = baseLockTemplateRepository; this.cookieFactory = cookieFactory; } record ProfilePictureRequest(String picture, String pictureHq) {} record NameChangeRequest(String name) {} record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {} record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId, boolean testSuccessful) {} record TtlockUserConfigRequest(String username, String password, Integer lockId) {} record ProfileRequest(Integer groesse, Integer gewicht, Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} record DatingRequest(boolean datingAktiv, String datingStadt, Double datingLat, Double datingLon, List datingGeschlechter, Integer datingMaxDistanzKm, Integer datingMinAlter, Integer datingMaxAlter) {} record DatingFilterRequest(List datingGeschlechter, Integer datingMaxDistanzKm, Integer datingMinAlter, Integer datingMaxAlter) {} record PrivacyRequest( Sichtbarkeit sichtbarkeitGrunddaten, Sichtbarkeit sichtbarkeitGalerie, Sichtbarkeit sichtbarkeitFreunde, Sichtbarkeit sichtbarkeitFeed, Sichtbarkeit sichtbarkeitPinnwand, Sichtbarkeit sichtbarkeitXp, Sichtbarkeit sichtbarkeitLockhistorie, Sichtbarkeit sichtbarkeitVorlieben, Boolean profilBeiVeroeffentlichungenSichtbar) {} @PutMapping("/me/dating") public ResponseEntity updateDating(@RequestBody DatingRequest request, Principal principal) { if (request.datingAktiv() && (request.datingStadt() == null || request.datingStadt().isBlank())) { return ResponseEntity.badRequest().build(); } var user = userService.requireUser(principal); user.setDatingAktiv(request.datingAktiv()); if (!request.datingAktiv()) { // Alle Dating-Einstellungen zurücksetzen user.setDatingStadt(null); user.setDatingLat(null); user.setDatingLon(null); user.setDatingGeschlechter(null); user.setDatingMaxDistanzKm(null); user.setDatingMinAlter(null); user.setDatingMaxAlter(null); } else { user.setDatingStadt(request.datingStadt().trim()); user.setDatingLat(request.datingLat()); user.setDatingLon(request.datingLon()); if (request.datingGeschlechter() != null && !request.datingGeschlechter().isEmpty()) { String joined = request.datingGeschlechter().stream() .filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { return false; } }) .collect(Collectors.joining(",")); user.setDatingGeschlechter(joined.isBlank() ? null : joined); } else { user.setDatingGeschlechter(null); } if (request.datingMaxDistanzKm() != null) user.setDatingMaxDistanzKm(Math.max(1, Math.min(500, request.datingMaxDistanzKm()))); if (request.datingMinAlter() != null) user.setDatingMinAlter(Math.max(18, Math.min(99, request.datingMinAlter()))); if (request.datingMaxAlter() != null) user.setDatingMaxAlter(Math.max(18, Math.min(99, request.datingMaxAlter()))); } userRepository.save(user); LOGGER.info("User {} hat Dating-Einstellungen aktualisiert: aktiv={}", user.getUserId(), request.datingAktiv()); return ResponseEntity.ok().build(); } @PutMapping("/me/dating-filter") public ResponseEntity updateDatingFilter(@RequestBody DatingFilterRequest request, Principal principal) { var user = userService.requireUser(principal); if (!user.isDatingAktiv()) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); if (request.datingGeschlechter() != null) { String joined = request.datingGeschlechter().stream() .filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { return false; } }) .collect(Collectors.joining(",")); user.setDatingGeschlechter(joined.isBlank() ? null : joined); } if (request.datingMaxDistanzKm() != null) user.setDatingMaxDistanzKm(Math.max(1, Math.min(500, request.datingMaxDistanzKm()))); if (request.datingMinAlter() != null) user.setDatingMinAlter(Math.max(18, Math.min(99, request.datingMinAlter()))); if (request.datingMaxAlter() != null) user.setDatingMaxAlter(Math.max(18, Math.min(99, request.datingMaxAlter()))); userRepository.save(user); return ResponseEntity.ok().build(); } @PutMapping("/me/picture") public ResponseEntity updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) { var user = userService.requireUser(principal); user.setProfilePicture(request.picture()); user.setProfilePictureHq(request.pictureHq()); userRepository.save(user); LOGGER.debug("User {} hat Profilbild aktualisiert", user.getUserId()); return ResponseEntity.ok().build(); } @PutMapping("/me/profile") public ResponseEntity updateProfile(@RequestBody ProfileRequest request, Principal principal) { var user = userService.requireUser(principal); if (request.beschreibung() != null && request.beschreibung().length() > 600) { return ResponseEntity.badRequest().build(); } user.setGroesse(request.groesse()); user.setGewicht(request.gewicht()); user.setGeschlecht(request.geschlecht()); user.setNeigung(request.neigung()); user.setBeziehungsstatus(request.beziehungsstatus()); user.setBeschreibung(request.beschreibung()); userRepository.save(user); LOGGER.info("User {} hat Profil aktualisiert", user.getUserId()); return ResponseEntity.ok().build(); } @PutMapping("/me/privacy") public ResponseEntity updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) { var user = userService.requireUser(principal); if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten()); if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie()); if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde()); if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed()); if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand()); if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp()); if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie()); if (request.sichtbarkeitVorlieben() != null) user.setSichtbarkeitVorlieben(request.sichtbarkeitVorlieben()); if (request.profilBeiVeroeffentlichungenSichtbar() != null) { boolean showAuthor = request.profilBeiVeroeffentlichungenSichtbar(); user.setProfilBeiVeroeffentlichungenSichtbar(showAuthor); // Alle veröffentlichten Templates synchronisieren var templates = baseLockTemplateRepository.findByOwnerAndPublishedTrue(user.getUserId()); for (var t : templates) { t.setShowAuthor(showAuthor); } baseLockTemplateRepository.saveAll(templates); } userRepository.save(user); LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId()); return ResponseEntity.ok().build(); } record NotificationPreferenceRequest(boolean inApp, boolean email) {} @GetMapping("/me/notifications") public ResponseEntity> getNotifications(Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); Map byKey = notificationPreferenceRepository.findByUserId(userId) .stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p)); Map result = new LinkedHashMap<>(); for (MessageCause cause : MessageCause.values()) { NotificationPreferenceEntity pref = byKey.getOrDefault( cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause)); Map entry = new LinkedHashMap<>(); entry.put("inApp", pref.isInApp()); entry.put("email", pref.isEmail()); result.put(cause.name(), entry); } return ResponseEntity.ok(result); } @PutMapping("/me/notifications") public ResponseEntity updateNotifications(@RequestBody Map request, Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); for (var entry : request.entrySet()) { MessageCause cause; try { cause = MessageCause.valueOf(entry.getKey()); } catch (IllegalArgumentException e) { continue; } NotificationPreferenceEntity pref = notificationPreferenceRepository .findByUserIdAndCause(userId, cause) .orElseGet(() -> { NotificationPreferenceEntity n = new NotificationPreferenceEntity(); n.setUserId(userId); n.setCause(cause); return n; }); pref.setInApp(entry.getValue().inApp()); pref.setEmail(entry.getValue().email()); notificationPreferenceRepository.save(pref); } return ResponseEntity.ok().build(); } record BdsmDefaultsRequest(List spieltMit, List rollen, List werkzeuge) {} @GetMapping("/me/bdsm-defaults") public ResponseEntity> getBdsmDefaults(Principal principal) { var currentUser = userService.requireUser(principal); UUID userId = currentUser.getUserId(); BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) .orElse(new BdsmDefaultsEntity()); Map result = new java.util.LinkedHashMap<>(); result.put("geschlecht", currentUser.getGeschlecht() != null ? currentUser.getGeschlecht().name() : null); result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); result.put("rollen", splitOrEmpty(d.getRollen())); result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); return ResponseEntity.ok(result); } @GetMapping("/{userId}/bdsm-defaults") public ResponseEntity> getBdsmDefaultsForUser(@PathVariable("userId") UUID userId) { var userOpt = userRepository.findById(userId); if (userOpt.isEmpty()) return ResponseEntity.notFound().build(); UserEntity user = userOpt.get(); BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) .orElse(new BdsmDefaultsEntity()); Map result = new java.util.LinkedHashMap<>(); result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null); result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); result.put("rollen", splitOrEmpty(d.getRollen())); result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); return ResponseEntity.ok(result); } @PutMapping("/me/bdsm-defaults") public ResponseEntity updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) .orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; }); d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit())); d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen())); d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge())); bdsmDefaultsRepository.save(d); return ResponseEntity.ok().build(); } private static List splitOrEmpty(String s) { if (s == null || s.isBlank()) return List.of(); return List.of(s.split(",")); } @PutMapping("/me/geburtsdatum") public ResponseEntity updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) { if (request.geburtsdatum() == null || Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) { return ResponseEntity.status(422).build(); } var user = userService.requireUser(principal); user.setGeburtsdatum(request.geburtsdatum()); userRepository.save(user); LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId()); return ResponseEntity.ok().build(); } @PutMapping("/me/name") public ResponseEntity updateName(@RequestBody NameChangeRequest request, Principal principal) { String newName = request.name(); if (userRepository.findByName(newName).isPresent() || registrationRepository.findByName(newName).isPresent()) { return ResponseEntity.status(409).build(); } var user = userService.requireUser(principal); user.setName(newName); userRepository.save(user); LOGGER.info("User {} hat Namen zu '{}' geändert", user.getUserId(), newName); return ResponseEntity.ok().build(); } @DeleteMapping("/me") public ResponseEntity deleteAccount(Principal principal) { var currentUser = userService.requireUser(principal); UUID userId = currentUser.getUserId(); String email = currentUser.getEmail(); userService.deleteAccount(userId, email); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookieFactory.jwtCookie("", java.time.Duration.ZERO).toString()) .build(); } // ── TTLock-Account ──────────────────────────────────────────────────────── @GetMapping("/me/ttlock") public ResponseEntity getTtlockUserConfig(Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId) .orElse(new TTLockUserConfigEntity()); return ResponseEntity.ok(new TtlockUserConfigDto( cfg.getUsername(), cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(), cfg.getLockId(), cfg.isTestSuccessful() )); } @PutMapping("/me/ttlock") public ResponseEntity saveTtlockUserConfig(@RequestBody TtlockUserConfigRequest body, Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId) .orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; }); boolean credentialsChanged = !java.util.Objects.equals(cfg.getUsername(), body.username()) || !java.util.Objects.equals(cfg.getLockId(), body.lockId()) || (body.password() != null && !body.password().isBlank()); if (credentialsChanged) { cfg.setTestSuccessful(false); } cfg.setUsername(body.username()); if (body.password() != null && !body.password().isBlank()) { cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8))); } cfg.setLockId(body.lockId()); ttLockUserConfigRepository.save(cfg); return ResponseEntity.ok().build(); } @GetMapping("/me/ttlock/test") public ResponseEntity> testTtlockConnection(Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); if (userCfg == null || userCfg.getUsername() == null || userCfg.getPasswordMd5() == null || userCfg.getLockId() == null) { return ResponseEntity.badRequest().body(Map.of("error", "ttlock_not_configured")); } var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); if (adminCfg == null || adminCfg.getClientId() == null || adminCfg.getClientSecret() == null) { return ResponseEntity.badRequest().body(Map.of("error", "admin_config_missing")); } String token = ttAuthService.getAccessToken( adminCfg.getClientId(), adminCfg.getClientSecret(), userCfg.getUsername(), userCfg.getPasswordMd5()); if (token == null) { return ResponseEntity.status(502).body(Map.of("error", "auth_failed")); } TTLockService.TTLockDetailResponse detail = ttLockService.getLockDetail( adminCfg.getClientId(), token, userCfg.getLockId()); if (detail == null || detail.getErrcode() != 0) { String msg = detail != null ? detail.getErrmsg() : "Keine Antwort"; return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg)); } userCfg.setTestSuccessful(true); ttLockUserConfigRepository.save(userCfg); Map result = new LinkedHashMap<>(); result.put("lockId", detail.getLockId()); result.put("lockName", detail.getLockName()); result.put("lockAlias", detail.getLockAlias()); result.put("modelNum", detail.getModelNum()); result.put("electricQuantity", detail.getElectricQuantity()); result.put("state", detail.getState()); return ResponseEntity.ok(result); } @PostMapping("/me/ttlock/open") public ResponseEntity> ttlockOpen(Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); if (userCfg == null || userCfg.getUsername() == null || userCfg.getPasswordMd5() == null || userCfg.getLockId() == null) { return ResponseEntity.badRequest().body(Map.of("error", "ttlock_not_configured")); } var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); if (adminCfg == null || adminCfg.getClientId() == null) { return ResponseEntity.badRequest().body(Map.of("error", "admin_config_missing")); } var activeLock = baseLockRepository.findByLockee(userId); if (activeLock.isPresent() && activeLock.get().getUnlockTime() == null) { return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); } String token = ttAuthService.getAccessToken( adminCfg.getClientId(), adminCfg.getClientSecret(), userCfg.getUsername(), userCfg.getPasswordMd5()); if (token == null) { return ResponseEntity.status(502).body(Map.of("error", "auth_failed")); } String pin = CodeCreator.createNumeric(6); Integer pwdId = ttLockService.addCustomPasscode(adminCfg.getClientId(), token, userCfg.getLockId(), pin); if (pwdId == null) { return ResponseEntity.status(502).body(Map.of("error", "passcode_failed")); } return ResponseEntity.ok(Map.of("pin", pin, "keyboardPwdId", pwdId)); } @DeleteMapping("/me/ttlock/open/{keyboardPwdId}") public ResponseEntity ttlockCloseOpen(@PathVariable("keyboardPwdId") int keyboardPwdId, Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); var userCfg = ttLockUserConfigRepository.findById(userId).orElse(null); if (userCfg == null || userCfg.getLockId() == null) return ResponseEntity.badRequest().build(); var adminCfg = ttLockConfigRepository.findById(1L).orElse(null); if (adminCfg == null) return ResponseEntity.badRequest().build(); String token = ttAuthService.getAccessToken( adminCfg.getClientId(), adminCfg.getClientSecret(), userCfg.getUsername(), userCfg.getPasswordMd5()); if (token == null) return ResponseEntity.status(502).build(); ttLockService.deleteCustomPasscode(adminCfg.getClientId(), token, userCfg.getLockId(), keyboardPwdId); return ResponseEntity.ok().build(); } @PostMapping public ResponseEntity userAnlegen(@RequestBody Registration registration) { try { userService.createUser(registration); return ResponseEntity.status(201).build(); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().build(); } catch (IllegalStateException e) { return ResponseEntity.status(409).build(); } catch (Exception e) { LOGGER.error(e.getMessage(), e); return ResponseEntity.internalServerError().build(); } } record NewMemberDto(UUID userId, String name, String profilePicture, Integer alter, String geschlecht, String neigung, String datingStadt, String beschreibung) {} @GetMapping("/new-members") public ResponseEntity> getNewMembers(Principal principal) { UUID myId = principal != null ? userService.requireUser(principal).getUserId() : null; LocalDate since = LocalDate.now().minusDays(14); List result = userRepository .findByRegistrierungsdatumAfterOrderByRegistrierungsdatumDesc(since) .stream() .filter(u -> !u.getUserId().equals(myId)) .map(u -> new NewMemberDto( u.getUserId(), u.getName(), u.getProfilePictureHq() != null ? u.getProfilePictureHq() : u.getProfilePicture(), u.getAlter(), u.getGeschlecht() != null ? u.getGeschlecht().name() : null, u.getNeigung() != null ? u.getNeigung().name() : null, u.getDatingStadt(), u.getBeschreibung())) .collect(Collectors.toList()); return ResponseEntity.ok(result); } record LocationFilterRequest(String filterCity, Double filterLat, Double filterLon, Integer filterMaxDistKm) {} @PutMapping("/me/location-filter") public ResponseEntity updateLocationFilter(@RequestBody LocationFilterRequest request, Principal principal) { var user = userService.requireUser(principal); if (request.filterCity() != null) user.setFilterCity(request.filterCity()); if (request.filterLat() != null) user.setFilterLat(request.filterLat()); if (request.filterLon() != null) user.setFilterLon(request.filterLon()); if (request.filterMaxDistKm() != null) user.setFilterMaxDistKm(Math.max(1, Math.min(500, request.filterMaxDistKm()))); userRepository.save(user); return ResponseEntity.ok().build(); } }