Adminmasken angefangen
This commit is contained in:
270
xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java
Normal file
270
xxxthegame/src/main/java/de/oaa/xxx/admin/AdminController.java
Normal file
@@ -0,0 +1,270 @@
|
||||
package de.oaa.xxx.admin;
|
||||
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppe;
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import de.oaa.xxx.meldung.MeldungEntity;
|
||||
import de.oaa.xxx.meldung.MeldungRepository;
|
||||
import de.oaa.xxx.meldung.MeldungStatus;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@Transactional
|
||||
public class AdminController {
|
||||
|
||||
private final AdminRepository adminRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final MeldungRepository meldungRepository;
|
||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public AdminController(AdminRepository adminRepository, UserRepository userRepository,
|
||||
MeldungRepository meldungRepository,
|
||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.adminRepository = adminRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.meldungRepository = meldungRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
// ── DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
record AdminDto(UUID adminId, UUID userId, String userName, AdminRolle rolle, LocalDateTime createdAt) {}
|
||||
|
||||
record MeldungDto(UUID meldungId, UUID melderId, String melderName,
|
||||
de.oaa.xxx.meldung.MeldungZielTyp zielTyp, UUID zielId,
|
||||
String grund, LocalDateTime gemeldetAt,
|
||||
MeldungStatus status, UUID bearbeitetVon, LocalDateTime bearbeitetAt) {}
|
||||
|
||||
record CreateAdminRequest(UUID userId, AdminRolle rolle) {}
|
||||
|
||||
record StatusRequest(MeldungStatus status) {}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private AdminEntity requireAdmin(Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElseThrow();
|
||||
return adminRepository.findByUserId(user.getUserId())
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.FORBIDDEN, "Kein Admin"));
|
||||
}
|
||||
|
||||
private AdminEntity requireSuperAdmin(Principal principal) {
|
||||
AdminEntity admin = requireAdmin(principal);
|
||||
if (admin.getRolle() != AdminRolle.SUPERADMIN) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.FORBIDDEN, "Kein Superadmin");
|
||||
}
|
||||
return admin;
|
||||
}
|
||||
|
||||
private AdminDto toDto(AdminEntity e) {
|
||||
String name = userRepository.findById(e.getUserId()).map(UserEntity::getName).orElse("?");
|
||||
return new AdminDto(e.getAdminId(), e.getUserId(), name, e.getRolle(), e.getCreatedAt());
|
||||
}
|
||||
|
||||
private MeldungDto toMeldungDto(MeldungEntity e) {
|
||||
String melderName = userRepository.findById(e.getMelderId()).map(UserEntity::getName).orElse("?");
|
||||
return new MeldungDto(e.getMeldungId(), e.getMelderId(), melderName,
|
||||
e.getZielTyp(), e.getZielId(), e.getGrund(), e.getGemeldetAt(),
|
||||
e.getStatus(), e.getBearbeitetVon(), e.getBearbeitetAt());
|
||||
}
|
||||
|
||||
// ── /admin/me ────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<AdminDto> me(Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) return ResponseEntity.status(403).build();
|
||||
return adminRepository.findByUserId(user.getUserId())
|
||||
.map(a -> ResponseEntity.ok(toDto(a)))
|
||||
.orElse(ResponseEntity.status(403).build());
|
||||
}
|
||||
|
||||
// ── Meldungen ────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/meldungen")
|
||||
public ResponseEntity<List<MeldungDto>> getMeldungen(
|
||||
@RequestParam(name = "status", required = false) MeldungStatus status,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<MeldungEntity> list = status != null
|
||||
? meldungRepository.findByStatusOrderByGemeldetAtDesc(status)
|
||||
: meldungRepository.findAllByOrderByGemeldetAtDesc();
|
||||
return ResponseEntity.ok(list.stream().map(this::toMeldungDto).toList());
|
||||
}
|
||||
|
||||
@PutMapping("/meldungen/{id}")
|
||||
public ResponseEntity<Void> updateMeldung(@PathVariable("id") UUID id,
|
||||
@RequestBody StatusRequest body,
|
||||
Principal principal) {
|
||||
var admin = requireAdmin(principal);
|
||||
var user = userRepository.findByEmail(principal.getName()).orElseThrow();
|
||||
MeldungEntity meldung = meldungRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
meldung.setStatus(body.status());
|
||||
meldung.setBearbeitetVon(user.getUserId());
|
||||
meldung.setBearbeitetAt(LocalDateTime.now());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Aufgabengruppen ──────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/aufgabengruppen")
|
||||
public ResponseEntity<List<de.oaa.xxx.aufgaben.AufgabenGruppe>> getAufgabengruppen(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<AufgabenGruppeEntity> list = aufgabenGruppeRepository
|
||||
.findByUserIdIsNull(PageRequest.of(0, 1000)).getContent();
|
||||
return ResponseEntity.ok(list.stream().map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||
}
|
||||
|
||||
@PostMapping("/aufgabengruppen")
|
||||
public ResponseEntity<de.oaa.xxx.aufgaben.AufgabenGruppeDisplay> createAufgabengruppe(
|
||||
@RequestBody AufgabenGruppe gruppe, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
gruppe.setUserId(null);
|
||||
gruppe.setPrivateGruppe(false);
|
||||
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
||||
aufgabenGruppeRepository.save(entity);
|
||||
return ResponseEntity.status(201).body(entity.toAufgabenGruppeDisplay());
|
||||
}
|
||||
|
||||
@PutMapping("/aufgabengruppen/{id}")
|
||||
public ResponseEntity<Void> updateAufgabengruppe(@PathVariable("id") UUID id,
|
||||
@RequestBody AufgabenGruppe gruppe,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
AufgabenGruppeEntity entity = aufgabenGruppeRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
entity.setName(gruppe.getName());
|
||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||
entity.setVon(gruppe.getVon());
|
||||
if (gruppe.getBild() != null) {
|
||||
entity.setBild(java.util.Base64.getDecoder().decode(gruppe.getBild()));
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/aufgabengruppen/{id}")
|
||||
public ResponseEntity<Void> deleteAufgabengruppe(@PathVariable("id") UUID id, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
AufgabenGruppeEntity entity = aufgabenGruppeRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
if (entity.getUserId() != null) {
|
||||
return ResponseEntity.status(403).build(); // Nur System-Gruppen
|
||||
}
|
||||
aufgabenGruppeRepository.delete(entity);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Toys ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/toys")
|
||||
public ResponseEntity<List<Toy>> getToys(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<ToyEntity> list = toyRepository.findByUserIdIsNull(PageRequest.of(0, 1000, Sort.by(Sort.Direction.ASC, "name"))).getContent();
|
||||
return ResponseEntity.ok(list.stream().map(ToyEntity::toToy).toList());
|
||||
}
|
||||
|
||||
@PostMapping("/toys")
|
||||
public ResponseEntity<Toy> createToy(@RequestBody Toy toy, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNull(toy.getName())) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
toy.setUserId(null);
|
||||
ToyEntity entity = ToyEntity.create(toy);
|
||||
toyRepository.save(entity);
|
||||
return ResponseEntity.status(201).body(entity.toToy());
|
||||
}
|
||||
|
||||
@PutMapping("/toys/{id}")
|
||||
public ResponseEntity<Void> updateToy(@PathVariable("id") UUID id,
|
||||
@RequestBody Toy toy, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
ToyEntity entity = toyRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(toy.getName(), id)) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
entity.setName(toy.getName());
|
||||
entity.setBeschreibung(toy.getBeschreibung());
|
||||
if (toy.getBild() != null) {
|
||||
entity.setBild(java.util.Base64.getDecoder().decode(toy.getBild()));
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/toys/{id}")
|
||||
public ResponseEntity<Void> deleteToy(@PathVariable("id") UUID id, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
ToyEntity entity = toyRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
long usage = toyRepository.countAufgabeUsage(id)
|
||||
+ toyRepository.countStrafeUsage(id)
|
||||
+ toyRepository.countSperreUsage(id);
|
||||
if (usage > 0) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
toyRepository.delete(entity);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Admin-Verwaltung (nur SUPERADMIN) ────────────────────────────────────
|
||||
|
||||
@GetMapping("/admins")
|
||||
public ResponseEntity<List<AdminDto>> getAdmins(Principal principal) {
|
||||
requireSuperAdmin(principal);
|
||||
return ResponseEntity.ok(adminRepository.findAll().stream().map(this::toDto).toList());
|
||||
}
|
||||
|
||||
@PostMapping("/admins")
|
||||
public ResponseEntity<AdminDto> createAdmin(@RequestBody CreateAdminRequest request, Principal principal) {
|
||||
requireSuperAdmin(principal);
|
||||
if (!userRepository.existsById(request.userId())) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
if (adminRepository.existsByUserId(request.userId())) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
AdminEntity entity = AdminEntity.create(request.userId(), request.rolle());
|
||||
adminRepository.save(entity);
|
||||
return ResponseEntity.status(201).body(toDto(entity));
|
||||
}
|
||||
|
||||
@DeleteMapping("/admins/{id}")
|
||||
public ResponseEntity<Void> deleteAdmin(@PathVariable("id") UUID id, Principal principal) {
|
||||
var requestingUser = userRepository.findByEmail(principal.getName()).orElseThrow();
|
||||
requireSuperAdmin(principal);
|
||||
AdminEntity entity = adminRepository.findById(id)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||
if (entity.getUserId().equals(requestingUser.getUserId())) {
|
||||
return ResponseEntity.status(400).build(); // Selbstlöschung verhindern
|
||||
}
|
||||
adminRepository.delete(entity);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
38
xxxthegame/src/main/java/de/oaa/xxx/admin/AdminEntity.java
Normal file
38
xxxthegame/src/main/java/de/oaa/xxx/admin/AdminEntity.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package de.oaa.xxx.admin;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "admin")
|
||||
public class AdminEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID adminId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20, nullable = false)
|
||||
private AdminRolle rolle;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public static AdminEntity create(UUID userId, AdminRolle rolle) {
|
||||
AdminEntity entity = new AdminEntity();
|
||||
entity.setAdminId(UUID.randomUUID());
|
||||
entity.setUserId(userId);
|
||||
entity.setRolle(rolle);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.oaa.xxx.admin;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AdminRepository extends JpaRepository<AdminEntity, UUID> {
|
||||
|
||||
Optional<AdminEntity> findByUserId(UUID userId);
|
||||
|
||||
boolean existsByUserId(UUID userId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.admin;
|
||||
|
||||
public enum AdminRolle {
|
||||
ADMIN, SUPERADMIN
|
||||
}
|
||||
@@ -61,6 +61,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/gruppen.html").authenticated()
|
||||
.requestMatchers("/gruppe.html").authenticated()
|
||||
.requestMatchers("/feed.html").authenticated()
|
||||
.requestMatchers("/admin.html").authenticated()
|
||||
.requestMatchers("/communityvotes.html").authenticated()
|
||||
.requestMatchers("/keyholder.html").authenticated()
|
||||
.requestMatchers("/meine-locks.html").authenticated()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.meldung;
|
||||
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/meldung")
|
||||
public class MeldungController {
|
||||
|
||||
private final MeldungRepository meldungRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public MeldungController(MeldungRepository meldungRepository, UserRepository userRepository) {
|
||||
this.meldungRepository = meldungRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record MeldungRequest(MeldungZielTyp zielTyp, UUID zielId, String grund) {}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ResponseEntity<Void> melden(@RequestBody MeldungRequest request, Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElseThrow();
|
||||
if (meldungRepository.existsByMelderIdAndZielTypAndZielId(user.getUserId(), request.zielTyp(), request.zielId())) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
meldungRepository.save(MeldungEntity.create(user.getUserId(), request.zielTyp(), request.zielId(), request.grund()));
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.oaa.xxx.meldung;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "meldung", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"melder_id", "ziel_typ", "ziel_id"})
|
||||
})
|
||||
public class MeldungEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID meldungId;
|
||||
|
||||
@Column(name = "melder_id", nullable = false)
|
||||
private UUID melderId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "ziel_typ", length = 10, nullable = false)
|
||||
private MeldungZielTyp zielTyp;
|
||||
|
||||
@Column(name = "ziel_id", nullable = false)
|
||||
private UUID zielId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String grund;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime gemeldetAt;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20, nullable = false)
|
||||
private MeldungStatus status;
|
||||
|
||||
@Column(name = "bearbeitet_von")
|
||||
private UUID bearbeitetVon;
|
||||
|
||||
@Column(name = "bearbeitet_at")
|
||||
private LocalDateTime bearbeitetAt;
|
||||
|
||||
public static MeldungEntity create(UUID melderId, MeldungZielTyp zielTyp, UUID zielId, String grund) {
|
||||
MeldungEntity entity = new MeldungEntity();
|
||||
entity.setMeldungId(UUID.randomUUID());
|
||||
entity.setMelderId(melderId);
|
||||
entity.setZielTyp(zielTyp);
|
||||
entity.setZielId(zielId);
|
||||
entity.setGrund(grund);
|
||||
entity.setGemeldetAt(LocalDateTime.now());
|
||||
entity.setStatus(MeldungStatus.OFFEN);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.oaa.xxx.meldung;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface MeldungRepository extends JpaRepository<MeldungEntity, UUID> {
|
||||
|
||||
List<MeldungEntity> findAllByOrderByGemeldetAtDesc();
|
||||
|
||||
List<MeldungEntity> findByStatusOrderByGemeldetAtDesc(MeldungStatus status);
|
||||
|
||||
boolean existsByMelderIdAndZielTypAndZielId(UUID melderId, MeldungZielTyp zielTyp, UUID zielId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.meldung;
|
||||
|
||||
public enum MeldungStatus {
|
||||
OFFEN, BEARBEITET, ABGELEHNT
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.meldung;
|
||||
|
||||
public enum MeldungZielTyp {
|
||||
POST, PROFIL
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class ActivationController {
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
public ResponseEntity<Void> activate(@PathVariable String uuid) {
|
||||
public ResponseEntity<Void> activate(@PathVariable("uuid") String uuid) {
|
||||
try {
|
||||
String email = registrationService.activate(uuid);
|
||||
String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
@@ -51,16 +51,24 @@ public class RegistrationController {
|
||||
LOGGER.warn("Registrierung abgelehnt – Mindestalter nicht erreicht");
|
||||
return ResponseEntity.status(422).build();
|
||||
}
|
||||
if (registrationRepository.findByEmail(registration.getEmail()).isPresent()
|
||||
|| userRepository.findByEmail(registration.getEmail()).isPresent()) {
|
||||
// Bereits aktivierte User blockieren
|
||||
if (userRepository.findByEmail(registration.getEmail()).isPresent()) {
|
||||
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (registrationRepository.findByName(registration.getName()).isPresent()
|
||||
|| userRepository.findByName(registration.getName()).isPresent()) {
|
||||
if (userRepository.findByName(registration.getName()).isPresent()) {
|
||||
LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName());
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
// Noch nicht aktivierte Registrierungen mit gleicher E-Mail oder Name überschreiben
|
||||
registrationRepository.findByEmail(registration.getEmail()).ifPresent(old -> {
|
||||
LOGGER.info("Überschreibe nicht aktivierte Registrierung mit E-Mail {}", registration.getEmail());
|
||||
registrationRepository.delete(old);
|
||||
});
|
||||
registrationRepository.findByName(registration.getName()).ifPresent(old -> {
|
||||
LOGGER.info("Überschreibe nicht aktivierte Registrierung mit Name {}", registration.getName());
|
||||
registrationRepository.delete(old);
|
||||
});
|
||||
// Passwort serverseitig mit BCrypt hashen
|
||||
registration.setPassword(passwordEncoder.encode(registration.getPassword()));
|
||||
RegistrationEntity entity = RegistrationEntity.create(registration);
|
||||
@@ -72,7 +80,7 @@ public class RegistrationController {
|
||||
String uuid = entity.getRegistrationId().toString();
|
||||
String activationLink = baseUrl + "/activation/" + uuid;
|
||||
String activatePageUrl = baseUrl + "/activate.html";
|
||||
email.setText(mailTemplateService.buildActivationMail(registration.getName(), activationLink, activatePageUrl, uuid));
|
||||
email.setText(mailTemplateService.buildActivationMail(registration.getName(), activationLink, activatePageUrl, entity.getActivationCode()));
|
||||
|
||||
if (!mailService.send(email)) {
|
||||
registrationRepository.delete(entity);
|
||||
|
||||
@@ -7,6 +7,7 @@ import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -27,6 +28,8 @@ public class RegistrationEntity {
|
||||
private String password;
|
||||
@Column
|
||||
private Boolean activated;
|
||||
@Column(length = 6)
|
||||
private String activationCode;
|
||||
@Column
|
||||
private LocalDate geburtsdatum;
|
||||
|
||||
@@ -50,6 +53,7 @@ public class RegistrationEntity {
|
||||
entity.setRegistrationId(UUID.randomUUID());
|
||||
entity.setEmail(registration.getEmail());
|
||||
entity.setActivated(Boolean.FALSE);
|
||||
entity.setActivationCode(String.format("%06d", new SecureRandom().nextInt(1_000_000)));
|
||||
entity.setName(registration.getName());
|
||||
entity.setPassword(registration.getPassword());
|
||||
entity.setGeburtsdatum(registration.getGeburtsdatum());
|
||||
|
||||
@@ -9,4 +9,5 @@ public interface RegistrationRepository extends JpaRepository<RegistrationEntity
|
||||
|
||||
Optional<RegistrationEntity> findByEmail(String email);
|
||||
Optional<RegistrationEntity> findByName(String name);
|
||||
Optional<RegistrationEntity> findByActivationCode(String activationCode);
|
||||
}
|
||||
|
||||
@@ -32,17 +32,18 @@ public class RegistrationService {
|
||||
* @throws IllegalArgumentException wenn UUID ungültig oder Registration nicht gefunden
|
||||
* @throws IllegalStateException wenn Registration bereits aktiviert
|
||||
*/
|
||||
public String activate(String uuid) {
|
||||
UUID registrationId;
|
||||
public String activate(String token) {
|
||||
RegistrationEntity registration;
|
||||
try {
|
||||
registrationId = UUID.fromString(uuid);
|
||||
UUID registrationId = UUID.fromString(token);
|
||||
registration = registrationRepository.findById(registrationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + token));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Ungültige UUID: " + uuid);
|
||||
// Kein UUID-Format → nach kurzem Aktivierungscode suchen
|
||||
registration = registrationRepository.findByActivationCode(token)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Aktivierungscode ungültig: " + token));
|
||||
}
|
||||
|
||||
RegistrationEntity registration = registrationRepository.findById(registrationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + uuid));
|
||||
|
||||
if (Boolean.TRUE.equals(registration.getActivated())) {
|
||||
throw new IllegalStateException("Registration bereits aktiviert");
|
||||
}
|
||||
@@ -52,7 +53,7 @@ public class RegistrationService {
|
||||
registration.setActivated(Boolean.TRUE);
|
||||
registrationRepository.save(registration);
|
||||
|
||||
LOGGER.info("Registration {} aktiviert, User {} angelegt", uuid, registration.getEmail());
|
||||
LOGGER.info("Registration {} aktiviert, User {} angelegt", token, registration.getEmail());
|
||||
return registration.getEmail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class SseService {
|
||||
}
|
||||
|
||||
public SseEmitter subscribe(UUID userId) {
|
||||
SseEmitter emitter = new SseEmitter(30_000L); // 30 s – Client reconnects automatically
|
||||
SseEmitter emitter = new SseEmitter(300_000L); // 5 min – Client reconnects automatically
|
||||
emitters.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>()).add(emitter);
|
||||
Runnable cleanup = () -> removeEmitter(userId, emitter);
|
||||
emitter.onCompletion(cleanup);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Datasource
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/xxxthegame?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/xxx_sphere?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
|
||||
spring.datasource.username=${DB_USER:xxx}
|
||||
spring.datasource.password=${DB_PASSWORD:xxxthegame$123!}
|
||||
spring.datasource.password=${DB_PASSWORD:xxx}
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
|
||||
# JPA / Hibernate
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>XXX The Game – Aktivierung</title>
|
||||
@@ -19,7 +19,7 @@
|
||||
</p>
|
||||
|
||||
<label for="uuid" style="margin-top:1.5rem;">Aktivierungscode</label>
|
||||
<input type="text" id="uuid" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" autocomplete="off" />
|
||||
<input type="text" id="uuid" placeholder="6-stelliger Code" autocomplete="off" inputmode="numeric" maxlength="6" />
|
||||
|
||||
<button class="full-width" id="activateBtn" onclick="activate()">Jetzt aktivieren</button>
|
||||
|
||||
|
||||
1642
xxxthegame/src/main/resources/static/admin.html
Normal file
1642
xxxthegame/src/main/resources/static/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -464,6 +464,7 @@
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script src="/js/meldung.js"></script>
|
||||
<script>
|
||||
// ── State ──
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -629,6 +630,7 @@
|
||||
} else {
|
||||
html += `<button id="friendActionBtn" onclick="addFriend()">+ Freund hinzufügen</button>`;
|
||||
}
|
||||
html += ` <button onclick="openMeldungDialog('PROFIL','${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⚑ Melden</button>`;
|
||||
actions.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>Feed – XXX The Game</title>
|
||||
@@ -163,9 +163,10 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script src="/js/meldung.js"></script>
|
||||
<script>
|
||||
// ── State ──
|
||||
let myUserId = null;
|
||||
@@ -260,6 +261,9 @@
|
||||
const deleteBtn = canDelete
|
||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
||||
: '';
|
||||
const meldenBtn = p.authorId !== myUserId
|
||||
? `<button class="post-action-btn" onclick="event.stopPropagation(); openMeldungDialog('POST','${p.postId}')" title="Melden" style="color:var(--color-muted)">⚑</button>`
|
||||
: '';
|
||||
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="openLb('${p.postId}','${p.postType}')" style="cursor:pointer;">
|
||||
@@ -281,6 +285,7 @@
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>
|
||||
${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
87
xxxthegame/src/main/resources/static/js/meldung.js
Normal file
87
xxxthegame/src/main/resources/static/js/meldung.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Wiederverwendbares Meldungs-Modul.
|
||||
* Bietet openMeldungDialog(zielTyp, zielId) und renderMeldenBtn(zielTyp, zielId).
|
||||
*/
|
||||
(function () {
|
||||
// Dialog einmalig in den DOM einfügen
|
||||
if (!document.getElementById('meldungDialog')) {
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<div id="meldungDialog" style="
|
||||
display:none; position:fixed; inset:0; z-index:9999;
|
||||
background:rgba(0,0,0,0.6); align-items:center; justify-content:center;">
|
||||
<div style="background:var(--color-card);border:1px solid var(--color-secondary);
|
||||
border-radius:12px;padding:1.5rem;width:min(420px,90vw);position:relative;">
|
||||
<h3 style="margin:0 0 1rem 0;color:var(--color-primary)">Inhalt melden</h3>
|
||||
<p id="meldungDialogLabel" style="color:var(--color-muted);font-size:0.9rem;margin:0 0 0.75rem 0;"></p>
|
||||
<textarea id="meldungGrund" placeholder="Grund (optional)"
|
||||
style="width:100%;box-sizing:border-box;padding:0.5rem;border-radius:6px;
|
||||
border:1px solid var(--color-secondary);background:var(--color-card);
|
||||
color:var(--color-text);resize:vertical;min-height:80px;font-family:inherit;"></textarea>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:1rem;">
|
||||
<button id="meldungAbbrechen" style="padding:0.45rem 1rem;border-radius:6px;
|
||||
border:1px solid var(--color-secondary);background:transparent;
|
||||
color:var(--color-text);cursor:pointer;">Abbrechen</button>
|
||||
<button id="meldungSenden" style="padding:0.45rem 1rem;border-radius:6px;
|
||||
border:none;background:var(--color-primary);color:#fff;cursor:pointer;font-weight:600;">
|
||||
Melden</button>
|
||||
</div>
|
||||
<p id="meldungMsg" style="margin:0.5rem 0 0 0;font-size:0.85rem;color:var(--color-primary);display:none;"></p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
document.getElementById('meldungAbbrechen').addEventListener('click', () => closeMeldungDialog());
|
||||
document.getElementById('meldungDialog').addEventListener('click', function (e) {
|
||||
if (e.target === this) closeMeldungDialog();
|
||||
});
|
||||
}
|
||||
|
||||
let _zielTyp = null, _zielId = null;
|
||||
|
||||
window.openMeldungDialog = function (zielTyp, zielId) {
|
||||
_zielTyp = zielTyp;
|
||||
_zielId = zielId;
|
||||
document.getElementById('meldungGrund').value = '';
|
||||
document.getElementById('meldungMsg').style.display = 'none';
|
||||
document.getElementById('meldungDialogLabel').textContent =
|
||||
zielTyp === 'PROFIL' ? 'Profil melden' : 'Post melden';
|
||||
document.getElementById('meldungDialog').style.display = 'flex';
|
||||
|
||||
document.getElementById('meldungSenden').onclick = async function () {
|
||||
const grund = document.getElementById('meldungGrund').value.trim();
|
||||
const r = await fetch('/meldung', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ zielTyp: _zielTyp, zielId: _zielId, grund: grund || null })
|
||||
});
|
||||
const msg = document.getElementById('meldungMsg');
|
||||
msg.style.display = 'block';
|
||||
if (r.status === 201) {
|
||||
msg.style.color = 'var(--color-success, #2ecc71)';
|
||||
msg.textContent = 'Meldung wurde übermittelt.';
|
||||
setTimeout(closeMeldungDialog, 1500);
|
||||
} else if (r.status === 409) {
|
||||
msg.style.color = 'var(--color-primary)';
|
||||
msg.textContent = 'Du hast diesen Inhalt bereits gemeldet.';
|
||||
} else {
|
||||
msg.style.color = 'var(--color-primary)';
|
||||
msg.textContent = 'Fehler beim Senden.';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
window.closeMeldungDialog = function () {
|
||||
document.getElementById('meldungDialog').style.display = 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* Erzeugt einen kleinen "Melden"-Button-HTML-String.
|
||||
* Verwendung: in innerHTML-Templates, wo onclick genutzt werden kann.
|
||||
*/
|
||||
window.renderMeldenBtn = function (zielTyp, zielId) {
|
||||
return `<button onclick="openMeldungDialog('${zielTyp}','${zielId}')"
|
||||
style="background:none;border:none;color:var(--color-muted,#888);
|
||||
font-size:0.8rem;cursor:pointer;padding:0.2rem 0.4rem;border-radius:4px;"
|
||||
title="Melden">⚑ Melden</button>`;
|
||||
};
|
||||
})();
|
||||
@@ -74,6 +74,9 @@
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
const adminCls = path === '/admin.html' ? ' class="active"' : '';
|
||||
const adminItem = `<li id="navAdminLink" style="display:none"><a href="/admin.html"${adminCls}><span class="icon">${I('ADMIN') || '⚙'}</span> Administration</a></li>`;
|
||||
|
||||
document.body.insertAdjacentHTML('afterbegin', `
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
@@ -89,6 +92,8 @@
|
||||
${socialNav}
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
${nav}
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
|
||||
${adminItem}
|
||||
</ul>
|
||||
</aside>
|
||||
`);
|
||||
@@ -155,6 +160,17 @@
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Admin-Link
|
||||
try {
|
||||
const adminRes = await fetch('/admin/me');
|
||||
if (adminRes.ok) {
|
||||
const navAdminLink = document.getElementById('navAdminLink');
|
||||
const navAdminDivider = document.getElementById('navAdminDivider');
|
||||
if (navAdminLink) navAdminLink.style.display = '';
|
||||
if (navAdminDivider) navAdminDivider.style.display = '';
|
||||
}
|
||||
} catch (_) {}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
|
||||
@@ -86,9 +86,17 @@
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
setTimeout(connectSse, 5000);
|
||||
// Vor dem Reconnect prüfen ob noch eingeloggt (verhindert Endlos-Schleife bei abgelaufener Session)
|
||||
setTimeout(() => {
|
||||
fetch('/login/me', { method: 'GET' })
|
||||
.then(r => { if (r.ok) connectSse(); })
|
||||
.catch(() => {});
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
connectSse();
|
||||
// SSE nur starten wenn authentifiziert – verhindert Fehler-Spam bei nicht eingeloggten Seiten
|
||||
fetch('/login/me', { method: 'GET' })
|
||||
.then(r => { if (r.ok) connectSse(); })
|
||||
.catch(() => {});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user