diff --git a/src/main/java/it/boergmann/tkdApp/TkdApiApplication.java b/src/main/java/it/boergmann/tkdApp/TkdAppApplication.java similarity index 55% rename from src/main/java/it/boergmann/tkdApp/TkdApiApplication.java rename to src/main/java/it/boergmann/tkdApp/TkdAppApplication.java index d07b101..449fd57 100644 --- a/src/main/java/it/boergmann/tkdApp/TkdApiApplication.java +++ b/src/main/java/it/boergmann/tkdApp/TkdAppApplication.java @@ -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); } } diff --git a/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java b/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java index 66a7179..260e613 100644 --- a/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java +++ b/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java @@ -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(); diff --git a/src/main/java/it/boergmann/tkdApp/controller/api/AuthController.java b/src/main/java/it/boergmann/tkdApp/controller/api/AuthController.java index 565a44b..19a2855 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/api/AuthController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/api/AuthController.java @@ -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."); - } } diff --git a/src/main/java/it/boergmann/tkdApp/controller/api/UserAdminApiController.java b/src/main/java/it/boergmann/tkdApp/controller/api/UserAdminApiController.java index a70941c..91152c0 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/api/UserAdminApiController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/api/UserAdminApiController.java @@ -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 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()); } } diff --git a/src/main/java/it/boergmann/tkdApp/controller/api/UserController.java b/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java similarity index 57% rename from src/main/java/it/boergmann/tkdApp/controller/api/UserController.java rename to src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java index 34bf1f0..29768c3 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/api/UserController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java @@ -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 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(); + } } diff --git a/src/main/java/it/boergmann/tkdApp/controller/web/AdminController.java b/src/main/java/it/boergmann/tkdApp/controller/web/AdminController.java index c1c4d0f..1049c47 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/web/AdminController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/web/AdminController.java @@ -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 pendingUsers = userRepository.findAll().stream() - .filter(u -> u.getRoles().contains(Role.NONE)) - .toList(); + public String showAllUsers(Model model) { + List 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"; } } diff --git a/src/main/java/it/boergmann/tkdApp/controller/web/RegisterController.java b/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java similarity index 59% rename from src/main/java/it/boergmann/tkdApp/controller/web/RegisterController.java rename to src/main/java/it/boergmann/tkdApp/controller/web/UserController.java index db21681..7444716 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/web/RegisterController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java @@ -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"; diff --git a/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java b/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java index 04e30ac..307e4fd 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java @@ -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"; + } + } diff --git a/src/main/java/it/boergmann/tkdApp/domain/AppUser.java b/src/main/java/it/boergmann/tkdApp/domain/AppUser.java index 5e2a0f2..adf5e4c 100644 --- a/src/main/java/it/boergmann/tkdApp/domain/AppUser.java +++ b/src/main/java/it/boergmann/tkdApp/domain/AppUser.java @@ -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 roles; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) + @Builder.Default + private List appUserRoles = new ArrayList<>(); @Override public Collection 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; } } diff --git a/src/main/java/it/boergmann/tkdApp/domain/AppUserRole.java b/src/main/java/it/boergmann/tkdApp/domain/AppUserRole.java new file mode 100644 index 0000000..de90f9e --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/domain/AppUserRole.java @@ -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 +} diff --git a/src/main/java/it/boergmann/tkdApp/domain/Role.java b/src/main/java/it/boergmann/tkdApp/domain/Role.java index c0480c3..dcd88c7 100644 --- a/src/main/java/it/boergmann/tkdApp/domain/Role.java +++ b/src/main/java/it/boergmann/tkdApp/domain/Role.java @@ -3,5 +3,6 @@ package it.boergmann.tkdApp.domain; public enum Role { NONE, USER, - ADMIN + ADMIN, + GOD } diff --git a/src/main/java/it/boergmann/tkdApp/dto/AppUserResponse.java b/src/main/java/it/boergmann/tkdApp/dto/AppUserResponse.java index 0f16de4..71aa3c8 100644 --- a/src/main/java/it/boergmann/tkdApp/dto/AppUserResponse.java +++ b/src/main/java/it/boergmann/tkdApp/dto/AppUserResponse.java @@ -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 roles; + private List roles; } diff --git a/src/main/java/it/boergmann/tkdApp/dto/RegisterRequest.java b/src/main/java/it/boergmann/tkdApp/dto/RegisterRequest.java index 8f15cba..9dacb95 100644 --- a/src/main/java/it/boergmann/tkdApp/dto/RegisterRequest.java +++ b/src/main/java/it/boergmann/tkdApp/dto/RegisterRequest.java @@ -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; } diff --git a/src/main/java/it/boergmann/tkdApp/init/StartupAdminInitializer.java b/src/main/java/it/boergmann/tkdApp/init/StartupAdminInitializer.java new file mode 100644 index 0000000..8a6a4e8 --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/init/StartupAdminInitializer.java @@ -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."); + } + }; + } +} diff --git a/src/main/java/it/boergmann/tkdApp/jobs/RoleCleanupScheduler.java b/src/main/java/it/boergmann/tkdApp/jobs/RoleCleanupScheduler.java new file mode 100644 index 0000000..1e3685e --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/jobs/RoleCleanupScheduler.java @@ -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 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); + } + } + } +} diff --git a/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java b/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java index dc3544c..b489271 100644 --- a/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java +++ b/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java @@ -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 { + boolean existsByUsername(String username); // 🔥 hier hinzufügen! + boolean existsByEmail(String email); Optional findByUsername(String username); + } \ No newline at end of file diff --git a/src/main/java/it/boergmann/tkdApp/repository/AppUserRoleRepository.java b/src/main/java/it/boergmann/tkdApp/repository/AppUserRoleRepository.java new file mode 100644 index 0000000..9d1f19a --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/repository/AppUserRoleRepository.java @@ -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 { + @Query("SELECT r FROM AppUserRole r WHERE r.role = :role AND r.validUntil IS NOT NULL AND r.validUntil < :now") + List findExpiredAppUserRole(@Param("role") Role role, @Param("now") LocalDateTime now); +} diff --git a/src/main/java/it/boergmann/tkdApp/security/CustomUserDetailsService.java b/src/main/java/it/boergmann/tkdApp/security/CustomUserDetailsService.java index c6120a2..892acd9 100644 --- a/src/main/java/it/boergmann/tkdApp/security/CustomUserDetailsService.java +++ b/src/main/java/it/boergmann/tkdApp/security/CustomUserDetailsService.java @@ -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"); } diff --git a/src/main/java/it/boergmann/tkdApp/service/AppUserService.java b/src/main/java/it/boergmann/tkdApp/service/AppUserService.java index e3a82a6..4fde198 100644 --- a/src/main/java/it/boergmann/tkdApp/service/AppUserService.java +++ b/src/main/java/it/boergmann/tkdApp/service/AppUserService.java @@ -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; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f675606..25d63e5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/templates/admin_users.html b/src/main/resources/templates/admin_users.html index 28c5fe0..0f457a6 100644 --- a/src/main/resources/templates/admin_users.html +++ b/src/main/resources/templates/admin_users.html @@ -1,31 +1,36 @@ - - - - Benutzer freischalten - - -

Offene Registrierungen

- - +

Benutzerverwaltung

+
+ - + + + + + + - - - + + + + + + + +
BenutzernameUsername EmailAktuelle RolleNeue RolleGültig bis (nur für ADMIN) Aktion
BenutzerEmail -
+ + + +
+ + - - -
- - diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 0000000..73aabbc --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,12 @@ + + + + + Kein Zugriff + + +

🚫 Zugriff verweigert

+

Du hast leider keine Berechtigung für diese Seite.

+Zur Startseite + + diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index fdc8141..b8d8f10 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -5,7 +5,9 @@

Willkommen, Nutzer!

-

Deine Rollen: []

+

Deine Rollen: +

Logout