From 9827f81230b17c5cee598eb51f7624536edfd19e Mon Sep 17 00:00:00 2001 From: csimonis Date: Thu, 15 May 2025 11:19:46 +0200 Subject: [PATCH 1/4] wip: stuff --- .../szut/casino/security/AuthController.java | 6 + .../casino/security/service/AuthService.java | 17 +- .../casino/security/service/EmailService.java | 17 ++ .../java/de/szut/casino/user/UserService.java | 4 + .../templates/email/recover-password.html | 160 ++++++++++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/resources/templates/email/recover-password.html 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..5d8717e 100644 --- a/backend/src/main/java/de/szut/casino/security/AuthController.java +++ b/backend/src/main/java/de/szut/casino/security/AuthController.java @@ -43,4 +43,10 @@ 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(); + } } 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..380b5e4 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 @@ -16,6 +16,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import javax.swing.text.html.Option; import java.io.IOException; import java.util.Optional; @@ -46,7 +47,7 @@ public class AuthService { SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtils.generateToken(authentication); - + return new AuthResponseDto(jwt); } @@ -66,7 +67,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 +80,16 @@ public class AuthService { return true; } + + public void recoverPassword(String email) { + Optional optionalUser = userService.getUserByEmail(email); + + if (optionalUser.isPresent()) { + UserEntity user = optionalUser.get(); + String token = jwtUtils.generateToken(user.getUsername()); + user.setVerificationToken(token); + userService.saveUser(user); + this.emailService.sendPasswordRecoveryEmail(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..b7a05b9 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,23 @@ 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("${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/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index 9113864..a04b02e 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,8 @@ public class UserService { return optionalUser.get().getEmailVerified(); } + + public Optional getUserByEmail(String email) { + return userRepository.findByEmail(email); + } } 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..6d4e1a1 --- /dev/null +++ b/backend/src/main/resources/templates/email/recover-password.html @@ -0,0 +1,160 @@ + + + + + + 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:

+ + + +

Alternativ können Sie auch diesen Code verwenden:

+ +
${resetCode}
+ +
+

Hinweis: Dieser Link und Code sind aus Sicherheitsgründen 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 jederzeit zur Verfügung.

+ +

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

+
+ +
+ + \ No newline at end of file From c8f2d16f076d04d64fbb8cbc2761a10bb1153d5e Mon Sep 17 00:00:00 2001 From: csimonis Date: Thu, 15 May 2025 12:15:33 +0200 Subject: [PATCH 2/4] feat(auth): add password reset functionality and DTO --- .../szut/casino/security/AuthController.java | 7 ++++++ .../casino/security/dto/ResetPasswordDto.java | 15 +++++++++++++ .../casino/security/service/AuthService.java | 22 ++++++++++++++++--- .../casino/security/service/EmailService.java | 1 + .../java/de/szut/casino/user/UserEntity.java | 2 ++ .../de/szut/casino/user/UserRepository.java | 3 +++ .../java/de/szut/casino/user/UserService.java | 4 ++++ .../templates/email/recover-password.html | 12 ++++------ 8 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 backend/src/main/java/de/szut/casino/security/dto/ResetPasswordDto.java 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 5d8717e..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; @@ -49,4 +50,10 @@ public class AuthController { 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 380b5e4..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,17 +3,20 @@ 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; @@ -35,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(); @@ -81,15 +87,25 @@ public class AuthService { return true; } - public void recoverPassword(String email) { + public void recoverPassword(String email) throws MessagingException, IOException { Optional optionalUser = userService.getUserByEmail(email); if (optionalUser.isPresent()) { UserEntity user = optionalUser.get(); - String token = jwtUtils.generateToken(user.getUsername()); - user.setVerificationToken(token); + 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 b7a05b9..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 @@ -91,6 +91,7 @@ public class EmailService { 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(); 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 a04b02e..baa2eab 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -66,4 +66,8 @@ public class UserService { 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 index 6d4e1a1..cf666d1 100644 --- a/backend/src/main/resources/templates/email/recover-password.html +++ b/backend/src/main/resources/templates/email/recover-password.html @@ -129,12 +129,8 @@ Passwort zurücksetzen -

Alternativ können Sie auch diesen Code verwenden:

- -
${resetCode}
-
-

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

+

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

@@ -145,15 +141,15 @@
-

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

+

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

Mit freundlichen Grüßen,
Ihr Trustworthy Casino Team

From 2305e836478571679546cf4087b649c3cd63663f Mon Sep 17 00:00:00 2001 From: csimonis Date: Thu, 15 May 2025 12:31:08 +0200 Subject: [PATCH 3/4] feat(auth): add recover and reset password functionality --- frontend/src/app/app.routes.ts | 14 ++ .../feature/auth/login/login.component.html | 12 ++ .../app/feature/auth/login/login.component.ts | 5 + .../recover-password.component.html | 162 ++++++++++++++++++ .../recover-password.component.ts | 121 +++++++++++++ frontend/src/app/service/auth.service.ts | 8 + 6 files changed, 322 insertions(+) create mode 100644 frontend/src/app/feature/auth/recover-password/recover-password.component.html create mode 100644 frontend/src/app/feature/auth/recover-password/recover-password.component.ts 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..dba76b2 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..5879f3d --- /dev/null +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.html @@ -0,0 +1,162 @@ +

+ +
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..fc3a4d9 --- /dev/null +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.ts @@ -0,0 +1,121 @@ +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..b51a4a1 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -77,6 +77,14 @@ export class AuthService { public verifyEmail(token: string): Observable { 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); From d049048206dfbf5b9d95ab954b0957e3f43e491c Mon Sep 17 00:00:00 2001 From: csimonis Date: Thu, 15 May 2025 12:34:28 +0200 Subject: [PATCH 4/4] style: format HTML and TypeScript files for consistency --- .../feature/auth/login/login.component.html | 20 +++++------ .../recover-password.component.html | 28 ++++++++++----- .../recover-password.component.ts | 36 ++++++++++--------- frontend/src/app/service/auth.service.ts | 4 +-- 4 files changed, 51 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html index dba76b2..04afd42 100644 --- a/frontend/src/app/feature/auth/login/login.component.html +++ b/frontend/src/app/feature/auth/login/login.component.html @@ -84,16 +84,16 @@
-

- Passwort vergessen? - -

-
+

+ Passwort vergessen? + +

+

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 index 5879f3d..8dccc09 100644 --- a/frontend/src/app/feature/auth/recover-password/recover-password.component.html +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.html @@ -22,7 +22,11 @@

@if (errorMessage()) { @@ -42,10 +46,11 @@

- Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines Passworts. + Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines + Passworts.

- +