Merge pull request 'feat: add password recovery (CAS-73)' (!194) from feat/password-recovery into main
Reviewed-on: #194 Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
This commit is contained in:
commit
7d471b6898
14 changed files with 582 additions and 2 deletions
|
@ -4,6 +4,7 @@ import de.szut.casino.exceptionHandling.ErrorDetails;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
import de.szut.casino.security.dto.AuthResponseDto;
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
import de.szut.casino.security.dto.LoginRequestDto;
|
||||||
|
import de.szut.casino.security.dto.ResetPasswordDto;
|
||||||
import de.szut.casino.security.service.AuthService;
|
import de.szut.casino.security.service.AuthService;
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import de.szut.casino.user.dto.CreateUserDto;
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
import de.szut.casino.user.dto.GetUserDto;
|
||||||
|
@ -43,4 +44,16 @@ public class AuthController {
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/recover-password")
|
||||||
|
public ResponseEntity<Void> recoverPassword(@RequestParam("email") String email) throws MessagingException, IOException {
|
||||||
|
authService.recoverPassword(email);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reset-password")
|
||||||
|
public ResponseEntity<Void> resetPassword(@Valid @RequestBody ResetPasswordDto passwordDto) throws MessagingException, IOException {
|
||||||
|
authService.resetPassword(passwordDto);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package de.szut.casino.security.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ResetPasswordDto {
|
||||||
|
private String token;
|
||||||
|
private String password;
|
||||||
|
}
|
|
@ -3,19 +3,23 @@ package de.szut.casino.security.service;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException;
|
||||||
import de.szut.casino.security.dto.AuthResponseDto;
|
import de.szut.casino.security.dto.AuthResponseDto;
|
||||||
import de.szut.casino.security.dto.LoginRequestDto;
|
import de.szut.casino.security.dto.LoginRequestDto;
|
||||||
|
import de.szut.casino.security.dto.ResetPasswordDto;
|
||||||
import de.szut.casino.security.jwt.JwtUtils;
|
import de.szut.casino.security.jwt.JwtUtils;
|
||||||
import de.szut.casino.user.UserEntity;
|
import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import de.szut.casino.user.dto.CreateUserDto;
|
import de.szut.casino.user.dto.CreateUserDto;
|
||||||
import de.szut.casino.user.dto.GetUserDto;
|
import de.szut.casino.user.dto.GetUserDto;
|
||||||
import jakarta.mail.MessagingException;
|
import jakarta.mail.MessagingException;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.swing.text.html.Option;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -34,6 +38,9 @@ public class AuthService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private EmailService emailService;
|
private EmailService emailService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException {
|
||||||
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
|
if (!userService.isVerified(loginRequest.getUsernameOrEmail())) {
|
||||||
throw new EmailNotVerifiedException();
|
throw new EmailNotVerifiedException();
|
||||||
|
@ -46,7 +53,7 @@ public class AuthService {
|
||||||
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
String jwt = jwtUtils.generateToken(authentication);
|
String jwt = jwtUtils.generateToken(authentication);
|
||||||
|
|
||||||
return new AuthResponseDto(jwt);
|
return new AuthResponseDto(jwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +73,7 @@ public class AuthService {
|
||||||
public Boolean verifyEmail(String token) throws MessagingException, IOException {
|
public Boolean verifyEmail(String token) throws MessagingException, IOException {
|
||||||
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
Optional<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
||||||
|
|
||||||
if(!optionalUser.isPresent()) {
|
if (!optionalUser.isPresent()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,4 +86,26 @@ public class AuthService {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void recoverPassword(String email) throws MessagingException, IOException {
|
||||||
|
Optional<UserEntity> optionalUser = userService.getUserByEmail(email);
|
||||||
|
|
||||||
|
if (optionalUser.isPresent()) {
|
||||||
|
UserEntity user = optionalUser.get();
|
||||||
|
user.setPasswordResetToken(RandomStringUtils.randomAlphanumeric(64));
|
||||||
|
userService.saveUser(user);
|
||||||
|
this.emailService.sendPasswordRecoveryEmail(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPassword(ResetPasswordDto passwordDto) {
|
||||||
|
Optional<UserEntity> optionalUser = userService.getUserByPasswordResetToken(passwordDto.getToken());
|
||||||
|
|
||||||
|
if (optionalUser.isPresent()) {
|
||||||
|
UserEntity user = optionalUser.get();
|
||||||
|
user.setPassword(passwordEncoder.encode(passwordDto.getPassword()));
|
||||||
|
user.setPasswordResetToken(null);
|
||||||
|
userService.saveUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,24 @@ public class EmailService {
|
||||||
mailSender.send(message);
|
mailSender.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendPasswordRecoveryEmail(UserEntity user) throws IOException, MessagingException {
|
||||||
|
String template = loadTemplate("email/recover-password.html");
|
||||||
|
String htmlContent = template
|
||||||
|
.replace("${username}", user.getUsername())
|
||||||
|
.replace("${resetToken}", user.getPasswordResetToken())
|
||||||
|
.replace("${feUrl}", feUrl);
|
||||||
|
|
||||||
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
|
||||||
|
helper.setFrom(mailConfig.fromAddress);
|
||||||
|
helper.setTo(user.getEmailAddress());
|
||||||
|
helper.setSubject("Zurücksetzen ihres Passworts");
|
||||||
|
helper.setText(htmlContent, true);
|
||||||
|
|
||||||
|
mailSender.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
private String loadTemplate(String templatePath) throws IOException {
|
private String loadTemplate(String templatePath) throws IOException {
|
||||||
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
|
ClassPathResource resource = new ClassPathResource("templates/" + templatePath);
|
||||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
||||||
|
|
|
@ -34,6 +34,8 @@ public class UserEntity {
|
||||||
|
|
||||||
private String verificationToken;
|
private String verificationToken;
|
||||||
|
|
||||||
|
private String passwordResetToken;
|
||||||
|
|
||||||
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
|
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
|
|
@ -21,4 +21,7 @@ public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||||
|
|
||||||
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
|
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
|
||||||
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
|
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
|
||||||
|
|
||||||
|
@Query("SELECT u FROM UserEntity u WHERE u.passwordResetToken = ?1")
|
||||||
|
Optional<UserEntity> findOneByPasswordResetToken(String token);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,4 +62,12 @@ public class UserService {
|
||||||
|
|
||||||
return optionalUser.get().getEmailVerified();
|
return optionalUser.get().getEmailVerified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getUserByEmail(String email) {
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getUserByPasswordResetToken(String token) {
|
||||||
|
return this.userRepository.findOneByPasswordResetToken(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
156
backend/src/main/resources/templates/email/recover-password.html
Normal file
156
backend/src/main/resources/templates/email/recover-password.html
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Passwort zurücksetzen - Trustworthy Casino©</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: #0a1219;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #1a2835;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background-color: #121e27;
|
||||||
|
padding: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background-color: #1a2835;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #ffffff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
li::marker {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #1a2835;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.recovery-code {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.security-info {
|
||||||
|
background-color: #1a2835;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Trustworthy Casino</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Hallo <span class="highlight">${username}</span>,</h2>
|
||||||
|
|
||||||
|
<p>wir haben eine Anfrage zum Zurücksetzen Ihres Passworts für Ihr Trustworthy Casino Konto erhalten. Um Ihr Passwort zurückzusetzen, klicken Sie bitte auf den folgenden Button:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="${feUrl}/reset-password?token=${resetToken}" class="button">Passwort zurücksetzen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><span class="warning">Hinweis:</span> Dieser Link und Code sind aus Sicherheitsgründen vielleicht nur <span class="highlight">60 Minuten</span> gültig.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="security-info">
|
||||||
|
<p>Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail bitte. In diesem Fall empfehlen wir Ihnen, Ihr Passwort zu ändern und unseren Kundenservice zu kontaktieren, um die Sicherheit Ihres Kontos zu gewährleisten.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<p>Bei Fragen steht Ihnen unser Support-Team nicht zur Verfügung.</p>
|
||||||
|
|
||||||
|
<p>Mit freundlichen Grüßen,<br>
|
||||||
|
Ihr <span style="color: #10b981;">Trustworthy Casino</span> Team</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>2025 Trustworthy Casino - Keine Rechte vorbehalten</p>
|
||||||
|
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||||
|
<p>Für Fragen zur Sicherheit Ihres Kontos kontaktieren Sie uns bitte nicht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -19,6 +19,20 @@ export const routes: Routes = [
|
||||||
(m) => m.VerifyEmailComponent
|
(m) => m.VerifyEmailComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'recover-password',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./feature/auth/recover-password/recover-password.component').then(
|
||||||
|
(m) => m.RecoverPasswordComponent
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./feature/auth/recover-password/recover-password.component').then(
|
||||||
|
(m) => m.RecoverPasswordComponent
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'game/blackjack',
|
path: 'game/blackjack',
|
||||||
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),
|
||||||
|
|
|
@ -83,6 +83,18 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<p class="text-sm text-text-secondary">
|
||||||
|
Passwort vergessen?
|
||||||
|
<button
|
||||||
|
(click)="switchToForgotPassword()"
|
||||||
|
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
|
||||||
|
>
|
||||||
|
Passwort zurücksetzen
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
<div class="mt-6 text-center">
|
||||||
<p class="text-sm text-text-secondary">
|
<p class="text-sm text-text-secondary">
|
||||||
Noch kein Konto?
|
Noch kein Konto?
|
||||||
|
|
|
@ -63,4 +63,9 @@ export class LoginComponent {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchToForgotPassword() {
|
||||||
|
this.closeDialog.emit();
|
||||||
|
this.router.navigate(['/recover-password']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
<div class="min-h-screen bg-deep-blue flex items-center justify-center">
|
||||||
|
<div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative">
|
||||||
|
<button
|
||||||
|
(click)="closeDialog.emit()"
|
||||||
|
class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors"
|
||||||
|
aria-label="Dialog schließen"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 class="modal-heading text-center">
|
||||||
|
@if (isResetMode()) {
|
||||||
|
Passwort zurücksetzen
|
||||||
|
} @else {
|
||||||
|
Passwort vergessen
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<div class="bg-accent-red text-white p-4 rounded mb-4">
|
||||||
|
{{ errorMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (successMessage()) {
|
||||||
|
<div class="bg-emerald text-white p-4 rounded mb-4">
|
||||||
|
{{ successMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!isResetMode()) {
|
||||||
|
<!-- Request Password Reset Form -->
|
||||||
|
<form [formGroup]="emailForm" (ngSubmit)="onSubmitEmail()" class="space-y-4">
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-text-secondary text-sm mb-4">
|
||||||
|
Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines
|
||||||
|
Passworts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||||
|
E-Mail
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
formControlName="email"
|
||||||
|
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||||
|
placeholder="Gib deine E-Mail-Adresse ein"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (emailFormControls['email'].touched && emailFormControls['email'].errors) {
|
||||||
|
<div class="text-accent-red mt-1 text-sm">
|
||||||
|
@if (emailFormControls['email'].errors['required']) {
|
||||||
|
<span>E-Mail ist erforderlich</span>
|
||||||
|
}
|
||||||
|
@if (emailFormControls['email'].errors['email']) {
|
||||||
|
<span>Bitte gib eine gültige E-Mail-Adresse ein</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="emailForm.invalid || isLoading()"
|
||||||
|
class="button-primary w-full py-2.5 rounded"
|
||||||
|
>
|
||||||
|
{{ isLoading() ? 'Wird gesendet...' : 'Link zum Zurücksetzen senden' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isResetMode()) {
|
||||||
|
<!-- Reset Password Form -->
|
||||||
|
<form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmitReset()" class="space-y-4">
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-text-secondary text-sm mb-4">Gib dein neues Passwort ein.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||||
|
Neues Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
formControlName="password"
|
||||||
|
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||||
|
placeholder="Gib dein neues Passwort ein"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (resetFormControls['password'].touched && resetFormControls['password'].errors) {
|
||||||
|
<div class="text-accent-red mt-1 text-sm">
|
||||||
|
@if (resetFormControls['password'].errors['required']) {
|
||||||
|
<span>Passwort ist erforderlich</span>
|
||||||
|
}
|
||||||
|
@if (resetFormControls['password'].errors['minlength']) {
|
||||||
|
<span>Passwort muss mindestens 8 Zeichen lang sein</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirmPassword" class="text-text-secondary text-sm font-medium mb-1 block">
|
||||||
|
Passwort bestätigen
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
formControlName="confirmPassword"
|
||||||
|
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
|
||||||
|
placeholder="Bestätige dein neues Passwort"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (
|
||||||
|
resetFormControls['confirmPassword'].touched &&
|
||||||
|
(resetFormControls['confirmPassword'].errors ||
|
||||||
|
resetPasswordForm.errors?.['passwordMismatch'])
|
||||||
|
) {
|
||||||
|
<div class="text-accent-red mt-1 text-sm">
|
||||||
|
@if (resetFormControls['confirmPassword'].errors?.['required']) {
|
||||||
|
<span>Passwortbestätigung ist erforderlich</span>
|
||||||
|
}
|
||||||
|
@if (resetPasswordForm.errors?.['passwordMismatch']) {
|
||||||
|
<span>Passwörter stimmen nicht überein</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="resetPasswordForm.invalid || isLoading()"
|
||||||
|
class="button-primary w-full py-2.5 rounded"
|
||||||
|
>
|
||||||
|
{{ isLoading() ? 'Wird aktualisiert...' : 'Passwort aktualisieren' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<p class="text-sm text-text-secondary">
|
||||||
|
<a
|
||||||
|
routerLink="/"
|
||||||
|
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
|
||||||
|
>
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '@service/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-recover-password',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
|
templateUrl: './recover-password.component.html',
|
||||||
|
})
|
||||||
|
export class RecoverPasswordComponent {
|
||||||
|
emailForm: FormGroup;
|
||||||
|
resetPasswordForm: FormGroup;
|
||||||
|
errorMessage = signal('');
|
||||||
|
successMessage = signal('');
|
||||||
|
isLoading = signal(false);
|
||||||
|
token = '';
|
||||||
|
isResetMode = signal(false);
|
||||||
|
|
||||||
|
@Output() closeDialog = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
this.emailForm = this.fb.group({
|
||||||
|
email: ['', [Validators.required, Validators.email]],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetPasswordForm = this.fb.group(
|
||||||
|
{
|
||||||
|
password: ['', [Validators.required, Validators.minLength(8)]],
|
||||||
|
confirmPassword: ['', [Validators.required]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validators: this.passwordMatchValidator,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if we're in reset mode
|
||||||
|
this.route.queryParamMap.subscribe((params) => {
|
||||||
|
const token = params.get('token');
|
||||||
|
if (token) {
|
||||||
|
this.token = token;
|
||||||
|
this.isResetMode.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordMatchValidator(form: FormGroup) {
|
||||||
|
const password = form.get('password')?.value;
|
||||||
|
const confirmPassword = form.get('confirmPassword')?.value;
|
||||||
|
return password === confirmPassword ? null : { passwordMismatch: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
get emailFormControls() {
|
||||||
|
return this.emailForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
get resetFormControls() {
|
||||||
|
return this.resetPasswordForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEmail(): void {
|
||||||
|
if (this.emailForm.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading.set(true);
|
||||||
|
this.errorMessage.set('');
|
||||||
|
this.successMessage.set('');
|
||||||
|
|
||||||
|
const email = this.emailFormControls['email'].value;
|
||||||
|
|
||||||
|
this.authService.recoverPassword(email).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
this.successMessage.set(
|
||||||
|
'Wenn ein Konto mit dieser E-Mail existiert, wird eine E-Mail mit weiteren Anweisungen gesendet.'
|
||||||
|
);
|
||||||
|
this.emailForm.reset();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
this.errorMessage.set(
|
||||||
|
err.error?.message || 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitReset(): void {
|
||||||
|
if (this.resetPasswordForm.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading.set(true);
|
||||||
|
this.errorMessage.set('');
|
||||||
|
this.successMessage.set('');
|
||||||
|
|
||||||
|
const password = this.resetFormControls['password'].value;
|
||||||
|
|
||||||
|
this.authService.resetPassword(this.token, password).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
this.successMessage.set(
|
||||||
|
'Dein Passwort wurde erfolgreich zurückgesetzt. Du kannst dich jetzt anmelden.'
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.router.navigate([''], { queryParams: { login: true } });
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
this.errorMessage.set(
|
||||||
|
err.error?.message || 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,14 @@ export class AuthService {
|
||||||
return this.http.post<unknown>(`${this.authUrl}/verify?token=${token}`, null);
|
return this.http.post<unknown>(`${this.authUrl}/verify?token=${token}`, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public recoverPassword(email: string): Observable<unknown> {
|
||||||
|
return this.http.post<unknown>(`${this.authUrl}/recover-password?email=${email}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetPassword(token: string, password: string): Observable<unknown> {
|
||||||
|
return this.http.post<unknown>(`${this.authUrl}/reset-password`, { token, password });
|
||||||
|
}
|
||||||
|
|
||||||
private setToken(token: string): void {
|
private setToken(token: string): void {
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue