feat: add email verification

This commit is contained in:
Tim
2025-06-30 17:53:13 +08:00
parent aa6d32e7dd
commit 39179c370e
5 changed files with 62 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@@ -25,9 +26,17 @@ public class AuthController {
@PostMapping("/register") @PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest req) { public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
User user = userService.register(req.getUsername(), req.getEmail(), req.getPassword()); User user = userService.register(req.getUsername(), req.getEmail(), req.getPassword());
emailService.sendEmail(user.getEmail(), "Welcome to OpenIsle", "Thank you for registering."); emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
String token = jwtService.generateToken(user.getUsername()); return ResponseEntity.ok(Map.of("message", "Verification code sent"));
return ResponseEntity.ok(new JwtResponse(token)); }
@PostMapping("/verify")
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
if (ok) {
return ResponseEntity.ok(Map.of("message", "Verified"));
}
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
} }
/** /**
@@ -39,7 +48,7 @@ public class AuthController {
public ResponseEntity<?> login(@RequestBody LoginRequest req) { public ResponseEntity<?> login(@RequestBody LoginRequest req) {
return userService.authenticate(req.getUsername(), req.getPassword()) return userService.authenticate(req.getUsername(), req.getPassword())
.map(user -> ResponseEntity.ok(new JwtResponse(jwtService.generateToken(user.getUsername())))) .map(user -> ResponseEntity.ok(new JwtResponse(jwtService.generateToken(user.getUsername()))))
.orElse(ResponseEntity.status(401).build()); .orElse(ResponseEntity.status(401).body(Map.of("error", "Invalid credentials or user not verified")));
} }
@Data @Data
@@ -55,6 +64,12 @@ public class AuthController {
private String password; private String password;
} }
@Data
private static class VerifyRequest {
private String username;
private String code;
}
@Data @Data
private static class JwtResponse { private static class JwtResponse {
private final String token; private final String token;

View File

@@ -0,0 +1,17 @@
package com.openisle.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}

View File

@@ -2,6 +2,7 @@ package com.openisle.controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController @RestController
public class HelloController { public class HelloController {
@@ -10,7 +11,7 @@ public class HelloController {
* -H "Authorization: Bearer <jwt-token>" * -H "Authorization: Bearer <jwt-token>"
*/ */
@GetMapping("/api/hello") @GetMapping("/api/hello")
public String hello() { public Map<String, String> hello() {
return "Hello, Authenticated User"; return Map.of("message", "Hello, Authenticated User");
} }
} }

View File

@@ -23,4 +23,9 @@ public class User {
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;
@Column(nullable = false)
private boolean verified = false;
private String verificationCode;
} }

View File

@@ -8,6 +8,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Optional; import java.util.Optional;
import java.util.Random;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -17,17 +18,33 @@ public class UserService {
public User register(String username, String email, String password) { public User register(String username, String email, String password) {
if (userRepository.findByUsername(username).isPresent() || userRepository.findByEmail(email).isPresent()) { if (userRepository.findByUsername(username).isPresent() || userRepository.findByEmail(email).isPresent()) {
throw new RuntimeException("User already exists"); throw new IllegalStateException("User already exists");
} }
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
user.setEmail(email); user.setEmail(email);
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setVerified(false);
String code = String.format("%06d", new Random().nextInt(1000000));
user.setVerificationCode(code);
return userRepository.save(user); return userRepository.save(user);
} }
public boolean verifyCode(String username, String code) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) {
User user = userOpt.get();
user.setVerified(true);
user.setVerificationCode(null);
userRepository.save(user);
return true;
}
return false;
}
public Optional<User> authenticate(String username, String password) { public Optional<User> authenticate(String username, String password) {
return userRepository.findByUsername(username) return userRepository.findByUsername(username)
.filter(User::isVerified)
.filter(user -> passwordEncoder.matches(password, user.getPassword())); .filter(user -> passwordEncoder.matches(password, user.getPassword()));
} }
} }