Adminmasken angefangen

This commit is contained in:
2026-03-23 23:16:30 +01:00
parent 4f521b6725
commit b63af50786
62 changed files with 4881 additions and 1856 deletions

View 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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.meldung;
public enum MeldungStatus {
OFFEN, BEARBEITET, ABGELEHNT
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.meldung;
public enum MeldungZielTyp {
POST, PROFIL
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
}

View File

@@ -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>`;
}

View 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>`;
};
})();

View File

@@ -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(() => {});

View File

@@ -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(() => {});
})();