Aufgabenverwaltung angepasst, Eventseite weiter bearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -203,8 +203,10 @@ public class AdminController {
|
||||
@GetMapping("/aufgabengruppen")
|
||||
public ResponseEntity<List<AufgabenGruppe>> getAufgabengruppen(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository
|
||||
.findByUserIdIsNull(PageRequest.of(0, 1000)).getContent();
|
||||
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository.findAll().stream()
|
||||
.filter(g -> g.getUserId() == null)
|
||||
.sorted(java.util.Comparator.comparing(AufgabenGruppeEntity::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
return ResponseEntity.ok(list.stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||
}
|
||||
|
||||
@@ -230,9 +232,11 @@ public class AdminController {
|
||||
entity.setName(gruppe.getName());
|
||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||
entity.setVon(gruppe.getVon());
|
||||
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||
if (gruppe.getBild() != null) {
|
||||
entity.setBild(java.util.Base64.getDecoder().decode(gruppe.getBild()));
|
||||
}
|
||||
aufgabenGruppeRepository.save(entity);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@ public class SecurityConfig {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.headers(headers -> headers
|
||||
.frameOptions(frame -> frame.deny())
|
||||
.contentTypeOptions(ct -> {})
|
||||
.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
|
||||
.addHeaderWriter((request, response) -> {
|
||||
response.setHeader("X-XSS-Protection", "1; mode=block");
|
||||
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
response.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
|
||||
})
|
||||
)
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint((request, response, authException) ->
|
||||
response.sendRedirect("/login.html")))
|
||||
@@ -77,6 +87,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/dating/matches.html").authenticated()
|
||||
.requestMatchers("/community/locations.html").authenticated()
|
||||
.requestMatchers("/community/location-detail.html").authenticated()
|
||||
.requestMatchers("/community/location-events.html").authenticated()
|
||||
.requestMatchers("/community/events.html").authenticated()
|
||||
.requestMatchers("/community/event-detail.html").authenticated()
|
||||
.requestMatchers("/gruppen/**").authenticated()
|
||||
|
||||
@@ -4,12 +4,17 @@ 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 jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/feedback")
|
||||
@@ -20,6 +25,10 @@ public class FeedbackController {
|
||||
private final UserRepository userRepository;
|
||||
private final SupportUserService supportUserService;
|
||||
|
||||
/** Rate-Limiting: Key → letzter Aufruf (Epoch-Sekunden) */
|
||||
private final Map<String, Long> lastCallAt = new ConcurrentHashMap<>();
|
||||
private static final long RATE_LIMIT_SECONDS = 60;
|
||||
|
||||
public FeedbackController(MailService mailService,
|
||||
FeedbackRepository feedbackRepository,
|
||||
UserRepository userRepository,
|
||||
@@ -33,11 +42,29 @@ public class FeedbackController {
|
||||
record FeedbackRequest(String name, String seite, String grund, String text) {}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req, Principal principal) {
|
||||
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req,
|
||||
Principal principal,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// E-Mail-Header-Injection verhindern: Felder dürfen keine Zeilenumbrüche enthalten
|
||||
if (containsLineBreak(req.name()) || containsLineBreak(req.seite()) || containsLineBreak(req.grund())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Rate-Limiting: 1 Aufruf pro Minute pro eingeloggtem User oder IP
|
||||
String rateLimitKey = principal != null ? "user:" + principal.getName()
|
||||
: "ip:" + httpRequest.getRemoteAddr();
|
||||
long now = Instant.now().getEpochSecond();
|
||||
Long last = lastCallAt.get(rateLimitKey);
|
||||
if (last != null && (now - last) < RATE_LIMIT_SECONDS) {
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
|
||||
}
|
||||
lastCallAt.put(rateLimitKey, now);
|
||||
|
||||
// Eingeloggten User ermitteln (optional)
|
||||
UUID userId = null;
|
||||
if (principal != null) {
|
||||
@@ -86,4 +113,8 @@ public class FeedbackController {
|
||||
if (s == null) return "";
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
private boolean containsLineBreak(String s) {
|
||||
return s != null && (s.contains("\n") || s.contains("\r"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,8 @@ public class AboController {
|
||||
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
||||
|
||||
List<AufgabenGruppe> dtos = gruppeRepository
|
||||
.findPublicFromOthers(user.getUserId(), namePattern).stream()
|
||||
.listAllWithUserAndSearch(user.getUserId(), namePattern, org.springframework.data.domain.PageRequest.of(0, 500)).stream()
|
||||
.filter(g -> !g.isPrivateGruppe() && g.getUserId() != null && !g.getUserId().equals(user.getUserId()))
|
||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
|
||||
@@ -1,257 +1,259 @@
|
||||
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(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", 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(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", 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("userId") 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("gruppeId") 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("gruppeId") 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("gruppeId") 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("gruppeId") 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;
|
||||
}
|
||||
}
|
||||
package de.oaa.xxx.games.bdsm.controller;
|
||||
|
||||
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;
|
||||
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.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
@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 (alle Gruppen – BDSM-Verwaltung und Spielstart) ──
|
||||
|
||||
@GetMapping("/list/user")
|
||||
public ResponseEntity<AufgabenGruppePage> listUser(
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", 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(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", 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.listAllWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
|
||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/own")
|
||||
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam("userId") UUID userId) {
|
||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||
list.setGruppen(gruppeRepository.findByUserId(userId)
|
||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
/** Gibt eine einzelne Gruppe zurück – typunabhängig, da BDSM-Spielstart auch vanillaAvailable-Gruppen laden muss. */
|
||||
@GetMapping("/{gruppeId}")
|
||||
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") 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);
|
||||
// vanillaAvailable kommt aus dem Request-Body (Checkbox im Frontend)
|
||||
gruppeRepository.save(entity);
|
||||
LOGGER.debug("User {} hat BDSM-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("gruppeId") 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());
|
||||
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||
if (gruppe.getBild() != null) {
|
||||
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
|
||||
}
|
||||
gruppeRepository.save(entity);
|
||||
LOGGER.debug("User {} hat BDSM-AufgabenGruppe {} aktualisiert", user.getUserId(), gruppeId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── Kopieren (Systemgruppe → eigene) ──
|
||||
|
||||
@PostMapping("/copy/{gruppeId}")
|
||||
public ResponseEntity<Void> copy(@PathVariable("gruppeId") 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("gruppeId") 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,5 @@ public class AufgabenGruppe {
|
||||
private String bild;
|
||||
private long subscriberCount;
|
||||
private boolean subscribed;
|
||||
private boolean vanillaAvailable;
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@ public class AufgabenGruppeDisplay {
|
||||
private boolean privateGruppe;
|
||||
private String bild;
|
||||
private String von;
|
||||
private boolean vanillaAvailable;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ public class AufgabenGruppeEntity {
|
||||
private byte[] bild;
|
||||
@Column
|
||||
private String von;
|
||||
@Column(columnDefinition = "BOOLEAN DEFAULT FALSE NOT NULL")
|
||||
private boolean vanillaAvailable = false;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
private List<AufgabeEntity> aufgaben;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
@@ -62,6 +64,7 @@ public class AufgabenGruppeEntity {
|
||||
gruppe.setPrivateGruppe(privateGruppe);
|
||||
gruppe.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||
gruppe.setVon(von);
|
||||
gruppe.setVanillaAvailable(vanillaAvailable);
|
||||
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
|
||||
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
|
||||
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());
|
||||
@@ -78,6 +81,7 @@ public class AufgabenGruppeEntity {
|
||||
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
||||
entity.setBild(gruppe.getBild() != null ? Base64.getDecoder().decode(gruppe.getBild()) : null);
|
||||
entity.setVon(gruppe.getVon());
|
||||
entity.setVanillaAvailable(gruppe.isVanillaAvailable());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -90,6 +94,7 @@ public class AufgabenGruppeEntity {
|
||||
display.setPrivateGruppe(privateGruppe);
|
||||
display.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||
display.setVon(von);
|
||||
display.setVanillaAvailable(vanillaAvailable);
|
||||
return display;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
package de.oaa.xxx.games.common.repository;
|
||||
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEntity, UUID> {
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where age.userId = :userId")
|
||||
// ── Account-Löschung (UserService) ────────────────────────────────────────
|
||||
|
||||
@Query("select g from AufgabenGruppeEntity g where g.userId = :userId")
|
||||
List<AufgabenGruppeEntity> findByUserId(@Param("userId") UUID userId);
|
||||
|
||||
long countByUserId(UUID userId);
|
||||
|
||||
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
|
||||
// ── BDSM-Verwaltung: alle Gruppen paginiert ───────────────────────────────
|
||||
|
||||
Page<AufgabenGruppeEntity> findByUserId(UUID userId, Pageable pageable);
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where (age.privateGruppe = false or age.userId = :userId) and (:search is null or age.name like :search)")
|
||||
List<AufgabenGruppeEntity> listWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and (:search is null or age.name like :search)")
|
||||
List<AufgabenGruppeEntity> listPublicWithSearch(@Param("search") String search, PageRequest pageable);
|
||||
// ── Vanilla-Verwaltung: nur vanillaAvailable=true, mit Inhalt ─────────────
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and age.userId is not null and age.userId <> :userId and (:name is null or lower(age.name) like lower(:name))")
|
||||
List<AufgabenGruppeEntity> findPublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId = :userId AND g.vanillaAvailable = true AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findByUserIdAndVanillaAvailableTrueWithContent(@Param("userId") UUID userId, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE (g.privateGruppe = false OR g.userId = :userId) AND g.strafen IS EMPTY AND g.sperren IS EMPTY AND (:search IS NULL OR g.name LIKE :search)")
|
||||
List<AufgabenGruppeEntity> listVanillaSafeWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId IS NULL AND g.vanillaAvailable = true AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findSystemGroupsByVanillaAvailableTrueWithContent(Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.privateGruppe = false AND g.userId IS NOT NULL AND g.userId <> :userId AND g.strafen IS EMPTY AND g.sperren IS EMPTY AND (:name IS NULL OR LOWER(g.name) LIKE LOWER(:name))")
|
||||
List<AufgabenGruppeEntity> findVanillaSafePublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
||||
// ── Spielstart-Auswahl ────────────────────────────────────────────────────
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId = :userId AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findByUserIdWithContent(@Param("userId") UUID userId, Pageable pageable);
|
||||
/** Nur vanillaAvailable-Gruppen – für Vanilla-Spielstart und Vanilla-Suche. */
|
||||
@Query("select g from AufgabenGruppeEntity g where g.vanillaAvailable = true and (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||
List<AufgabenGruppeEntity> listVanillaAvailableWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId IS NULL AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findSystemGroupsWithContent(Pageable pageable);
|
||||
/** Alle Gruppen – für BDSM-Spielstart (vanillaAvailable-Gruppen werden im Frontend hervorgehoben). */
|
||||
@Query("select g from AufgabenGruppeEntity g where (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||
List<AufgabenGruppeEntity> listAllWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ 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;
|
||||
@@ -58,16 +56,21 @@ public class VanillaAboController {
|
||||
Principal principal) {
|
||||
UserEntity user = userService.requireUser(principal);
|
||||
|
||||
Page<GruppenAboEntity> dbPage = aboRepository.findByUserIdWithContent(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("aufgabenGruppe.name")));
|
||||
List<AufgabenGruppe> dtos = dbPage.getContent().stream()
|
||||
.map(a -> enrich(a.getAufgabenGruppe(), user.getUserId(), true))
|
||||
List<AufgabenGruppe> all = aboRepository.findByUserId(user.getUserId()).stream()
|
||||
.map(GruppenAboEntity::getAufgabenGruppe)
|
||||
.filter(g -> g.isVanillaAvailable()
|
||||
&& !g.isPrivateGruppe()
|
||||
&& (!g.getAufgaben().isEmpty() || !g.getFinisher().isEmpty()))
|
||||
.map(g -> enrich(g, user.getUserId(), true))
|
||||
.sorted(java.util.Comparator.comparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
int total = all.size();
|
||||
int start = page * size;
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(dtos);
|
||||
result.setCurrentPage(dbPage.getNumber());
|
||||
result.setTotalPages(dbPage.getTotalPages());
|
||||
result.setTotalElements(dbPage.getTotalElements());
|
||||
result.setContent(start >= total ? List.of() : all.subList(start, Math.min(start + size, total)));
|
||||
result.setCurrentPage(page);
|
||||
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||
result.setTotalElements(total);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@@ -84,7 +87,8 @@ public class VanillaAboController {
|
||||
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
||||
|
||||
List<AufgabenGruppe> dtos = gruppeRepository
|
||||
.findVanillaSafePublicFromOthers(user.getUserId(), namePattern).stream()
|
||||
.listVanillaAvailableWithUserAndSearch(user.getUserId(), namePattern, PageRequest.of(0, 500)).stream()
|
||||
.filter(g -> !g.isPrivateGruppe() && g.getUserId() != null && !g.getUserId().equals(user.getUserId()))
|
||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||
.sorted(java.util.Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
@@ -111,8 +115,7 @@ public class VanillaAboController {
|
||||
if (gruppe == null || gruppe.isPrivateGruppe() || user.getUserId().equals(gruppe.getUserId())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
// Vanilla-safe validation
|
||||
if (!gruppe.getStrafen().isEmpty() || !gruppe.getSperren().isEmpty()) {
|
||||
if (!gruppe.isVanillaAvailable()) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
if (aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), gruppe)) {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
package de.oaa.xxx.games.vanilla.controller;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -22,20 +32,9 @@ 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;
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/vanilla/gruppe")
|
||||
@@ -84,7 +83,7 @@ public class VanillaAufgabenGruppeController {
|
||||
Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findByUserIdWithContent(
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findByUserIdAndVanillaAvailableTrueWithContent(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(dbPage.getContent().stream().map(entity -> {
|
||||
@@ -102,11 +101,10 @@ public class VanillaAufgabenGruppeController {
|
||||
public ResponseEntity<AufgabenGruppePage> listSystem(
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findSystemGroupsWithContent(
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findSystemGroupsByVanillaAvailableTrueWithContent(
|
||||
PageRequest.of(page, size, Sort.by("name")));
|
||||
AufgabenGruppePage r = new AufgabenGruppePage();
|
||||
r.setContent(dbPage.getContent().stream()
|
||||
.map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||
r.setContent(dbPage.getContent().stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||
r.setCurrentPage(dbPage.getNumber());
|
||||
r.setTotalPages(dbPage.getTotalPages());
|
||||
r.setTotalElements(dbPage.getTotalElements());
|
||||
@@ -120,7 +118,8 @@ public class VanillaAufgabenGruppeController {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
String searchPattern = search != null ? "%" + search + "%" : null;
|
||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||
list.setGruppen(gruppeRepository.listVanillaSafeWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 500))
|
||||
list.setGruppen(gruppeRepository.listVanillaAvailableWithUserAndSearch(
|
||||
userId, searchPattern, PageRequest.of(0, 500))
|
||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
@@ -129,7 +128,7 @@ public class VanillaAufgabenGruppeController {
|
||||
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam("userId") UUID userId) {
|
||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||
list.setGruppen(gruppeRepository.findByUserId(userId).stream()
|
||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
||||
.filter(AufgabenGruppeEntity::isVanillaAvailable)
|
||||
.map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
@@ -137,7 +136,7 @@ public class VanillaAufgabenGruppeController {
|
||||
@GetMapping("/{gruppeId}")
|
||||
public ResponseEntity<AufgabenGruppe> get(@PathVariable("gruppeId") UUID gruppeId) {
|
||||
return gruppeRepository.findById(gruppeId)
|
||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
||||
.filter(AufgabenGruppeEntity::isVanillaAvailable)
|
||||
.map(entity -> ResponseEntity.ok(entity.toAufgabenGruppe()))
|
||||
.orElse(ResponseEntity.status(403).build());
|
||||
}
|
||||
@@ -159,6 +158,7 @@ public class VanillaAufgabenGruppeController {
|
||||
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
||||
entity.setUserId(user.getUserId());
|
||||
entity.setPrivateGruppe(true);
|
||||
entity.setVanillaAvailable(true);
|
||||
gruppeRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Vanilla-AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
||||
return ResponseEntity.created(
|
||||
@@ -181,8 +181,7 @@ public class VanillaAufgabenGruppeController {
|
||||
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();
|
||||
// Vanilla-safe check: cannot edit a non-vanilla-safe group
|
||||
if (!entity.getStrafen().isEmpty() || !entity.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
||||
if (!entity.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||
|
||||
entity.setName(gruppe.getName().trim());
|
||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||
@@ -202,10 +201,9 @@ public class VanillaAufgabenGruppeController {
|
||||
public ResponseEntity<Void> copy(@PathVariable("gruppeId") UUID gruppeId, Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
// Only allow copying vanilla-safe groups
|
||||
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
|
||||
if (source == null) return ResponseEntity.notFound().build();
|
||||
if (!source.getStrafen().isEmpty() || !source.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
||||
if (!source.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||
try {
|
||||
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
|
||||
return ResponseEntity.status(201).build();
|
||||
@@ -228,8 +226,7 @@ public class VanillaAufgabenGruppeController {
|
||||
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();
|
||||
// Only allow deletion of vanilla-safe groups
|
||||
if (!entity.getStrafen().isEmpty() || !entity.getSperren().isEmpty()) return ResponseEntity.status(403).build();
|
||||
if (!entity.isVanillaAvailable()) return ResponseEntity.status(403).build();
|
||||
|
||||
try {
|
||||
aboRepository.deleteByAufgabenGruppe(entity);
|
||||
@@ -249,7 +246,7 @@ public class VanillaAufgabenGruppeController {
|
||||
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
|
||||
try {
|
||||
gruppeRepository.findById(gruppe.getGruppenId()).ifPresent(entity -> {
|
||||
if (entity.getStrafen().isEmpty() && entity.getSperren().isEmpty()) {
|
||||
if (entity.isVanillaAvailable()) {
|
||||
gruppeRepository.delete(entity);
|
||||
}
|
||||
});
|
||||
@@ -266,5 +263,4 @@ public class VanillaAufgabenGruppeController {
|
||||
if (principal == null) return null;
|
||||
return userService.requireUser(principal);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class NotificationController {
|
||||
n.put("text", m.getText());
|
||||
n.put("sentAt", m.getSentAt().toString());
|
||||
n.put("read", m.getReadAt() != null);
|
||||
n.put("targetUrl", m.getTargetUrl() != null ? m.getTargetUrl() : "");
|
||||
n.put("targetUrl", sanitizeTargetUrl(m.getTargetUrl()));
|
||||
userRepository.findById(m.getSenderId()).ifPresent(sender -> {
|
||||
n.put("senderName", sender.getName());
|
||||
n.put("senderAvatar", sender.getProfilePicture() != null ? sender.getProfilePicture() : "");
|
||||
@@ -67,6 +67,12 @@ public class NotificationController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** Erlaubt nur relative Pfade (beginnen mit '/') – verhindert javascript:-URLs und externe Redirects. */
|
||||
private String sanitizeTargetUrl(String url) {
|
||||
if (url == null || url.isBlank()) return "";
|
||||
return url.startsWith("/") ? url : "";
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PostMapping("/read-all")
|
||||
public ResponseEntity<Void> markAllRead(Principal principal) {
|
||||
|
||||
@@ -283,7 +283,8 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable("userId") UUID userId) {
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable("userId") UUID userId, Principal principal) {
|
||||
userService.requireUser(principal);
|
||||
var userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
UserEntity user = userOpt.get();
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
.gruppe-info { font-size:0.75rem; color:var(--color-muted); margin-top:0.2rem; }
|
||||
.gruppe-badges { display:flex; gap:0.3rem; margin-top:0.25rem; flex-wrap:wrap; }
|
||||
.gruppe-badge { font-size:0.65rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||||
.gruppe-badge-public { background:rgba(46,204,113,0.15); color:var(--color-success); }
|
||||
.gruppe-badge-public { background:rgba(46,204,113,0.15); color:var(--color-success); }
|
||||
.gruppe-badge-vanilla { background:#e8f5e9; color:#2e7d32; border:1px solid #a5d6a7; }
|
||||
.gruppe-toggle { font-size:0.75rem; color:var(--color-muted); flex-shrink:0; transition:transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform:rotate(90deg); }
|
||||
.gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; }
|
||||
@@ -277,6 +278,12 @@
|
||||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues wählen zum Ersetzen</span>
|
||||
</div>
|
||||
<input type="file" id="gBild" accept="image/*">
|
||||
<label style="margin-top:0.5rem;">
|
||||
<span class="modal-check">
|
||||
<input type="checkbox" id="gVanilla">
|
||||
Auch für Vanilla-Game verfügbar
|
||||
</span>
|
||||
</label>
|
||||
<div class="modal-error" id="gruppeModalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="gruppeModalCancel">Abbrechen</button>
|
||||
@@ -986,7 +993,7 @@ function renderAdminGruppen(gruppen) {
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span></div>
|
||||
<div class="gruppe-badges"><span class="gruppe-badge gruppe-badge-public">Öffentlich</span>${g.vanillaAvailable ? '<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>' : ''}</div>
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
@@ -1262,6 +1269,7 @@ function openGruppeModal(editId) {
|
||||
document.getElementById('gName').value = g.name || '';
|
||||
document.getElementById('gVon').value = g.von || '';
|
||||
document.getElementById('gDesc').value = g.beschreibung || '';
|
||||
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||
if (g.bild) { document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild; imgWrap.style.display = 'flex'; }
|
||||
else imgWrap.style.display = 'none';
|
||||
@@ -1270,6 +1278,7 @@ function openGruppeModal(editId) {
|
||||
document.getElementById('gName').value = '';
|
||||
document.getElementById('gVon').value = '';
|
||||
document.getElementById('gDesc').value = '';
|
||||
document.getElementById('gVanilla').checked = false;
|
||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||
}
|
||||
gruppeModal.classList.add('open');
|
||||
@@ -1348,7 +1357,7 @@ gruppeModalSave.addEventListener('click', async () => {
|
||||
let bildBase64 = null;
|
||||
const fi = document.getElementById('gBild');
|
||||
if (fi.files.length > 0) bildBase64 = await toBase64(fi.files[0]);
|
||||
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, bild: bildBase64 };
|
||||
const payload = { name, von: document.getElementById('gVon').value.trim() || null, beschreibung: document.getElementById('gDesc').value.trim() || null, vanillaAvailable: document.getElementById('gVanilla').checked, bild: bildBase64 };
|
||||
const isEdit = currentEditGruppeId != null;
|
||||
fetch(isEdit ? `/admin/aufgabengruppen/${currentEditGruppeId}` : '/admin/aufgabengruppen', {
|
||||
method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
||||
|
||||
@@ -195,6 +195,7 @@ function renderPage(evt) {
|
||||
|
||||
const totalAttendees = (evt.attendees || []).length;
|
||||
const attending = evt.attendingMe;
|
||||
const isPast = new Date(evt.startAt) < new Date();
|
||||
|
||||
document.getElementById('content').innerHTML = `
|
||||
<div class="evt-header">
|
||||
@@ -211,11 +212,13 @@ function renderPage(evt) {
|
||||
<button class="btn" style="background:#c0392b;font-size:0.85rem;" onclick="openDeleteConfirm()">Löschen</button>
|
||||
</div>` : ''}
|
||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
||||
<button class="btn" id="attendBtn"
|
||||
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||
onclick="toggleAttend()">
|
||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||
</button>
|
||||
${isPast
|
||||
? `<span style="color:var(--color-muted);font-size:0.85rem;">Veranstaltung bereits beendet</span>`
|
||||
: `<button class="btn" id="attendBtn"
|
||||
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||
onclick="toggleAttend()">
|
||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||
</button>`}
|
||||
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -445,6 +445,9 @@ function renderPage() {
|
||||
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
||||
</div>
|
||||
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
|
||||
<div id="allEventsLinkWrap" style="display:none;margin-top:0.75rem;">
|
||||
<a id="allEventsLink" href="#" class="btn" style="display:inline-block;font-size:0.85rem;background:var(--color-secondary);color:var(--color-text);text-decoration:none;padding:0.45rem 1rem;border-radius:6px;">Alle Events anzeigen →</a>
|
||||
</div>
|
||||
<div id="pastEventsSection" style="display:none;">
|
||||
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
||||
<div class="event-list" id="pastEventList"></div>
|
||||
@@ -517,8 +520,8 @@ function renderPage() {
|
||||
${locHeaderHtml}
|
||||
${hoursHtml}
|
||||
${gallerySection}
|
||||
${feedSection}
|
||||
${eventsSection}`;
|
||||
${eventsSection}
|
||||
${feedSection}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,15 +590,27 @@ async function loadEvents() {
|
||||
if (!list) return;
|
||||
|
||||
const now = new Date();
|
||||
const future = events.filter(e => new Date(e.startAt) >= now);
|
||||
const future = events.filter(e => new Date(e.startAt) >= now)
|
||||
.sort((a, b) => new Date(a.startAt) - new Date(b.startAt));
|
||||
const past = events.filter(e => new Date(e.startAt) < now)
|
||||
.slice(-5) // letzte 5
|
||||
.slice(-3) // letzte 3
|
||||
.reverse(); // neueste zuerst
|
||||
|
||||
list.innerHTML = future.length
|
||||
? future.map(e => buildEventCard(e, false)).join('')
|
||||
const preview = future.slice(0, 3);
|
||||
list.innerHTML = preview.length
|
||||
? preview.map(e => buildEventCard(e, false)).join('')
|
||||
: '<p style="color:var(--color-muted);font-size:0.9rem;">Keine bevorstehenden Veranstaltungen.</p>';
|
||||
|
||||
const linkWrap = document.getElementById('allEventsLinkWrap');
|
||||
if (linkWrap) {
|
||||
if (future.length > 3) {
|
||||
document.getElementById('allEventsLink').href = `/community/location-events.html?id=${locationId}`;
|
||||
linkWrap.style.display = '';
|
||||
} else {
|
||||
linkWrap.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const pastSection = document.getElementById('pastEventsSection');
|
||||
if (past.length && pastSection) {
|
||||
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
|
||||
@@ -1358,11 +1373,15 @@ function renderLocPost(p) {
|
||||
<button class="post-action-btn post-delete" onclick="event.stopPropagation();deleteLocPost('${p.postId}')" title="Löschen">🗑</button>
|
||||
</div>` : '';
|
||||
|
||||
const authorUrl = p.posterType === 'LOCATION'
|
||||
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
|
||||
: `/community/benutzer.html?userId=${p.authorId}`;
|
||||
|
||||
return `<div class="post-card${clickableClass}" id="lp-${p.postId}"${onClickAttr}>
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avHtml}</div>
|
||||
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avHtml}</a></div>
|
||||
<div>
|
||||
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
|
||||
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${escHtml(p.authorName || p.locationName || '')}</a></div>
|
||||
<div class="post-meta">${dateStr}${editedHtml}</div>
|
||||
</div>
|
||||
${adminBtns}
|
||||
@@ -1401,11 +1420,15 @@ function openLpLb(postId) {
|
||||
+ ' ' + new Date(p.createdAt).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
||||
const editedHtml = p.editedAt ? ' <span style="font-size:0.7rem;color:var(--color-muted);">(bearbeitet)</span>' : '';
|
||||
|
||||
const lbAuthorUrl = p.posterType === 'LOCATION'
|
||||
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
|
||||
: `/community/benutzer.html?userId=${p.authorId}`;
|
||||
|
||||
document.getElementById('lbPostBody').innerHTML = `
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avHtml}</div>
|
||||
<div class="post-avatar"><a href="${lbAuthorUrl}" style="display:contents;">${avHtml}</a></div>
|
||||
<div>
|
||||
<div class="post-author">${escHtml(p.authorName || p.locationName || '')}</div>
|
||||
<div class="post-author"><a href="${lbAuthorUrl}" style="color:inherit;text-decoration:none;">${escHtml(p.authorName || p.locationName || '')}</a></div>
|
||||
<div class="post-meta">${dateStr}${editedHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
164
src/main/resources/static/community/location-events.html
Normal file
164
src/main/resources/static/community/location-events.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Alle Events – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/community.css">
|
||||
<style>
|
||||
.back-link { display:inline-flex; align-items:center; gap:0.35rem; color:var(--color-muted); font-size:0.88rem; text-decoration:none; margin-bottom:1rem; }
|
||||
.back-link:hover { color:var(--color-primary); }
|
||||
|
||||
.page-title { font-size:1.15rem; font-weight:700; margin:0 0 1.25rem; }
|
||||
|
||||
.event-list { display:flex; flex-direction:column; gap:0.75rem; }
|
||||
.event-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; display:flex; gap:0.75rem; padding:0.75rem; text-decoration:none; color:inherit; transition:border-color 0.15s; cursor:pointer; }
|
||||
.event-card:hover { border-color:var(--color-primary); }
|
||||
.event-card-img { width:64px; height:64px; border-radius:8px; object-fit:cover; background:var(--color-secondary); flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:1.4rem; }
|
||||
.event-card-img img { width:100%; height:100%; object-fit:cover; }
|
||||
.event-card-body { flex:1; min-width:0; }
|
||||
.event-card-title { font-weight:600; font-size:0.92rem; margin:0 0 0.2rem; }
|
||||
.event-card-date { font-size:0.78rem; color:var(--color-muted); }
|
||||
.event-card-attendees { font-size:0.78rem; color:var(--color-muted); }
|
||||
|
||||
.paging-bar { display:flex; align-items:center; justify-content:center; gap:0.75rem; margin-top:1.25rem; flex-wrap:wrap; }
|
||||
.paging-bar button { background:var(--color-secondary); border:none; color:var(--color-text); padding:0.45rem 1rem; border-radius:6px; font-size:0.88rem; cursor:pointer; transition:background 0.15s; }
|
||||
.paging-bar button:hover:not(:disabled) { background:var(--color-primary); color:#fff; }
|
||||
.paging-bar button:disabled { opacity:0.4; cursor:default; }
|
||||
.paging-info { font-size:0.85rem; color:var(--color-muted); }
|
||||
|
||||
.empty-hint { color:var(--color-muted); font-size:0.9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<a class="back-link" id="backLink" href="#">‹ Zurück zur Location</a>
|
||||
|
||||
<div id="locName" class="page-title">Alle Events</div>
|
||||
|
||||
<div id="eventList" class="event-list">
|
||||
<p class="empty-hint">Wird geladen…</p>
|
||||
</div>
|
||||
|
||||
<div class="paging-bar" id="pagingBar" style="display:none;">
|
||||
<button id="prevBtn" onclick="changePage(-1)" disabled>‹ Zurück</button>
|
||||
<span class="paging-info" id="pagingInfo"></span>
|
||||
<button id="nextBtn" onclick="changePage(1)">Weiter ›</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const locationId = params.get('id');
|
||||
|
||||
let allEvents = [];
|
||||
let currentPage = 0;
|
||||
|
||||
document.getElementById('backLink').href = `/community/location-detail.html?id=${locationId}`;
|
||||
|
||||
function escHtml(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('de-DE', { weekday:'short', day:'2-digit', month:'2-digit', year:'numeric' })
|
||||
+ ' ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
|
||||
}
|
||||
|
||||
function buildEventCard(e) {
|
||||
const imgHtml = e.imageData
|
||||
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="${escHtml(e.title)}">`
|
||||
: '🗓';
|
||||
return `
|
||||
<a class="event-card" href="/community/event-detail.html?id=${e.eventId}">
|
||||
<div class="event-card-img">${imgHtml}</div>
|
||||
<div class="event-card-body">
|
||||
<div class="event-card-title">${escHtml(e.title)}</div>
|
||||
<div class="event-card-date">${formatDate(e.startAt)}</div>
|
||||
<div class="event-card-attendees">${e.attendeeCount} Teilnehmer*in(nen)${e.attendingMe ? ' · Du nimmst teil' : ''}</div>
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
const list = document.getElementById('eventList');
|
||||
const totalPages = Math.ceil(allEvents.length / PAGE_SIZE);
|
||||
const start = currentPage * PAGE_SIZE;
|
||||
const slice = allEvents.slice(start, start + PAGE_SIZE);
|
||||
|
||||
list.innerHTML = slice.length
|
||||
? slice.map(buildEventCard).join('')
|
||||
: '<p class="empty-hint">Keine weiteren Veranstaltungen.</p>';
|
||||
|
||||
const pagingBar = document.getElementById('pagingBar');
|
||||
if (allEvents.length > PAGE_SIZE) {
|
||||
pagingBar.style.display = '';
|
||||
document.getElementById('prevBtn').disabled = currentPage === 0;
|
||||
document.getElementById('nextBtn').disabled = currentPage >= totalPages - 1;
|
||||
document.getElementById('pagingInfo').textContent =
|
||||
`Seite ${currentPage + 1} von ${totalPages} (${allEvents.length} Events)`;
|
||||
} else {
|
||||
pagingBar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(dir) {
|
||||
const totalPages = Math.ceil(allEvents.length / PAGE_SIZE);
|
||||
const next = currentPage + dir;
|
||||
if (next < 0 || next >= totalPages) return;
|
||||
currentPage = next;
|
||||
renderPage();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (!locationId) {
|
||||
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Keine Location-ID angegeben.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Locationname laden
|
||||
const locRes = await fetch(`/locations/${locationId}`);
|
||||
if (locRes.ok) {
|
||||
const loc = await locRes.json();
|
||||
document.getElementById('locName').textContent = `Alle Events – ${loc.name}`;
|
||||
document.title = `Events – ${loc.name} – xXx Sphere`;
|
||||
}
|
||||
|
||||
// Events laden
|
||||
const res = await fetch(`/locations/${locationId}/events`);
|
||||
if (!res.ok) {
|
||||
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Events konnten nicht geladen werden.</p>';
|
||||
return;
|
||||
}
|
||||
const events = await res.json();
|
||||
const now = new Date();
|
||||
allEvents = events
|
||||
.filter(e => new Date(e.startAt) >= now)
|
||||
.sort((a, b) => new Date(a.startAt) - new Date(b.startAt));
|
||||
|
||||
if (allEvents.length === 0) {
|
||||
document.getElementById('eventList').innerHTML = '<p class="empty-hint">Keine bevorstehenden Veranstaltungen.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
renderPage();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -95,6 +95,7 @@
|
||||
}
|
||||
.gruppe-badge-private { background: rgba(233,69,96,0.15); color: var(--color-primary); }
|
||||
.gruppe-badge-public { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-badge-vanilla { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
@@ -367,6 +368,12 @@
|
||||
Gruppe veröffentlichen (für alle sichtbar)
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span class="modal-check">
|
||||
<input type="checkbox" id="gVanilla">
|
||||
Auch für Vanilla-Game verfügbar
|
||||
</span>
|
||||
</label>
|
||||
<div class="modal-error" id="modalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||||
@@ -715,6 +722,7 @@
|
||||
if (g.privateGruppe) badges.push(`<span class="gruppe-badge gruppe-badge-private">Privat</span>`);
|
||||
else badges.push(`<span class="gruppe-badge gruppe-badge-public">Öffentlich</span>`);
|
||||
if (type === 'user' && g.subscriberCount > 0) badges.push(`<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`);
|
||||
if (g.vanillaAvailable) badges.push(`<span class="gruppe-badge gruppe-badge-vanilla">Vanilla</span>`);
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="gruppe-${esc(g.gruppenId)}">
|
||||
@@ -1069,6 +1077,7 @@
|
||||
pubCb.checked = !g.privateGruppe;
|
||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||
document.getElementById('gPublicLabel').style.display = 'block';
|
||||
document.getElementById('gVanilla').checked = g.vanillaAvailable || false;
|
||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||
if (g.bild) {
|
||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||
@@ -1086,6 +1095,7 @@
|
||||
document.getElementById('gDesc').value = '';
|
||||
document.getElementById('gPublic').checked = false;
|
||||
document.getElementById('gPublicLabel').style.display = 'none';
|
||||
document.getElementById('gVanilla').checked = false;
|
||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||
gruppeModal.classList.add('open');
|
||||
document.getElementById('gName').focus();
|
||||
@@ -1119,6 +1129,7 @@
|
||||
name,
|
||||
beschreibung: document.getElementById('gDesc').value.trim() || null,
|
||||
privateGruppe: isEdit ? !document.getElementById('gPublic').checked : true,
|
||||
vanillaAvailable: document.getElementById('gVanilla').checked,
|
||||
bild: bildBase64
|
||||
};
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
.item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||
.gruppe-item-name, .toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
|
||||
.gruppe-item-desc, .toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
|
||||
.vanilla-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; padding: 0.1rem 0.35rem; border-radius: 3px; background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; margin-left: 0.35rem; vertical-align: middle; letter-spacing: 0.03em; }
|
||||
.empty-hint { color: var(--color-muted); font-size: 0.875rem; font-style: italic; padding: 0.5rem 0; }
|
||||
.aufgaben-section-label { font-size: 0.78rem; font-weight: 600; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 1rem 0 0.5rem 0; }
|
||||
.aufgaben-section-label:first-child { margin-top: 0; }
|
||||
@@ -653,9 +654,10 @@
|
||||
}
|
||||
ul.innerHTML = gruppen.map(g => {
|
||||
const checked = savedGruppen.has(g.gruppenId);
|
||||
const vanillaBadge = g.vanillaAvailable ? '<span class="vanilla-badge">Vanilla</span>' : '';
|
||||
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
|
||||
<span><span class="gruppe-item-name">${g.name}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
|
||||
<span><span class="gruppe-item-name">${g.name}${vanillaBadge}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
|
||||
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
|
||||
</label></li>`;
|
||||
}).join('');
|
||||
|
||||
@@ -887,11 +887,14 @@
|
||||
</div>`
|
||||
: '';
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
const authorUrl = p.posterType === 'LOCATION'
|
||||
? `/community/location-detail.html?id=${p.locationId || p.authorId}`
|
||||
: `/community/benutzer.html?userId=${p.authorId}`;
|
||||
return `<div class="post-card" id="hpc-${p.postId}"${gruppeIdAttr} onclick="homeOpenPost('${p.postId}')">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avatarHtml}</div>
|
||||
<div class="post-avatar"><a href="${authorUrl}" onclick="event.stopPropagation()" style="display:contents;">${avatarHtml}</a></div>
|
||||
<div>
|
||||
<div class="post-author">${esc(p.authorName)}${privacyLabel}</div>
|
||||
<div class="post-author"><a href="${authorUrl}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
|
||||
<div class="post-meta" id="hpm-${p.postId}">${fmtDate(p.createdAt)}${editedLabel}${groupBadge}</div>
|
||||
</div>
|
||||
${ownBtns}
|
||||
|
||||
Reference in New Issue
Block a user