Userrollen Verwaltung, Admin-Ablauf möglich, Rolle GOD

This commit is contained in:
klaas 2025-11-04 22:32:51 +01:00
parent fcd94bfba0
commit fd3f82659f
23 changed files with 394 additions and 127 deletions

View File

@ -2,12 +2,14 @@ package it.boergmann.tkdApp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
public class TkdApiApplication {
@EnableScheduling
public class TkdAppApplication {
public static void main(String[] args) {
SpringApplication.run(TkdApiApplication.class, args);
SpringApplication.run(TkdAppApplication.class, args);
}
}

View File

@ -2,20 +2,31 @@ package it.boergmann.tkdApp.config;
import it.boergmann.tkdApp.security.CustomUserDetailsService;
import it.boergmann.tkdApp.security.JwtAuthenticationFilter;
import jakarta.servlet.Filter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Configuration
@EnableWebSecurity
@ -25,18 +36,34 @@ public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthFilter;
// 🧱 1. API Security (JWT, kein Redirect)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
.authorizeHttpRequests(auth -> auth
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/login", "/api/register").permitAll()
.requestMatchers("/admin/**", "/api/users/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.requestMatchers("/register", "/login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, ex1) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 🌐 2. Web Security (Form Login)
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
@ -44,12 +71,36 @@ public class SecurityConfig {
.defaultSuccessUrl("/me", true)
.permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.exceptionHandling(exception -> exception
.accessDeniedPage("/access-denied")
);
return http.build();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler(); // Siehe unten
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// Leite weiter auf eine benutzerdefinierte Seite
response.sendRedirect("/access-denied");
}
}
@Bean
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

View File

@ -49,25 +49,4 @@ public class AuthController {
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody @Valid RegisterRequest request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Benutzername bereits vergeben");
}
if (userRepository.findAll().stream().anyMatch(u -> u.getEmail().equalsIgnoreCase(request.getEmail()))) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Email bereits registriert");
}
AppUser user = AppUser.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.email(request.getEmail())
.roles(List.of(Role.NONE)) // 👉 noch nicht freigeschaltet
.build();
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body("Registrierung erfolgreich. Admin muss freischalten.");
}
}

View File

@ -3,30 +3,44 @@ package it.boergmann.tkdApp.controller.api;
import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.service.AppUserService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'GOD')")
public class UserAdminApiController {
private static final Logger log = LoggerFactory.getLogger(UserAdminApiController.class);
private final AppUserService userService;
@PostMapping("/{id}/enable")
public ResponseEntity<?> enableUser(@PathVariable UUID id, @RequestParam Role role) {
userService.activateUser(id, role);
return ResponseEntity.ok(Map.of("message", "Benutzer freigeschaltet"));
@PostMapping("/{id}/role/add")
public ResponseEntity<?> addRoleApi(@PathVariable UUID id,
@RequestParam Role role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validUntil) {
log.info("Role Add called");
userService.updateUserRole(id, role, validUntil);
Map<String, Object> response = new HashMap<>(Map.of("message", "Rolle hinzugefügt"));
Optional.ofNullable(validUntil)
.ifPresent(v -> response.put("expires", v));
return ResponseEntity.ok(response);
}
@PutMapping("/{id}/role")
public ResponseEntity<?> updateRole(@PathVariable UUID id, @RequestParam Role role) {
userService.changeUserRole(id, role);
return ResponseEntity.ok(Map.of("message", "Benutzerrolle aktualisiert"));
@GetMapping("/test")
public ResponseEntity<?> testApiAccess() {
log.info("TEST CALLED");
return ResponseEntity.ok("Du bist eingeloggt als: " + SecurityContextHolder.getContext().getAuthentication().getName());
}
}

View File

@ -2,17 +2,23 @@ package it.boergmann.tkdApp.controller.api;
import it.boergmann.tkdApp.dto.AppUserResponse;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.dto.RegisterRequest;
import it.boergmann.tkdApp.service.AppUserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserController {
public class UserApiController {
private final AppUserService appUserService;
private static final Logger log = LoggerFactory.getLogger(UserAdminApiController.class);
@GetMapping("/me")
public ResponseEntity<AppUserResponse> getCurrentUser() {
@ -20,8 +26,14 @@ public class UserController {
AppUserResponse response = new AppUserResponse(
user.getId(),
user.getUsername(),
user.getRoles()
user.getAppUserRoles()
);
return ResponseEntity.ok(response);
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody RegisterRequest request) {
appUserService.registerNewUser(request);
return ResponseEntity.ok().build();
}
}

View File

@ -5,42 +5,48 @@ import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.service.AppUserService;
import it.boergmann.tkdApp.repository.AppUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Controller
@RequestMapping("/admin")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'GOD')")
public class AdminController {
private final AppUserRepository userRepository;
private final AppUserService userService;
@GetMapping("/users")
public String showPendingUsers(Model model) {
List<AppUser> pendingUsers = userRepository.findAll().stream()
.filter(u -> u.getRoles().contains(Role.NONE))
.toList();
public String showAllUsers(Model model) {
List<AppUser> allUsers = userRepository.findAll();
model.addAttribute("users", pendingUsers);
model.addAttribute("users", allUsers);
model.addAttribute("roles", Role.values()); // für Dropdown
return "admin_users";
}
@PostMapping("/users/{id}/enable")
public String enableUser(@PathVariable UUID id, @RequestParam Role role) {
userService.activateUser(id, role);
@PostMapping("/users/update")
public String updateUserRoles(@RequestParam UUID userId,
@RequestParam Role role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validUntil) {
userService.updateUserRole(userId, role, validUntil);
return "redirect:/admin/users";
}
@PostMapping("/users/{id}/role")
public String changeUserRole(@PathVariable UUID id, @RequestParam Role role) {
userService.changeUserRole(id, role);
@PostMapping("/users/{id}/role/add")
public String addRole(@PathVariable UUID id,
@RequestParam Role role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validUntil) {
userService.updateUserRole(id, role, validUntil);
return "redirect:/admin/users";
}
}

View File

@ -4,6 +4,7 @@ import it.boergmann.tkdApp.dto.RegisterRequest;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.repository.AppUserRepository;
import it.boergmann.tkdApp.service.AppUserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -16,9 +17,9 @@ import java.util.List;
@Controller
@RequiredArgsConstructor
public class RegisterController {
public class UserController {
private final AppUserRepository userRepository;
private final AppUserService appUserService;
private final PasswordEncoder passwordEncoder;
@GetMapping("/register")
@ -29,26 +30,7 @@ public class RegisterController {
@PostMapping("/register")
public String handleRegister(@ModelAttribute("user") @Valid RegisterRequest request, BindingResult result, Model model) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
result.rejectValue("username", "error.user", "Benutzername existiert bereits");
}
if (userRepository.findAll().stream().anyMatch(u -> u.getEmail().equalsIgnoreCase(request.getEmail()))) {
result.rejectValue("email", "error.email", "Email-Adresse ist bereits registriert");
}
if (result.hasErrors()) {
return "register";
}
AppUser newUser = AppUser.builder()
.username(request.getUsername())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.roles(List.of(Role.NONE))
.build();
userRepository.save(newUser);
appUserService.registerNewUser(request);
model.addAttribute("message", "Registrierung erfolgreich! Bitte auf Freischaltung durch Admin warten.");
return "login";

View File

@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
@ -24,4 +26,10 @@ public class WebController {
model.addAttribute("user", user);
return "profile"; // src/main/resources/templates/profile.html
}
@GetMapping("/access-denied")
public String accessDenied() {
return "error/403";
}
}

View File

@ -5,6 +5,8 @@ import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
@ -27,19 +29,20 @@ public class AppUser implements UserDetails{
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
private List<Role> roles;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@Builder.Default
private List<AppUserRole> appUserRoles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role ->(GrantedAuthority)() -> "ROLE_" +role)
LocalDateTime now = LocalDateTime.now();
return appUserRoles.stream()
.filter(r -> r.getValidUntil() == null || r.getValidUntil().isAfter(now)) // nur gültige Rollen
.map(r -> (GrantedAuthority) () -> "ROLE_" + r.getRole().name())
.toList();
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
}

View File

@ -0,0 +1,29 @@
package it.boergmann.tkdApp.domain;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppUserRole {
@Id
@GeneratedValue
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private AppUser user;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
private LocalDateTime validUntil; // 🕒 Ablaufzeitpunkt, optional
}

View File

@ -3,5 +3,6 @@ package it.boergmann.tkdApp.domain;
public enum Role {
NONE,
USER,
ADMIN
ADMIN,
GOD
}

View File

@ -1,5 +1,6 @@
package it.boergmann.tkdApp.dto;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.domain.Role;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -12,5 +13,5 @@ import java.util.UUID;
public class AppUserResponse {
private UUID id;
private String username;
private List<Role> roles;
private List<AppUserRole> roles;
}

View File

@ -7,14 +7,14 @@ import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank
@NotBlank(message = "Username darf nicht leer sein")
private String username;
@NotBlank
@NotBlank(message = "Passwort darf nicht leer sein")
private String password;
@Email
@NotBlank
@NotBlank(message = "Email darf nicht leer sein")
@Email(message = "Ungültige E-Mail-Adresse")
private String email;
}

View File

@ -0,0 +1,57 @@
package it.boergmann.tkdApp.init;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.repository.AppUserRepository;
import it.boergmann.tkdApp.repository.AppUserRoleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class StartupAdminInitializer {
private final AppUserRepository userRepository;
private final AppUserRoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
@Bean
public ApplicationRunner initAdminUser() {
return args -> {
boolean adminExists = userRepository.findAll().stream()
.flatMap(user -> user.getAppUserRoles().stream())
.anyMatch(role -> role.getRole() == Role.ADMIN &&
(role.getValidUntil() == null || role.getValidUntil().isAfter(LocalDateTime.now())));
if (!adminExists) {
System.out.println("⚠️ Kein aktiver Admin gefunden. Erstelle Standard-Admin-User...");
AppUser admin = AppUser.builder()
.email("admin@boergmann.it")
.username("admin")
.password(passwordEncoder.encode("Dimrb#7361"))
.appUserRoles(List.of()) // Initial leer
.build();
AppUser savedUser = userRepository.save(admin);
AppUserRole adminRole = AppUserRole.builder()
.user(savedUser)
.role(Role.GOD)
.validUntil(null) // dauerhaft gültig
.build();
roleRepository.save(adminRole);
System.out.println("✅ Admin-User admin/admin123 wurde erfolgreich angelegt.");
}
};
}
}

View File

@ -0,0 +1,43 @@
package it.boergmann.tkdApp.jobs;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.repository.AppUserRepository;
import it.boergmann.tkdApp.repository.AppUserRoleRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
@Component
@RequiredArgsConstructor
public class RoleCleanupScheduler {
private final AppUserRoleRepository appUserRoleRepository;
private final AppUserRepository appUserRepository;
@Scheduled(cron = "0 0 * * * *") // stündlich z. B.
public void downgradeExpiredAdmins() {
List<AppUserRole> expiredAdmins = appUserRoleRepository.findExpiredAppUserRole(Role.ADMIN, LocalDateTime.now());
for (AppUserRole expired : expiredAdmins) {
appUserRoleRepository.delete(expired);
AppUser user = expired.getUser();
// Nur wenn keine andere Rolle vorhanden, neue Rolle setzen
if (user.getAppUserRoles().isEmpty()) {
var fallback = AppUserRole.builder()
.user(user)
.role(Role.USER)
.build();
user.getAppUserRoles().add(fallback);
appUserRepository.save(user);
}
}
}
}

View File

@ -1,11 +1,20 @@
package it.boergmann.tkdApp.repository;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface AppUserRepository extends JpaRepository<AppUser, UUID> {
boolean existsByUsername(String username); // 🔥 hier hinzufügen!
boolean existsByEmail(String email);
Optional<AppUser> findByUsername(String username);
}

View File

@ -0,0 +1,16 @@
package it.boergmann.tkdApp.repository;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public interface AppUserRoleRepository extends JpaRepository<AppUserRole, UUID> {
@Query("SELECT r FROM AppUserRole r WHERE r.role = :role AND r.validUntil IS NOT NULL AND r.validUntil < :now")
List<AppUserRole> findExpiredAppUserRole(@Param("role") Role role, @Param("now") LocalDateTime now);
}

View File

@ -20,7 +20,7 @@ public class CustomUserDetailsService implements UserDetailsService {
AppUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
if (user.getRoles() == null || user.getRoles().contains(Role.NONE) || user.getRoles().isEmpty()) {
if (user.getAppUserRoles() == null || user.getAppUserRoles().contains(Role.NONE) || user.getAppUserRoles().isEmpty()) {
throw new UsernameNotFoundException("User has no active roles");
}

View File

@ -2,13 +2,17 @@ package it.boergmann.tkdApp.service;
import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.domain.AppUserRole;
import it.boergmann.tkdApp.dto.RegisterRequest;
import it.boergmann.tkdApp.repository.AppUserRepository;
import it.boergmann.tkdApp.repository.AppUserRoleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
@ -16,6 +20,8 @@ import java.util.UUID;
public class AppUserService {
private final AppUserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AppUserRoleRepository roleAssignmentRepository;
public AppUser getCurrentUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
@ -23,23 +29,52 @@ public class AppUserService {
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
public void activateUser(UUID userId, Role newRole) {
public void updateUserRole(UUID userId, Role newRole, LocalDateTime validUntil) {
System.out.println(userId.toString());
System.out.println(newRole.toString());
AppUser user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
if (!user.getRoles().contains(Role.NONE)) {
throw new IllegalStateException("User is already activated");
if (user.getAppUserRoles().stream().anyMatch(ra -> ra.getRole() == Role.GOD)) {
throw new IllegalStateException("GOD-Nutzer dürfen nicht geändert werden.");
}
user.setRoles(List.of(newRole)); // Nur eine Rolle zuweisen oder erweitern
// Alte Rollen entfernen
user.getAppUserRoles().clear();
// Neue Rolle zuweisen
var roleAssignment = AppUserRole.builder()
.user(user)
.role(newRole)
.validUntil(validUntil)
.build();
user.getAppUserRoles().add(roleAssignment);
userRepository.save(user);
}
public void changeUserRole(UUID userId, Role newRole) {
AppUser user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
public AppUser registerNewUser(RegisterRequest newUser) {
if (userRepository.existsByUsername(newUser.getUsername()) || userRepository.existsByEmail(newUser.getEmail())) {
throw new IllegalArgumentException("Benutzername oder E-Mail bereits vergeben");
}
user.setRoles(List.of(newRole)); // 👈 Rolle hart überschrieben (alternativ: erweitern)
userRepository.save(user);
var user = AppUser.builder()
.username(newUser.getUsername())
.email(newUser.getEmail())
.password(passwordEncoder.encode(newUser.getPassword()))
.build();
var savedUser = userRepository.save(user);
// Setze initiale Rolle: NONE
var roleAssignment = AppUserRole.builder()
.user(savedUser)
.role(Role.NONE)
.validUntil(null)
.build();
roleAssignmentRepository.save(roleAssignment);
return savedUser;
}
}

View File

@ -5,7 +5,7 @@ spring:
password: Sanctuary1-Crane-Erupt-Bogged
jpa:
hibernate:
ddl-auto: update
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect

View File

@ -1,31 +1,36 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Benutzer freischalten</title>
</head>
<body>
<h1>Offene Registrierungen</h1>
<table border="1">
<h1>Benutzerverwaltung</h1>
<table>
<thead>
<tr>
<th>Benutzername</th>
<th>Username</th>
<th>Email</th>
<th>Aktuelle Rolle</th>
<th>Neue Rolle</th>
<th>Gültig bis (nur für ADMIN)</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.username}">Benutzer</td>
<td th:text="${user.email}">Email</td>
<td>
<form th:action="@{'/admin/users/' + ${user.id} + '/role'}" method="post">
<form th:action="@{/admin/users/update}" method="post">
<input type="hidden" name="userId" th:value="${user.id}"/>
<td th:text="${user.username}"></td>
<td th:text="${user.email}"></td>
<td>
<span th:each="ra : ${user.appUserRoles}"
th:text="${ra.role} + ' (bis ' + ${ra.validUntil} + ')'"></span>
</td>
<td>
<select name="role">
<option th:selected="${user.roles.contains(T(it.boergmann.tkdApp.domain.Role).USER)}" value="USER">USER</option>
<option th:selected="${user.roles.contains(T(it.boergmann.tkdApp.domain.Role).ADMIN)}" value="ADMIN">ADMIN</option>
<option th:selected="${user.roles.contains(T(it.boergmann.tkdApp.domain.Role).NONE)}" value="NONE">NONE</option>
<option th:each="r : ${roles}"
th:value="${r}"
th:text="${r}"></option>
</select>
<button type="submit">Rolle ändern</button>
</form>
</td>
</td>
<td><input type="datetime-local" name="validUntil"/></td>
<td><button type="submit">Ändern</button></td>
</form>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!-- src/main/resources/templates/error/403.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Kein Zugriff</title>
</head>
<body>
<h1>🚫 Zugriff verweigert</h1>
<p>Du hast leider keine Berechtigung für diese Seite.</p>
<a href="/static" th:href="@{/static}">Zur Startseite</a>
</body>
</html>

View File

@ -5,7 +5,9 @@
</head>
<body>
<h1>Willkommen, <span th:text="${user.username}">Nutzer</span>!</h1>
<p>Deine Rollen: <span th:text="${user.roles}">[]</span></p>
<p>Deine Rollen: <span th:each="ra : ${user.appUserRoles}"
th:text="${ra.role} + ' (bis ' + ${ra.validUntil} + ')'"></span>
</td></p>
<a href="/logout">Logout</a>
</body>
</html>