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.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
public class TkdApiApplication { @EnableScheduling
public class TkdAppApplication {
public static void main(String[] args) { 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.CustomUserDetailsService;
import it.boergmann.tkdApp.security.JwtAuthenticationFilter; 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 lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@ -25,18 +36,34 @@ public class SecurityConfig {
private final CustomUserDetailsService userDetailsService; private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthFilter; private final JwtAuthenticationFilter jwtAuthFilter;
// 🧱 1. API Security (JWT, kein Redirect)
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http http
.csrf(csrf -> csrf.disable()) .securityMatcher("/api/**")
.sessionManagement(session -> session .csrf(AbstractHttpConfigurer::disable)
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
) .authorizeHttpRequests(authz -> authz
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login", "/api/register").permitAll() .requestMatchers("/api/login", "/api/register").permitAll()
.requestMatchers("/admin/**", "/api/users/**").hasRole("ADMIN") .anyRequest().authenticated()
.requestMatchers("/api/**").authenticated() )
.requestMatchers("/register", "/login", "/css/**", "/js/**").permitAll() .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() .anyRequest().authenticated()
) )
.formLogin(form -> form .formLogin(form -> form
@ -44,12 +71,36 @@ public class SecurityConfig {
.defaultSuccessUrl("/me", true) .defaultSuccessUrl("/me", true)
.permitAll() .permitAll()
) )
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); .logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.exceptionHandling(exception -> exception
.accessDeniedPage("/access-denied")
);
return http.build(); 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 @Bean
public AuthenticationProvider authProvider() { public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); 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.domain.Role;
import it.boergmann.tkdApp.service.AppUserService; import it.boergmann.tkdApp.service.AppUserService;
import lombok.RequiredArgsConstructor; 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.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/users")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'GOD')")
public class UserAdminApiController { public class UserAdminApiController {
private static final Logger log = LoggerFactory.getLogger(UserAdminApiController.class);
private final AppUserService userService; private final AppUserService userService;
@PostMapping("/{id}/enable") @PostMapping("/{id}/role/add")
public ResponseEntity<?> enableUser(@PathVariable UUID id, @RequestParam Role role) { public ResponseEntity<?> addRoleApi(@PathVariable UUID id,
userService.activateUser(id, role); @RequestParam Role role,
return ResponseEntity.ok(Map.of("message", "Benutzer freigeschaltet")); @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") @GetMapping("/test")
public ResponseEntity<?> updateRole(@PathVariable UUID id, @RequestParam Role role) { public ResponseEntity<?> testApiAccess() {
userService.changeUserRole(id, role); log.info("TEST CALLED");
return ResponseEntity.ok(Map.of("message", "Benutzerrolle aktualisiert")); 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.dto.AppUserResponse;
import it.boergmann.tkdApp.domain.AppUser; import it.boergmann.tkdApp.domain.AppUser;
import it.boergmann.tkdApp.dto.RegisterRequest;
import it.boergmann.tkdApp.service.AppUserService; import it.boergmann.tkdApp.service.AppUserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserController { public class UserApiController {
private final AppUserService appUserService; private final AppUserService appUserService;
private static final Logger log = LoggerFactory.getLogger(UserAdminApiController.class);
@GetMapping("/me") @GetMapping("/me")
public ResponseEntity<AppUserResponse> getCurrentUser() { public ResponseEntity<AppUserResponse> getCurrentUser() {
@ -20,8 +26,14 @@ public class UserController {
AppUserResponse response = new AppUserResponse( AppUserResponse response = new AppUserResponse(
user.getId(), user.getId(),
user.getUsername(), user.getUsername(),
user.getRoles() user.getAppUserRoles()
); );
return ResponseEntity.ok(response); 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.service.AppUserService;
import it.boergmann.tkdApp.repository.AppUserRepository; import it.boergmann.tkdApp.repository.AppUserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@Controller @Controller
@RequestMapping("/admin") @RequestMapping("/admin")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'GOD')")
public class AdminController { public class AdminController {
private final AppUserRepository userRepository; private final AppUserRepository userRepository;
private final AppUserService userService; private final AppUserService userService;
@GetMapping("/users") @GetMapping("/users")
public String showPendingUsers(Model model) { public String showAllUsers(Model model) {
List<AppUser> pendingUsers = userRepository.findAll().stream() List<AppUser> allUsers = userRepository.findAll();
.filter(u -> u.getRoles().contains(Role.NONE))
.toList();
model.addAttribute("users", pendingUsers); model.addAttribute("users", allUsers);
model.addAttribute("roles", Role.values()); // für Dropdown
return "admin_users"; return "admin_users";
} }
@PostMapping("/users/{id}/enable") @PostMapping("/users/update")
public String enableUser(@PathVariable UUID id, @RequestParam Role role) { public String updateUserRoles(@RequestParam UUID userId,
userService.activateUser(id, role); @RequestParam Role role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validUntil) {
userService.updateUserRole(userId, role, validUntil);
return "redirect:/admin/users"; return "redirect:/admin/users";
} }
@PostMapping("/users/{id}/role") @PostMapping("/users/{id}/role/add")
public String changeUserRole(@PathVariable UUID id, @RequestParam Role role) { public String addRole(@PathVariable UUID id,
userService.changeUserRole(id, role); @RequestParam Role role,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validUntil) {
userService.updateUserRole(id, role, validUntil);
return "redirect:/admin/users"; 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.AppUser;
import it.boergmann.tkdApp.domain.Role; import it.boergmann.tkdApp.domain.Role;
import it.boergmann.tkdApp.repository.AppUserRepository; import it.boergmann.tkdApp.repository.AppUserRepository;
import it.boergmann.tkdApp.service.AppUserService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@ -16,9 +17,9 @@ import java.util.List;
@Controller @Controller
@RequiredArgsConstructor @RequiredArgsConstructor
public class RegisterController { public class UserController {
private final AppUserRepository userRepository; private final AppUserRepository userRepository;
private final AppUserService appUserService;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
@GetMapping("/register") @GetMapping("/register")
@ -29,26 +30,7 @@ public class RegisterController {
@PostMapping("/register") @PostMapping("/register")
public String handleRegister(@ModelAttribute("user") @Valid RegisterRequest request, BindingResult result, Model model) { public String handleRegister(@ModelAttribute("user") @Valid RegisterRequest request, BindingResult result, Model model) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) { appUserService.registerNewUser(request);
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);
model.addAttribute("message", "Registrierung erfolgreich! Bitte auf Freischaltung durch Admin warten."); model.addAttribute("message", "Registrierung erfolgreich! Bitte auf Freischaltung durch Admin warten.");
return "login"; return "login";

View File

@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller @Controller
@RequiredArgsConstructor @RequiredArgsConstructor
@ -24,4 +26,10 @@ public class WebController {
model.addAttribute("user", user); model.addAttribute("user", user);
return "profile"; // src/main/resources/templates/profile.html 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.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -27,19 +29,20 @@ public class AppUser implements UserDetails{
private String password; private String password;
@ElementCollection(fetch = FetchType.EAGER) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@Enumerated(EnumType.STRING) @Builder.Default
private List<Role> roles; private List<AppUserRole> appUserRoles = new ArrayList<>();
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream() LocalDateTime now = LocalDateTime.now();
.map(role ->(GrantedAuthority)() -> "ROLE_" +role) return appUserRoles.stream()
.filter(r -> r.getValidUntil() == null || r.getValidUntil().isAfter(now)) // nur gültige Rollen
.map(r -> (GrantedAuthority) () -> "ROLE_" + r.getRole().name())
.toList(); .toList();
} }
@Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; } @Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { 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 { public enum Role {
NONE, NONE,
USER, USER,
ADMIN ADMIN,
GOD
} }

View File

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

View File

@ -7,14 +7,14 @@ import lombok.Data;
@Data @Data
public class RegisterRequest { public class RegisterRequest {
@NotBlank @NotBlank(message = "Username darf nicht leer sein")
private String username; private String username;
@NotBlank @NotBlank(message = "Passwort darf nicht leer sein")
private String password; private String password;
@Email @NotBlank(message = "Email darf nicht leer sein")
@NotBlank @Email(message = "Ungültige E-Mail-Adresse")
private String email; 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; package it.boergmann.tkdApp.repository;
import it.boergmann.tkdApp.domain.AppUser; 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.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.Optional;
import java.util.UUID; import java.util.UUID;
public interface AppUserRepository extends JpaRepository<AppUser, UUID> { public interface AppUserRepository extends JpaRepository<AppUser, UUID> {
boolean existsByUsername(String username); // 🔥 hier hinzufügen!
boolean existsByEmail(String email);
Optional<AppUser> findByUsername(String username); 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) AppUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + 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"); 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.AppUser;
import it.boergmann.tkdApp.domain.Role; 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.AppUserRepository;
import it.boergmann.tkdApp.repository.AppUserRoleRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
@Service @Service
@ -16,6 +20,8 @@ import java.util.UUID;
public class AppUserService { public class AppUserService {
private final AppUserRepository userRepository; private final AppUserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AppUserRoleRepository roleAssignmentRepository;
public AppUser getCurrentUser() { public AppUser getCurrentUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName(); String username = SecurityContextHolder.getContext().getAuthentication().getName();
@ -23,23 +29,52 @@ public class AppUserService {
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); .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) AppUser user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found")); .orElseThrow(() -> new IllegalArgumentException("User not found"));
if (!user.getRoles().contains(Role.NONE)) { if (user.getAppUserRoles().stream().anyMatch(ra -> ra.getRole() == Role.GOD)) {
throw new IllegalStateException("User is already activated"); 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); userRepository.save(user);
} }
public void changeUserRole(UUID userId, Role newRole) { public AppUser registerNewUser(RegisterRequest newUser) {
AppUser user = userRepository.findById(userId) if (userRepository.existsByUsername(newUser.getUsername()) || userRepository.existsByEmail(newUser.getEmail())) {
.orElseThrow(() -> new IllegalArgumentException("User not found")); throw new IllegalArgumentException("Benutzername oder E-Mail bereits vergeben");
}
user.setRoles(List.of(newRole)); // 👈 Rolle hart überschrieben (alternativ: erweitern) var user = AppUser.builder()
userRepository.save(user); .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 password: Sanctuary1-Crane-Erupt-Bogged
jpa: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: create-drop
properties: properties:
hibernate: hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect dialect: org.hibernate.dialect.PostgreSQLDialect

View File

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