Mailservice, passwort reset-Api Passwort reset anfordern web

This commit is contained in:
klaas 2025-11-07 18:47:47 +01:00
parent fd3f82659f
commit af021c7830
14 changed files with 179 additions and 9 deletions

View File

@ -107,6 +107,10 @@
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
<build>

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AppUserRole> appUserRoles = new ArrayList<>();

View File

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

View File

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

View File

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

View File

@ -16,5 +16,8 @@ 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> findByResetToken(String token);
Optional<AppUser> findByEmailVerificationToken(String token);
Optional<AppUser> findByEmail(String email);
}

View File

@ -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 🔐",
"<p>Hier ist dein Link: <a href=\"" + link + "\">Passwort ändern</a></p>",
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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Passwort zurücksetzen</title>
</head>
<body>
<h2>Passwort zurücksetzen</h2>
<form th:action="@{/reset-request}" method="post" th:object="${emailRequest}">
<label for="email">E-Mail:</label>
<input type="email" id="email" th:field="*{email}" required />
<button type="submit">Zurücksetzen anfordern</button>
</form>
<p th:if="${message}" th:text="${message}"></p>
</body>
</html>