From d2225decc19c942e6d03a9083dc9a15b33b60e3c Mon Sep 17 00:00:00 2001 From: csimonis Date: Thu, 15 May 2025 10:49:24 +0200 Subject: [PATCH] feat(auth): add email verification feature and handler --- .../GlobalExceptionHandler.java | 7 +++++ .../exceptions/EmailNotVerifiedException.java | 9 ++++++ .../szut/casino/security/AuthController.java | 19 +++++++++--- .../casino/security/service/AuthService.java | 24 +++++++++++++- .../de/szut/casino/user/UserRepository.java | 7 +++++ .../java/de/szut/casino/user/UserService.java | 18 +++++++++++ .../resources/templates/email/verify.html | 2 +- frontend/src/app/app.routes.ts | 4 +++ .../auth/register/register.component.ts | 19 ------------ .../verify-email/verify-email.component.css | 0 .../verify-email/verify-email.component.html | 1 + .../verify-email/verify-email.component.ts | 31 +++++++++++++++++++ .../app/feature/landing/landing.component.ts | 6 +++- frontend/src/app/service/auth.service.ts | 4 +++ 14 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java create mode 100644 frontend/src/app/feature/auth/verify-email/verify-email.component.css create mode 100644 frontend/src/app/feature/auth/verify-email/verify-email.component.html create mode 100644 frontend/src/app/feature/auth/verify-email/verify-email.component.ts diff --git a/backend/src/main/java/de/szut/casino/exceptionHandling/GlobalExceptionHandler.java b/backend/src/main/java/de/szut/casino/exceptionHandling/GlobalExceptionHandler.java index 573abb8..65b3b4b 100644 --- a/backend/src/main/java/de/szut/casino/exceptionHandling/GlobalExceptionHandler.java +++ b/backend/src/main/java/de/szut/casino/exceptionHandling/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package de.szut.casino.exceptionHandling; +import de.szut.casino.exceptionHandling.exceptions.EmailNotVerifiedException; import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException; import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; import jakarta.persistence.EntityExistsException; @@ -31,4 +32,10 @@ public class GlobalExceptionHandler { ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); return new ResponseEntity<>(errorDetails, HttpStatus.CONFLICT); } + + @ExceptionHandler(EmailNotVerifiedException.class) + public ResponseEntity handleEmailNotVerifiedException(EmailNotVerifiedException ex, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); + } } diff --git a/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java new file mode 100644 index 0000000..ea08367 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/exceptionHandling/exceptions/EmailNotVerifiedException.java @@ -0,0 +1,9 @@ +package de.szut.casino.exceptionHandling.exceptions; + +import de.szut.casino.security.service.EmailService; + +public class EmailNotVerifiedException extends Exception { + public EmailNotVerifiedException() { + super("Email not verified"); + } +} 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 6d99625..7862ae7 100644 --- a/backend/src/main/java/de/szut/casino/security/AuthController.java +++ b/backend/src/main/java/de/szut/casino/security/AuthController.java @@ -1,5 +1,7 @@ package de.szut.casino.security; +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.service.AuthService; @@ -9,12 +11,10 @@ import jakarta.mail.MessagingException; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.util.Date; @RestController @RequestMapping("/auth") @@ -24,7 +24,7 @@ public class AuthController { private AuthService authService; @PostMapping("/login") - public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) { + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) throws EmailNotVerifiedException { AuthResponseDto response = authService.login(loginRequest); return ResponseEntity.ok(response); } @@ -34,4 +34,13 @@ public class AuthController { GetUserDto response = authService.register(signUpRequest); return ResponseEntity.ok(response); } + + @PostMapping("/verify") + public ResponseEntity verifyEmail(@RequestParam("token") String token) { + if (authService.verifyEmail(token)) { + return ResponseEntity.badRequest().build(); + } + + 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 c4d324d..714b7f3 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 @@ -1,5 +1,6 @@ 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.jwt.JwtUtils; @@ -16,6 +17,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.io.IOException; +import java.util.Optional; @Service public class AuthService { @@ -32,7 +34,11 @@ public class AuthService { @Autowired private EmailService emailService; - public AuthResponseDto login(LoginRequestDto loginRequest) { + public AuthResponseDto login(LoginRequestDto loginRequest) throws EmailNotVerifiedException { + if (!userService.isVerified(loginRequest.getUsernameOrEmail())) { + throw new EmailNotVerifiedException(); + } + Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsernameOrEmail(), @@ -56,4 +62,20 @@ public class AuthService { user.getBalance() ); } + + public Boolean verifyEmail(String token) { + Optional optionalUser = userService.getUserByVerificationToken(token); + + if(!optionalUser.isPresent()) { + return false; + } + + UserEntity user = optionalUser.get(); + + user.setEmailVerified(true); + user.setVerificationToken(null); + userService.saveUser(user); + + return true; + } } 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 863e744..9eafd54 100644 --- a/backend/src/main/java/de/szut/casino/user/UserRepository.java +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -1,6 +1,7 @@ package de.szut.casino.user; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Service; import java.util.Optional; @@ -14,4 +15,10 @@ public interface UserRepository extends JpaRepository { boolean existsByUsername(String username); boolean existsByEmail(String email); + + @Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1") + Optional findOneByVerificationToken(String token); + + @Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1") + Optional findOneByUsernameOrEmail(String usernameOrEmail); } 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 6855619..9113864 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -44,4 +44,22 @@ public class UserService { return userRepository.findByUsername(username); } + + public Optional getUserByVerificationToken(String token) { + return this.userRepository.findOneByVerificationToken(token); + } + + public void saveUser(UserEntity user) { + userRepository.save(user); + } + + public boolean isVerified(String usernameOrEmail) { + Optional optionalUser = userRepository.findOneByUsernameOrEmail(usernameOrEmail); + + if (!optionalUser.isPresent()) { + return false; + } + + return optionalUser.get().getEmailVerified(); + } } diff --git a/backend/src/main/resources/templates/email/verify.html b/backend/src/main/resources/templates/email/verify.html index 008236d..b7dc2a6 100644 --- a/backend/src/main/resources/templates/email/verify.html +++ b/backend/src/main/resources/templates/email/verify.html @@ -123,7 +123,7 @@

Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:

diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 3792038..48c1e8e 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -12,6 +12,10 @@ export const routes: Routes = [ loadComponent: () => import('./feature/home/home.component'), canActivate: [authGuard], }, + { + path: 'verify', + loadComponent: () => import('./feature/auth/verify-email/verify-email.component').then(m => m.VerifyEmailComponent), + }, { path: 'game/blackjack', loadComponent: () => import('./feature/game/blackjack/blackjack.component'), diff --git a/frontend/src/app/feature/auth/register/register.component.ts b/frontend/src/app/feature/auth/register/register.component.ts index 60f289b..a421184 100644 --- a/frontend/src/app/feature/auth/register/register.component.ts +++ b/frontend/src/app/feature/auth/register/register.component.ts @@ -56,25 +56,6 @@ export class RegisterComponent { }; this.authService.register(registerRequest).subscribe({ - next: () => { - this.authService - .login({ - usernameOrEmail: registerRequest.email, - password: registerRequest.password, - }) - .subscribe({ - next: () => { - this.closeDialog.emit(); - this.router.navigate(['/home']); - }, - error: () => { - this.isLoading.set(false); - this.errorMessage.set( - 'Registration successful but failed to login automatically. Please log in manually.' - ); - }, - }); - }, error: (err: HttpErrorResponse) => { this.isLoading.set(false); diff --git a/frontend/src/app/feature/auth/verify-email/verify-email.component.css b/frontend/src/app/feature/auth/verify-email/verify-email.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/feature/auth/verify-email/verify-email.component.html b/frontend/src/app/feature/auth/verify-email/verify-email.component.html new file mode 100644 index 0000000..c39c8c0 --- /dev/null +++ b/frontend/src/app/feature/auth/verify-email/verify-email.component.html @@ -0,0 +1 @@ +

verify-email works!

diff --git a/frontend/src/app/feature/auth/verify-email/verify-email.component.ts b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts new file mode 100644 index 0000000..c67e1ff --- /dev/null +++ b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts @@ -0,0 +1,31 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@service/auth.service'; + +@Component({ + selector: 'app-verify-email', + imports: [], + templateUrl: './verify-email.component.html', + styleUrl: './verify-email.component.css' +}) +export class VerifyEmailComponent implements OnInit{ + route: ActivatedRoute = inject(ActivatedRoute); + router: Router = inject(Router); + authService: AuthService = inject(AuthService); + + ngOnInit(): void { + const token = this.route.snapshot.queryParamMap.get('token'); + + if (!token) { + this.router.navigate(['']); + console.log('no token'); + return; + } + + this.authService.verifyEmail(token).subscribe(() => { + this.router.navigate([''], { + queryParams: { login: true }, + }); + }) + } +} diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index d4d6078..5cfe0c5 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -7,7 +7,7 @@ import { signal, } from '@angular/core'; import { NgFor } from '@angular/common'; -import { RouterLink } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { AuthService } from '@service/auth.service'; import { LoginComponent } from '../auth/login/login.component'; import { RegisterComponent } from '../auth/register/register.component'; @@ -23,12 +23,16 @@ export class LandingComponent implements OnInit, OnDestroy { currentSlide = 0; private autoplayInterval: ReturnType | undefined; authService: AuthService = inject(AuthService); + route: ActivatedRoute = inject(ActivatedRoute); showLogin = signal(false); showRegister = signal(false); ngOnInit() { this.startAutoplay(); document.body.style.overflow = 'auto'; + if (this.route.snapshot.queryParamMap.get('login') === 'true') { + this.showLoginForm(); + } } ngOnDestroy() { diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 1066008..1133958 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -74,6 +74,10 @@ export class AuthService { }); } + public verifyEmail(token: string): Observable { + return this.http.post(`${this.authUrl}/verify?token=${token}`, null); + } + private setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); }