diff --git a/backend/src/main/java/de/szut/casino/security/AuthController.java b/backend/src/main/java/de/szut/casino/security/AuthController.java index f833d78..d22de30 100644 --- a/backend/src/main/java/de/szut/casino/security/AuthController.java +++ b/backend/src/main/java/de/szut/casino/security/AuthController.java @@ -4,6 +4,7 @@ import de.szut.casino.exceptionHandling.ErrorDetails; import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException; import de.szut.casino.security.dto.AuthResponseDto; 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.user.dto.CreateUserDto; import de.szut.casino.user.dto.GetUserDto; @@ -43,4 +44,16 @@ public class AuthController { return ResponseEntity.ok().build(); } + + @PostMapping("/recover-password") + public ResponseEntity recoverPassword(@RequestParam("email") String email) throws MessagingException, IOException { + authService.recoverPassword(email); + return ResponseEntity.ok().build(); + } + + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordDto passwordDto) throws MessagingException, IOException { + authService.resetPassword(passwordDto); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/de/szut/casino/security/dto/ResetPasswordDto.java b/backend/src/main/java/de/szut/casino/security/dto/ResetPasswordDto.java new file mode 100644 index 0000000..192d928 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/dto/ResetPasswordDto.java @@ -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; +} diff --git a/backend/src/main/java/de/szut/casino/security/service/AuthService.java b/backend/src/main/java/de/szut/casino/security/service/AuthService.java index f51ff83..959a55a 100644 --- a/backend/src/main/java/de/szut/casino/security/service/AuthService.java +++ b/backend/src/main/java/de/szut/casino/security/service/AuthService.java @@ -3,19 +3,23 @@ package de.szut.casino.security.service; import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException; import de.szut.casino.security.dto.AuthResponseDto; 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.user.UserEntity; import de.szut.casino.user.UserService; import de.szut.casino.user.dto.CreateUserDto; import de.szut.casino.user.dto.GetUserDto; import jakarta.mail.MessagingException; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import javax.swing.text.html.Option; import java.io.IOException; import java.util.Optional; @@ -34,6 +38,9 @@ public class AuthService { @Autowired private EmailService emailService; + @Autowired + private PasswordEncoder passwordEncoder; + public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException { if (!userService.isVerified(loginRequest.getUsernameOrEmail())) { throw new EmailNotVerifiedException(); @@ -46,7 +53,7 @@ public class AuthService { SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtils.generateToken(authentication); - + return new AuthResponseDto(jwt); } @@ -66,7 +73,7 @@ public class AuthService { public Boolean verifyEmail(String token) throws MessagingException, IOException { Optional optionalUser = userService.getUserByVerificationToken(token); - if(!optionalUser.isPresent()) { + if (!optionalUser.isPresent()) { return false; } @@ -79,4 +86,26 @@ public class AuthService { return true; } + + public void recoverPassword(String email) throws MessagingException, IOException { + Optional 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 optionalUser = userService.getUserByPasswordResetToken(passwordDto.getToken()); + + if (optionalUser.isPresent()) { + UserEntity user = optionalUser.get(); + user.setPassword(passwordEncoder.encode(passwordDto.getPassword())); + user.setPasswordResetToken(null); + userService.saveUser(user); + } + } } diff --git a/backend/src/main/java/de/szut/casino/security/service/EmailService.java b/backend/src/main/java/de/szut/casino/security/service/EmailService.java index 4d83262..f276a3c 100644 --- a/backend/src/main/java/de/szut/casino/security/service/EmailService.java +++ b/backend/src/main/java/de/szut/casino/security/service/EmailService.java @@ -87,6 +87,24 @@ public class EmailService { 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 { ClassPathResource resource = new ClassPathResource("templates/" + templatePath); try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 161ce52..2190867 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -34,6 +34,8 @@ public class UserEntity { private String verificationToken; + private String passwordResetToken; + public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) { this.email = email; this.username = username; diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java index 9eafd54..29790f5 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -21,4 +21,7 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1") Optional findOneByUsernameOrEmail(String usernameOrEmail); + + @Query("SELECT u FROM UserEntity u WHERE u.passwordResetToken = ?1") + Optional findOneByPasswordResetToken(String token); } diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index 9113864..baa2eab 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -62,4 +62,12 @@ public class UserService { return optionalUser.get().getEmailVerified(); } + + public Optional getUserByEmail(String email) { + return userRepository.findByEmail(email); + } + + public Optional getUserByPasswordResetToken(String token) { + return this.userRepository.findOneByPasswordResetToken(token); + } } diff --git a/backend/src/main/resources/templates/email/recover-password.html b/backend/src/main/resources/templates/email/recover-password.html new file mode 100644 index 0000000..cf666d1 --- /dev/null +++ b/backend/src/main/resources/templates/email/recover-password.html @@ -0,0 +1,156 @@ + + + + + + Passwort zurücksetzen - Trustworthy Casino© + + + +
+
+

Trustworthy Casino

+
+
+

Hallo ${username},

+ +

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:

+ + + +
+

Hinweis: Dieser Link und Code sind aus Sicherheitsgründen vielleicht nur 60 Minuten gültig.

+
+ +
+ +
+

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.

+
+ +
+ +

Bei Fragen steht Ihnen unser Support-Team nicht zur Verfügung.

+ +

Mit freundlichen Grüßen,
+ Ihr Trustworthy Casino Team

+
+ +
+ + \ No newline at end of file diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 5c57416..b4bf818 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -19,6 +19,20 @@ export const routes: Routes = [ (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', loadComponent: () => import('./feature/game/blackjack/blackjack.component'), diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html index 5f4c535..04afd42 100644 --- a/frontend/src/app/feature/auth/login/login.component.html +++ b/frontend/src/app/feature/auth/login/login.component.html @@ -83,6 +83,18 @@ +
+

+ Passwort vergessen? + +

+
+

Noch kein Konto? diff --git a/frontend/src/app/feature/auth/login/login.component.ts b/frontend/src/app/feature/auth/login/login.component.ts index b5a67a0..946f412 100644 --- a/frontend/src/app/feature/auth/login/login.component.ts +++ b/frontend/src/app/feature/auth/login/login.component.ts @@ -63,4 +63,9 @@ export class LoginComponent { }, }); } + + switchToForgotPassword() { + this.closeDialog.emit(); + this.router.navigate(['/recover-password']); + } } diff --git a/frontend/src/app/feature/auth/recover-password/recover-password.component.html b/frontend/src/app/feature/auth/recover-password/recover-password.component.html new file mode 100644 index 0000000..8dccc09 --- /dev/null +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.html @@ -0,0 +1,172 @@ +

+ +
diff --git a/frontend/src/app/feature/auth/recover-password/recover-password.component.ts b/frontend/src/app/feature/auth/recover-password/recover-password.component.ts new file mode 100644 index 0000000..d1ad253 --- /dev/null +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.ts @@ -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(); + + 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.' + ); + }, + }); + } +} diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 657067f..f1cfbe9 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -78,6 +78,14 @@ export class AuthService { return this.http.post(`${this.authUrl}/verify?token=${token}`, null); } + public recoverPassword(email: string): Observable { + return this.http.post(`${this.authUrl}/recover-password?email=${email}`, null); + } + + public resetPassword(token: string, password: string): Observable { + return this.http.post(`${this.authUrl}/reset-password`, { token, password }); + } + private setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); }