diff --git a/pom.xml b/pom.xml index 3bcb755..91bc4d7 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,10 @@ hibernate-validator 8.0.1.Final + + org.springframework.boot + spring-boot-starter-mail + diff --git a/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java b/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java index 260e613..54ac09d 100644 --- a/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java +++ b/src/main/java/it/boergmann/tkdApp/config/SecurityConfig.java @@ -46,7 +46,7 @@ public class SecurityConfig { .csrf(AbstractHttpConfigurer::disable) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz - .requestMatchers("/api/login", "/api/register").permitAll() + .requestMatchers("/api/login", "/api/register", "/api/request", "api/confirm").permitAll() .anyRequest().authenticated() ) .exceptionHandling(ex -> ex @@ -63,7 +63,7 @@ public class SecurityConfig { public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz - .requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll() + .requestMatchers("/login", "/register", "/css/**", "/js/**", "/reset-request").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form diff --git a/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java b/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java index 29768c3..f06454c 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/api/UserApiController.java @@ -2,16 +2,19 @@ package it.boergmann.tkdApp.controller.api; import it.boergmann.tkdApp.dto.AppUserResponse; import it.boergmann.tkdApp.domain.AppUser; +import it.boergmann.tkdApp.dto.PasswordResetRequest; import it.boergmann.tkdApp.dto.RegisterRequest; +import it.boergmann.tkdApp.dto.PasswordConfirmRequest; 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.*; +import java.util.Map; + @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -36,4 +39,16 @@ public class UserApiController { appUserService.registerNewUser(request); return ResponseEntity.ok().build(); } + + @PostMapping("/request") + public ResponseEntity requestReset(@RequestBody PasswordResetRequest request) { + appUserService.requestPasswordReset(request.getEmail()); + return ResponseEntity.ok(Map.of("message", "Falls die E-Mail existiert, wurde ein Link gesendet")); + } + + @PostMapping("/confirm") + public ResponseEntity resetPassword(@RequestBody PasswordConfirmRequest request) { + appUserService.confirmPasswordReset(request); + return ResponseEntity.ok(Map.of("message", "Passwort wurde geändert")); + } } diff --git a/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java b/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java index 7444716..6aa62a7 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/web/UserController.java @@ -1,10 +1,12 @@ package it.boergmann.tkdApp.controller.web; +import it.boergmann.tkdApp.dto.PasswordResetRequest; 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 it.boergmann.tkdApp.service.MailService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -35,4 +37,24 @@ public class UserController { model.addAttribute("message", "Registrierung erfolgreich! Bitte auf Freischaltung durch Admin warten."); return "login"; } + + @GetMapping("/login") + public String loginPage() { + return "login"; // src/main/resources/templates/login.html + } + + private final MailService mailService; // oder dein eigener Service + + @GetMapping("/reset-request") + public String showRequestForm(Model model) { + model.addAttribute("emailRequest", new PasswordResetRequest()); + return "password_reset_request"; + } + + @PostMapping("/reset-request") + public String handleRequest(@ModelAttribute PasswordResetRequest request, Model model) { + appUserService.requestPasswordReset(request.getEmail()); + model.addAttribute("message", "Wenn ein Konto existiert, wurde eine Mail gesendet."); + return "password_reset_request"; + } } 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 307e4fd..b490821 100644 --- a/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java +++ b/src/main/java/it/boergmann/tkdApp/controller/web/WebController.java @@ -15,11 +15,6 @@ public class WebController { private final AppUserService appUserService; - @GetMapping("/login") - public String loginPage() { - return "login"; // src/main/resources/templates/login.html - } - @GetMapping("/me") public String profilePage(Model model) { AppUser user = appUserService.getCurrentUser(); diff --git a/src/main/java/it/boergmann/tkdApp/domain/AppUser.java b/src/main/java/it/boergmann/tkdApp/domain/AppUser.java index adf5e4c..82e1d28 100644 --- a/src/main/java/it/boergmann/tkdApp/domain/AppUser.java +++ b/src/main/java/it/boergmann/tkdApp/domain/AppUser.java @@ -29,6 +29,12 @@ public class AppUser implements UserDetails{ private String password; + private String resetToken; + private LocalDateTime resetExpires; + + private String emailVerificationToken; + private LocalDateTime emailVerifiedAt; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) @Builder.Default private List appUserRoles = new ArrayList<>(); diff --git a/src/main/java/it/boergmann/tkdApp/dto/PasswordConfirmRequest.java b/src/main/java/it/boergmann/tkdApp/dto/PasswordConfirmRequest.java new file mode 100644 index 0000000..a0e8eef --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/dto/PasswordConfirmRequest.java @@ -0,0 +1,12 @@ +package it.boergmann.tkdApp.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class PasswordConfirmRequest { + @NotBlank(message = "Token darf nicht leer sein") + private String token; + @NotBlank(message = "Passwort darf nicht leer sein") + private String newPassword; +} diff --git a/src/main/java/it/boergmann/tkdApp/dto/PasswordResetRequest.java b/src/main/java/it/boergmann/tkdApp/dto/PasswordResetRequest.java new file mode 100644 index 0000000..ca505e0 --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/dto/PasswordResetRequest.java @@ -0,0 +1,12 @@ +package it.boergmann.tkdApp.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class PasswordResetRequest { + @Email + @NotBlank + 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 index 8a6a4e8..5a80bd8 100644 --- a/src/main/java/it/boergmann/tkdApp/init/StartupAdminInitializer.java +++ b/src/main/java/it/boergmann/tkdApp/init/StartupAdminInitializer.java @@ -5,6 +5,7 @@ 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 it.boergmann.tkdApp.service.MailService; import lombok.RequiredArgsConstructor; import org.springframework.boot.ApplicationRunner; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java b/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java index b489271..2b80024 100644 --- a/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java +++ b/src/main/java/it/boergmann/tkdApp/repository/AppUserRepository.java @@ -16,5 +16,8 @@ public interface AppUserRepository extends JpaRepository { boolean existsByUsername(String username); // 🔥 hier hinzufügen! boolean existsByEmail(String email); Optional findByUsername(String username); + Optional findByResetToken(String token); + Optional findByEmailVerificationToken(String token); + Optional findByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/it/boergmann/tkdApp/service/AppUserService.java b/src/main/java/it/boergmann/tkdApp/service/AppUserService.java index 4fde198..9114535 100644 --- a/src/main/java/it/boergmann/tkdApp/service/AppUserService.java +++ b/src/main/java/it/boergmann/tkdApp/service/AppUserService.java @@ -4,6 +4,7 @@ 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.dto.PasswordConfirmRequest; import it.boergmann.tkdApp.repository.AppUserRepository; import it.boergmann.tkdApp.repository.AppUserRoleRepository; import lombok.RequiredArgsConstructor; @@ -22,6 +23,7 @@ public class AppUserService { private final AppUserRepository userRepository; private final PasswordEncoder passwordEncoder; private final AppUserRoleRepository roleAssignmentRepository; + private final MailService mailService; public AppUser getCurrentUser() { String username = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -77,4 +79,39 @@ public class AppUserService { return savedUser; } + + public void requestPasswordReset(String email) { + AppUser user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("E-Mail nicht gefunden")); + + user.setResetToken(UUID.randomUUID().toString()); + user.setResetExpires(LocalDateTime.now().plusHours(1)); + userRepository.save(user); + + String link = "https://tkdapp.de/reset-password?token=" + user.getResetToken(); + + + mailService.sendMail( + email, + "Passwort zurücksetzen 🔐", + "

Hier ist dein Link: Passwort ändern

", + true + ); + } + + public void confirmPasswordReset(PasswordConfirmRequest request) { + String token = request.getToken(); + String newPassword = request.getNewPassword(); + AppUser user = userRepository.findByResetToken(token) + .orElseThrow(() -> new IllegalArgumentException("Token ungültig")); + + if (user.getResetExpires().isBefore(LocalDateTime.now())) { + throw new IllegalArgumentException("Token abgelaufen"); + } + + user.setPassword(passwordEncoder.encode(newPassword)); + user.setResetToken(null); // ✅ mark as used + user.setResetExpires(null); + userRepository.save(user); + } } diff --git a/src/main/java/it/boergmann/tkdApp/service/MailService.java b/src/main/java/it/boergmann/tkdApp/service/MailService.java new file mode 100644 index 0000000..923dd1d --- /dev/null +++ b/src/main/java/it/boergmann/tkdApp/service/MailService.java @@ -0,0 +1,34 @@ +package it.boergmann.tkdApp.service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MailService { + + private final JavaMailSender mailSender; + + public void sendMail(String to, String subject, String text, boolean html) { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, "utf-8"); + + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, html); + helper.setFrom("admin@boergmann.it"); // anpassen + + mailSender.send(message); + log.info("E-Mail gesendet an {}", to); + } catch (MessagingException e) { + log.error("Fehler beim Senden der E-Mail: {}", e.getMessage()); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 25d63e5..d444b1b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,19 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect open-in-view: false - + mail: + host: mail.boergmann.it + port: 587 + username: klaas@boergmann.it + password: October7-Obsessed-Gumdrop-Remote + protocol: smtp + properties: + mail: + smtp: + auth: true + starttls: + enable: true + default-encoding: UTF-8 jwt: secret: 2T5tOkcLVT8vKfi0qyS0HxYnI2DklK9Mr0BHWWQgsjaAtO3aX5QhVi93h7jVPYiY expiration-ms: 86400000 # 1 Tag \ No newline at end of file diff --git a/src/main/resources/templates/password_reset_request.html b/src/main/resources/templates/password_reset_request.html new file mode 100644 index 0000000..4977b67 --- /dev/null +++ b/src/main/resources/templates/password_reset_request.html @@ -0,0 +1,17 @@ + + + + Passwort zurücksetzen + + +

Passwort zurücksetzen

+ +
+ + + +
+ +

+ +