Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>it.ozimov</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import dev.felipemlozx.api_auth.controller.dto.CreateUserDto;
import dev.felipemlozx.api_auth.controller.dto.LoginDTO;
import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO;
import dev.felipemlozx.api_auth.core.AuthError;
import dev.felipemlozx.api_auth.core.LoginFailure;
import dev.felipemlozx.api_auth.core.LoginResult;
import dev.felipemlozx.api_auth.core.LoginSuccess;
import dev.felipemlozx.api_auth.services.AuthService;
import dev.felipemlozx.api_auth.utils.ApiResponse;
import jakarta.mail.MessagingException;
Expand All @@ -24,32 +28,48 @@ public AuthController(AuthService authService) {

@PostMapping("/register")
public ResponseEntity<ApiResponse<List<String>>> register(@RequestBody CreateUserDto body) throws MessagingException {
var fails = authService.register(body);
if (fails.isEmpty()){
List<String> response = authService.register(body);
if (response.isEmpty()){
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse
.success("User registered.", List.of("Verification email sent.")));
.success("User created. Verify your email.", null));
}
return ResponseEntity.badRequest().body(ApiResponse.error(fails));
return ResponseEntity.badRequest()
.body(ApiResponse.error("Validation errors", response));
}

@PostMapping("/login")
public ResponseEntity<ApiResponse<ResponseLoginDTO>> login(@RequestBody LoginDTO body){
ResponseLoginDTO response = authService.login(body);
if(response != null ){
ApiResponse<ResponseLoginDTO> apiResponse = ApiResponse.success("User logged in successfully", response);
return ResponseEntity.ok().body(apiResponse);
} else {
ApiResponse<ResponseLoginDTO> apiResponse = ApiResponse.error("User or password is Incorrect", null);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiResponse);
LoginResult result = authService.login(body);

if(result instanceof LoginSuccess(var accessToken, var refreshToken)) {
ResponseLoginDTO response = new ResponseLoginDTO(accessToken, refreshToken);
return ResponseEntity.ok().body(ApiResponse.success(response));
}

LoginFailure loginFailure = (LoginFailure) result;

if(loginFailure.error().equals(AuthError.EMAIL_NOT_VERIFIED)){
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error("Email not verified", null));
}
if(loginFailure.error().equals(AuthError.INVALID_CREDENTIALS)){
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error("User or password is incorrect", null));
}

return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error("User not register.", null));
}

@GetMapping("/verify-email/{id}")
public ResponseEntity<ApiResponse<String>> verifyEmail(@PathVariable(name = "id") String token){
@GetMapping("/verify-email/{token}")
public ResponseEntity<ApiResponse<Void>> verifyEmail(@PathVariable String token){
boolean isValid = authService.verifyEmailToken(token);
if(!isValid) return ResponseEntity.badRequest().body(ApiResponse.error("Link invalid."));
return ResponseEntity.ok().build();
if (isValid) {
return ResponseEntity.ok(ApiResponse.success("Email verified", null));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error("Invalid or expired token", null));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.felipemlozx.api_auth.core;

public record AuthCheckFailure(AuthError error) implements AuthCheckResult { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.felipemlozx.api_auth.core;

public sealed interface AuthCheckResult permits AuthCheckSuccess, AuthCheckFailure { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.felipemlozx.api_auth.core;

import dev.felipemlozx.api_auth.entity.User;

public record AuthCheckSuccess(User user) implements AuthCheckResult { }
8 changes: 8 additions & 0 deletions src/main/java/dev/felipemlozx/api_auth/core/AuthError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.felipemlozx.api_auth.core;

public enum AuthError {
USER_NOT_FOUND,
EMAIL_NOT_VERIFIED,
INVALID_CREDENTIALS,
USER_NOT_REGISTER
}
3 changes: 3 additions & 0 deletions src/main/java/dev/felipemlozx/api_auth/core/LoginFailure.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.felipemlozx.api_auth.core;

public record LoginFailure(AuthError error) implements LoginResult { }
3 changes: 3 additions & 0 deletions src/main/java/dev/felipemlozx/api_auth/core/LoginResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.felipemlozx.api_auth.core;

public sealed interface LoginResult permits LoginSuccess, LoginFailure { }
3 changes: 3 additions & 0 deletions src/main/java/dev/felipemlozx/api_auth/core/LoginSuccess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.felipemlozx.api_auth.core;

public record LoginSuccess(String accessToken, String refreshToken) implements LoginResult { }
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public interface UserRepository extends JpaRepository<User, Long> {

List<User> findByVerifiedIsFalse();

boolean existsByEmail(String email);
}
28 changes: 20 additions & 8 deletions src/main/java/dev/felipemlozx/api_auth/services/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import dev.felipemlozx.api_auth.controller.dto.CreateUserDto;
import dev.felipemlozx.api_auth.controller.dto.LoginDTO;
import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO;
import dev.felipemlozx.api_auth.core.AuthCheckFailure;
import dev.felipemlozx.api_auth.core.AuthCheckResult;
import dev.felipemlozx.api_auth.core.AuthCheckSuccess;
import dev.felipemlozx.api_auth.core.LoginFailure;
import dev.felipemlozx.api_auth.core.LoginResult;
import dev.felipemlozx.api_auth.core.LoginSuccess;
import dev.felipemlozx.api_auth.infra.security.TokenService;
import jakarta.mail.MessagingException;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -30,7 +35,9 @@ public List<String> register(CreateUserDto body) throws MessagingException {
List<String> result = userService.register(body);
if(result.isEmpty()){
String token = userService.createEmailVerificationToken(body.email());
emailService.sendEmail(body.email(), body.name(), generateLinkToVerifyEmail(token));
if(token != null ) {
emailService.sendEmail(body.email(), body.name(), generateLinkToVerifyEmail(token));
}
}
return result;
}
Expand All @@ -39,14 +46,19 @@ protected String generateLinkToVerifyEmail(String token){
return this.apiUrl + "/verify-email/" + token;
}

public ResponseLoginDTO login(LoginDTO request) {
boolean authorization = userService.login(request);
if(!authorization) return null;
public LoginResult login(LoginDTO request) {
AuthCheckResult checkResult = userService.login(request);
if(checkResult instanceof AuthCheckFailure(var error)) {
return new LoginFailure(error);
}

var success = (AuthCheckSuccess) checkResult;
var user = success.user();

String accessToken = tokenService.generateToken(request.email());
String refreshToken = tokenService.generateToken(request.email());
String accessToken = tokenService.generateToken(user.getEmail());
String refreshToken = tokenService.generateToken(user.getEmail());

return new ResponseLoginDTO(accessToken, refreshToken);
return new LoginSuccess(accessToken, refreshToken);
}

public boolean verifyEmailToken(String token) {
Expand Down
80 changes: 46 additions & 34 deletions src/main/java/dev/felipemlozx/api_auth/services/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import dev.felipemlozx.api_auth.controller.dto.CreateUserDto;
import dev.felipemlozx.api_auth.controller.dto.LoginDTO;
import dev.felipemlozx.api_auth.core.AuthCheckFailure;
import dev.felipemlozx.api_auth.core.AuthCheckResult;
import dev.felipemlozx.api_auth.core.AuthCheckSuccess;
import dev.felipemlozx.api_auth.core.AuthError;
import dev.felipemlozx.api_auth.entity.User;
import dev.felipemlozx.api_auth.repository.UserRepository;
import dev.felipemlozx.api_auth.utils.CheckUtils;
Expand All @@ -14,6 +18,7 @@

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Service
Expand All @@ -29,28 +34,13 @@ public UserService(UserRepository userRepository, PasswordEncoder encoder, Cach
this.cacheManager = cacheManager;
}

public void saveToken(String token, String email) {
Cache cache = cacheManager.getCache("EmailVerificationTokens");
if (cache != null) {
cache.put(token, email);
}
}

public String recuperarToken(String token) {
Cache cache = cacheManager.getCache("EmailVerificationTokens");
if (cache != null) {
Cache.ValueWrapper wrapper = cache.get(token);
if (wrapper != null) {
return (String) wrapper.get();
}
}
return null;
}

@Transactional
public List<String> register(CreateUserDto userDto){
public List<String> register(CreateUserDto userDto) {
List<String> errors = CheckUtils.validatePasswordAndEmail(userDto.password(), userDto.email());

boolean userExist = userRepository.existsByEmail(userDto.email());
if(userExist) errors.add("Email already exists");

if (errors.isEmpty()) {
User user = new User();
user.setName(userDto.name());
Expand All @@ -61,34 +51,45 @@ public List<String> register(CreateUserDto userDto){
return errors;
}

public Boolean login(LoginDTO userLogin) {
User user = userRepository.findByEmail(userLogin.email())
.orElseThrow(() -> new RuntimeException("User not found."));
public AuthCheckResult login(LoginDTO userLogin) {
Optional<User> maybeUser = userRepository.findByEmail(userLogin.email());

if(maybeUser.isEmpty()) return new AuthCheckFailure(AuthError.USER_NOT_REGISTER);
User user = maybeUser.get();

if(!user.isVerified()) return new AuthCheckFailure(AuthError.EMAIL_NOT_VERIFIED);

if(!user.isVerified()){
throw new RuntimeException("Email not verify");
boolean matches = passwordEncoder.matches(userLogin.password(), user.getPassword());

if (!matches) {
return new AuthCheckFailure(AuthError.INVALID_CREDENTIALS);
}

return passwordEncoder.matches(userLogin.password(), user.getPassword());
return new AuthCheckSuccess(user);
}

public String createEmailVerificationToken(String email) {
userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found."));
Optional<User> maybeUser = userRepository.findByEmail(email);
if(maybeUser.isEmpty()) return null;

UUID token = UUID.randomUUID();
saveToken(token.toString(), email);
return token.toString();
}

public Boolean verifyEmailToken(String token) {
String email = recuperarToken(token);
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("Link invalid."));
String email = recoverToken(token);
if(email == null) return false;

Optional<User> maybeUser = userRepository.findByEmail(email);
if(maybeUser.isEmpty()) return false;
User user = maybeUser.get();

boolean isValid = Instant.now().isBefore(user.getTimeVerify());
if(!isValid){
return false;
}

user.setVerified(true);
userRepository.save(user);
return true;
Expand All @@ -98,16 +99,27 @@ public Boolean verifyEmailToken(String token) {
@Scheduled(fixedRate = 1800000)
public void deleteUserNotVerify() {
List<User> userList = userRepository.findByVerifiedIsFalse();
Instant now = Instant.now();
for(User user : userList){
boolean isValid = Instant.now().isBefore(user.getTimeVerify());
if(!isValid){
userRepository.delete(user);
}
boolean isValid = now.isBefore(user.getTimeVerify());
if(!isValid) userRepository.delete(user);
}
}

public User findById(Long id){
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found."));
}

public void saveToken(String token, String email) {
Cache cache = cacheManager.getCache("EmailVerificationTokens");
if (cache != null) cache.put(token, email);
}

public String recoverToken(String token) {
Cache cache = cacheManager.getCache("EmailVerificationTokens");
if (cache == null) return null;
Cache.ValueWrapper wrapper = cache.get(token);
return wrapper != null ? (String) wrapper.get() : null;
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ api:
secret:
key: secrete

API_URL: http://localhost:8080/api/v1
API_URL: http://localhost:4200/veryfi-email/
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
package dev.felipemlozx.api_auth;

import dev.felipemlozx.api_auth.config.EmbeddedRedisConfig;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;


import java.io.IOException;

@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc(addFilters = false)
class ApiAuthApplicationTests {

// @Test
// void contextLoads() {
// }
@Autowired
private MockMvc mockMvc;

@BeforeAll
static void up() throws IOException {
EmbeddedRedisConfig.startRedis();
}

@AfterAll
static void down() {
EmbeddedRedisConfig.stopRedis();
}

@Test
void contextLoads() {
}

}
Loading