feat(auth): add email verification feature and handler
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / Docker frontend validation (pull_request) Successful in 1m2s
CI / Checkstyle Main (pull_request) Successful in 1m8s
CI / eslint (pull_request) Failing after 1m29s
CI / Docker backend validation (pull_request) Successful in 1m32s
CI / prettier (pull_request) Failing after 31s
CI / oxlint (pull_request) Successful in 44s
CI / test-build (pull_request) Successful in 38s
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / Docker frontend validation (pull_request) Successful in 1m2s
CI / Checkstyle Main (pull_request) Successful in 1m8s
CI / eslint (pull_request) Failing after 1m29s
CI / Docker backend validation (pull_request) Successful in 1m32s
CI / prettier (pull_request) Failing after 31s
CI / oxlint (pull_request) Successful in 44s
CI / test-build (pull_request) Successful in 38s
This commit is contained in:
parent
59aa831981
commit
d2225decc1
14 changed files with 124 additions and 27 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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<AuthResponseDto> authenticateUser(@Valid @RequestBody LoginRequestDto loginRequest) {
|
||||
public ResponseEntity<AuthResponseDto> 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<Void> verifyEmail(@RequestParam("token") String token) {
|
||||
if (authService.verifyEmail(token)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UserEntity> optionalUser = userService.getUserByVerificationToken(token);
|
||||
|
||||
if(!optionalUser.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UserEntity user = optionalUser.get();
|
||||
|
||||
user.setEmailVerified(true);
|
||||
user.setVerificationToken(null);
|
||||
userService.saveUser(user);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UserEntity, Long> {
|
|||
boolean existsByUsername(String username);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
@Query("SELECT u FROM UserEntity u WHERE u.verificationToken = ?1")
|
||||
Optional<UserEntity> findOneByVerificationToken(String token);
|
||||
|
||||
@Query("SELECT u FROM UserEntity u WHERE u.username = ?1 OR u.email = ?1")
|
||||
Optional<UserEntity> findOneByUsernameOrEmail(String usernameOrEmail);
|
||||
}
|
||||
|
|
|
@ -44,4 +44,22 @@ public class UserService {
|
|||
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
public Optional<UserEntity> getUserByVerificationToken(String token) {
|
||||
return this.userRepository.findOneByVerificationToken(token);
|
||||
}
|
||||
|
||||
public void saveUser(UserEntity user) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public boolean isVerified(String usernameOrEmail) {
|
||||
Optional<UserEntity> optionalUser = userRepository.findOneByUsernameOrEmail(usernameOrEmail);
|
||||
|
||||
if (!optionalUser.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return optionalUser.get().getEmailVerified();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
<p>Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen:</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="${feUrl}/backend/verify?token=${token}" class="button">E-Mail bestätigen</a>
|
||||
<a href="${feUrl}/verify?token=${token}" class="button">E-Mail bestätigen</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<p>verify-email works!</p>
|
|
@ -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 },
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<typeof setInterval> | 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() {
|
||||
|
|
|
@ -74,6 +74,10 @@ export class AuthService {
|
|||
});
|
||||
}
|
||||
|
||||
public verifyEmail(token: string): Observable<any> {
|
||||
return this.http.post<any>(`${this.authUrl}/verify?token=${token}`, null);
|
||||
}
|
||||
|
||||
private setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
|
Reference in a new issue