diff --git a/docker-compose.yml b/docker-compose.yml
index 7e669a1..71f3ee4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,8 +3,6 @@
services:
client:
container_name: tasknote
- profiles:
- - "dev"
image: node:20.17-bullseye-slim
ports:
- "5000:5000"
@@ -20,8 +18,6 @@ services:
java-api:
container_name: java-api
- profiles:
- - "dev"
depends_on:
postgres:
condition: service_started
@@ -49,8 +45,6 @@ services:
postgres:
container_name: postgres
- profiles:
- - "dev"
image: postgres:15.8-bookworm
environment:
POSTGRES_DB: tasknote
@@ -59,7 +53,7 @@ services:
POSTGRES_PASSWORD: default
POSTGRES_PORT: 5432
volumes:
- - "./localpgdata:/pgdata"
+ - "/pgdata"
ports:
- "5432:5432"
healthcheck:
diff --git a/java-api/pom.xml b/java-api/pom.xml
index db437f2..4df81cb 100644
--- a/java-api/pom.xml
+++ b/java-api/pom.xml
@@ -102,6 +102,25 @@
springdoc-openapi-starter-webmvc-ui
2.6.0
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/JavaApiApplication.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/JavaApiApplication.java
index 7ef4353..86382b9 100644
--- a/java-api/src/main/java/br/com/tasknoteapp/java_api/JavaApiApplication.java
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/JavaApiApplication.java
@@ -3,11 +3,16 @@ package br.com.tasknoteapp.java_api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+/** Entrypoint of the Java API service application. */
@SpringBootApplication
public class JavaApiApplication {
- public static void main(String[] args) {
- SpringApplication.run(JavaApiApplication.class, args);
- }
-
+ /**
+ * Main method of the application.
+ *
+ * @param args Additional arguments, if any.
+ */
+ public static void main(String[] args) {
+ SpringApplication.run(JavaApiApplication.class, args);
+ }
}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/JwtAuthenticationResponse.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/JwtAuthenticationResponse.java
new file mode 100644
index 0000000..8848cde
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/JwtAuthenticationResponse.java
@@ -0,0 +1,3 @@
+package br.com.tasknoteapp.java_api.auth;
+
+public record JwtAuthenticationResponse(String token) {}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/LoginRequest.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/LoginRequest.java
new file mode 100644
index 0000000..b2351df
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/auth/LoginRequest.java
@@ -0,0 +1,11 @@
+package br.com.tasknoteapp.java_api.auth;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotNull;
+
+/** This record represents a login request with user email and password. */
+@Schema(description = "Login request with user email and password.")
+public record LoginRequest(
+ @Schema(description = "User email.") @Email @NotNull String email,
+ @Schema(description = "User password.") @NotNull String password) {}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SecurityConfig.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SecurityConfig.java
index 9d14c11..c5a7f2d 100644
--- a/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SecurityConfig.java
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SecurityConfig.java
@@ -1,22 +1,35 @@
package br.com.tasknoteapp.java_api.config;
+import br.com.tasknoteapp.java_api.filter.JwtAuthenticationFilter;
+import br.com.tasknoteapp.java_api.service.UserService;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
-import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/** This class contains security configurations. */
@Configuration
@EnableWebSecurity
-@EnableMethodSecurity
+@RequiredArgsConstructor
public class SecurityConfig {
+ private final UserService userService;
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
/**
* Filters a request to add security checks and configurations.
*
@@ -27,18 +40,44 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults())
- .csrf(custom -> custom.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
+ .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
- custom ->
- custom
+ request ->
+ request
+ .requestMatchers("/auth/**")
+ .permitAll()
.requestMatchers("/rest/**")
.authenticated()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.anyRequest()
.permitAll())
+ .sessionManagement(
+ manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(AbstractHttpConfigurer::disable)
- .formLogin(AbstractHttpConfigurer::disable);
+ .formLogin(AbstractHttpConfigurer::disable)
+ .authenticationProvider(authenticationProvider())
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
return http.build();
}
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public AuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userService.userDetailsService());
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
+ throws Exception {
+ return config.getAuthenticationManager();
+ }
}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SwaggerConfig.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SwaggerConfig.java
index 4548eb2..753294e 100644
--- a/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SwaggerConfig.java
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/config/SwaggerConfig.java
@@ -55,6 +55,6 @@ public class SwaggerConfig {
openApi.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
openApi.setComponents(components);
- return new OpenAPI().info(info);
+ return openApi;
}
}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/AuthenticationController.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/AuthenticationController.java
new file mode 100644
index 0000000..d4adae7
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/AuthenticationController.java
@@ -0,0 +1,68 @@
+package br.com.tasknoteapp.java_api.controller;
+
+import br.com.tasknoteapp.java_api.auth.JwtAuthenticationResponse;
+import br.com.tasknoteapp.java_api.auth.LoginRequest;
+import br.com.tasknoteapp.java_api.exception.UserAlreadyExistsException;
+import br.com.tasknoteapp.java_api.service.AuthService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+@Tag(name = "Authentication", description = "Authentication controller.")
+@AllArgsConstructor
+public class AuthenticationController {
+
+ private final AuthService authService;
+
+ /**
+ * Authenticate a user given his email and password.
+ *
+ * @param loginRequest User data containing email and password.
+ * @return OK if authenticated, 401 - Unauthorized otherwise
+ */
+ @PostMapping(path = "/signin", consumes = "application/json", produces = "application/json")
+ @Operation(
+ summary = "SigIn an existing user",
+ description = "SigIn an existing user given his email and password",
+ responses = {
+ @ApiResponse(responseCode = "200", description = "User successfully logged in"),
+ @ApiResponse(responseCode = "400", description = "Wrong or missing information"),
+ })
+ public JwtAuthenticationResponse signin(@RequestBody @Valid LoginRequest loginRequest) {
+ String token = authService.signin(loginRequest);
+ return new JwtAuthenticationResponse(token);
+ }
+
+ /**
+ * Signup a new user.
+ *
+ * @param loginRequest User data with email and password.
+ * @return JwtAuthenticationResponse containing user token
+ * @throws UserAlreadyExistsException
+ */
+ @PutMapping(path = "/signup", consumes = "application/json", produces = "application/json")
+ @Operation(
+ summary = "Signup a new user",
+ description = "Signup a new user given his email and password",
+ responses = {
+ @ApiResponse(responseCode = "201", description = "User successfully created and saved"),
+ @ApiResponse(responseCode = "400", description = "Wrong or missing information"),
+ @ApiResponse(responseCode = "409", description = "User already exists")
+ })
+ public ResponseEntity signup(
+ @RequestBody @Valid LoginRequest loginRequest) {
+ String token = authService.create(loginRequest);
+ return ResponseEntity.status(HttpStatus.CREATED).body(new JwtAuthenticationResponse(token));
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/SomeController.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/SomeController.java
new file mode 100644
index 0000000..4294e91
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/controller/SomeController.java
@@ -0,0 +1,15 @@
+package br.com.tasknoteapp.java_api.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/rest/some")
+public class SomeController {
+
+ @GetMapping
+ public String some() {
+ return "Some";
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/entity/UserEntity.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/entity/UserEntity.java
new file mode 100644
index 0000000..1c04f14
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/entity/UserEntity.java
@@ -0,0 +1,71 @@
+package br.com.tasknoteapp.java_api.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+import lombok.Data;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/** This class represents a User in the database. */
+@Data
+@Entity
+@Table(name = "users")
+public class UserEntity implements UserDetails {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String email;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Column(nullable = false)
+ private Boolean admin;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "inactivated_at", nullable = true)
+ private LocalDateTime inactivatedAt;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+
+ @Override
+ public String getUsername() {
+ // email in our case
+ return email;
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserAlreadyExistsException.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserAlreadyExistsException.java
new file mode 100644
index 0000000..cca7b1a
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserAlreadyExistsException.java
@@ -0,0 +1,13 @@
+package br.com.tasknoteapp.java_api.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+@ResponseStatus(code = HttpStatus.CONFLICT)
+public class UserAlreadyExistsException extends ResponseStatusException {
+
+ public UserAlreadyExistsException() {
+ super(HttpStatus.CONFLICT, "User already exists");
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserNotFoundException.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserNotFoundException.java
new file mode 100644
index 0000000..8d61ddd
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/exception/UserNotFoundException.java
@@ -0,0 +1,13 @@
+package br.com.tasknoteapp.java_api.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class UserNotFoundException extends ResponseStatusException {
+
+ public UserNotFoundException() {
+ super(HttpStatus.NOT_FOUND, "User not found");
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/filter/JwtAuthenticationFilter.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..fd659fa
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,74 @@
+package br.com.tasknoteapp.java_api.filter;
+
+import br.com.tasknoteapp.java_api.service.JwtService;
+import br.com.tasknoteapp.java_api.service.UserService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@Slf4j
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ @Autowired private UserService userService;
+
+ @Autowired private JwtService jwtService;
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain)
+ throws ServletException, IOException {
+ final String authorizationHeader = request.getHeader("Authorization");
+
+ log.info("-1-authorizationHeader {}", authorizationHeader);
+
+ if (Objects.isNull(authorizationHeader) || authorizationHeader.isBlank()) {
+ log.info("-2-authorizationHeader {}", authorizationHeader);
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ log.info("-3-authorizationHeader {}", authorizationHeader);
+
+ String jwtToken = authorizationHeader.substring(7);
+ String email = jwtService.getEmailFromToken(jwtToken);
+
+ log.info("-4-email {}", email);
+
+ if (!Objects.isNull(email)
+ && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
+ log.info("-5-email {}", email);
+ UserDetails user = userService.userDetailsService().loadUserByUsername(email);
+
+ if (jwtService.validateTokenAndUser(jwtToken, user)) {
+ log.info("-6-jwtToken {}", jwtToken);
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(
+ user.getUsername(), user.getPassword(), user.getAuthorities());
+
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ context.setAuthentication(authToken);
+ SecurityContextHolder.setContext(context);
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/repository/UserRepository.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/repository/UserRepository.java
new file mode 100644
index 0000000..af80264
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/repository/UserRepository.java
@@ -0,0 +1,11 @@
+package br.com.tasknoteapp.java_api.repository;
+
+import br.com.tasknoteapp.java_api.entity.UserEntity;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/** This interface contains methods to access the user table in the database. */
+public interface UserRepository extends JpaRepository {
+
+ Optional findByEmail(String email);
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/AuthService.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/AuthService.java
new file mode 100644
index 0000000..91b4a53
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/AuthService.java
@@ -0,0 +1,41 @@
+package br.com.tasknoteapp.java_api.service;
+
+import br.com.tasknoteapp.java_api.auth.LoginRequest;
+import br.com.tasknoteapp.java_api.entity.UserEntity;
+import java.util.Optional;
+import org.springframework.security.core.userdetails.User;
+
+public interface AuthService {
+
+ /**
+ * Create a new user in the app.
+ *
+ * @param login User details with email and password.
+ * @return Token
+ */
+ public String create(LoginRequest login);
+
+ /**
+ * Find a user by email in the database.
+ *
+ * @param email The user email.
+ * @return Optional of a UserEntity instance.
+ */
+ public Optional findByEmail(String email);
+
+ /**
+ * Load a user from the database given his email.
+ *
+ * @param email The user email.
+ * @return User with found record.
+ */
+ public User loadUserByUsername(String email);
+
+ /**
+ * SignIn a user given his email and password.
+ *
+ * @param login User details with email and password.
+ * @return Token
+ */
+ public String signin(LoginRequest login);
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/JwtService.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/JwtService.java
new file mode 100644
index 0000000..7246f78
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/JwtService.java
@@ -0,0 +1,20 @@
+package br.com.tasknoteapp.java_api.service;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public interface JwtService {
+
+ public String getEmailFromToken(String token);
+
+ public LocalDateTime extractExpiration(String token);
+
+ public String generateToken(String username);
+
+ public String createToken(Map claims, String email);
+
+ public boolean isTokenExpired(String token);
+
+ public boolean validateTokenAndUser(String token, UserDetails user);
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/UserService.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/UserService.java
new file mode 100644
index 0000000..1bb628b
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/UserService.java
@@ -0,0 +1,7 @@
+package br.com.tasknoteapp.java_api.service;
+
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+public interface UserService {
+ UserDetailsService userDetailsService();
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/AuthServiceImpl.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/AuthServiceImpl.java
new file mode 100644
index 0000000..a9b9f84
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/AuthServiceImpl.java
@@ -0,0 +1,111 @@
+package br.com.tasknoteapp.java_api.service.impl;
+
+import br.com.tasknoteapp.java_api.auth.LoginRequest;
+import br.com.tasknoteapp.java_api.entity.UserEntity;
+import br.com.tasknoteapp.java_api.exception.UserAlreadyExistsException;
+import br.com.tasknoteapp.java_api.exception.UserNotFoundException;
+import br.com.tasknoteapp.java_api.repository.UserRepository;
+import br.com.tasknoteapp.java_api.service.AuthService;
+import br.com.tasknoteapp.java_api.service.JwtService;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Optional;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+class AuthServiceImpl implements AuthService {
+
+ private final UserRepository userRepository;
+
+ private final PasswordEncoder passwordEncoder;
+
+ private final JwtService jwtService;
+
+ private final AuthenticationManager authenticationManager;
+
+ /**
+ * Create a new user in the app.
+ *
+ * @param login User details with email and password.
+ * @return Token
+ */
+ @Override
+ public String create(LoginRequest login) {
+ log.info("Creating user! {}", login.email());
+
+ if (findByEmail(login.email()).isPresent()) {
+ throw new UserAlreadyExistsException();
+ }
+
+ UserEntity user = new UserEntity();
+ user.setEmail(login.email());
+ user.setPassword(passwordEncoder.encode(login.password()));
+ user.setAdmin(login.email().equals("ricardompcampos@gmail.com"));
+ user.setCreatedAt(LocalDateTime.now());
+ userRepository.save(user);
+
+ String token = jwtService.generateToken(user.getEmail());
+
+ log.info("User created! Token {}", token);
+ return token;
+ }
+
+ /**
+ * Find a user by email in the database.
+ *
+ * @param email The user email.
+ * @return Optional of a UserEntity instance.
+ */
+ @Override
+ public Optional findByEmail(String email) {
+ return userRepository.findByEmail(email);
+ }
+
+ /**
+ * Load a user from the database given his email.
+ *
+ * @param email The user email.
+ * @return User with found record.
+ */
+ @Override
+ public User loadUserByUsername(String email) {
+ Optional user = userRepository.findByEmail(email);
+ if (user.isEmpty()) {
+ throw new UserNotFoundException();
+ }
+
+ return new User(user.get().getEmail(), user.get().getPassword(), new ArrayList<>());
+ }
+
+ /**
+ * SignIn a user given his email and password.
+ *
+ * @param login User details with email and password.
+ * @return Token
+ */
+ @Override
+ public String signin(LoginRequest login) {
+ log.info("Creating user! {}", login.email());
+
+ Optional user = findByEmail(login.email());
+ if (user.isEmpty()) {
+ throw new UserNotFoundException();
+ }
+
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(login.email(), login.password()));
+
+ String token = jwtService.generateToken(user.get().getEmail());
+
+ log.info("User authenticated! Token {}", token);
+ return token;
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/JwtServiceImpl.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/JwtServiceImpl.java
new file mode 100644
index 0000000..acfc44d
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/JwtServiceImpl.java
@@ -0,0 +1,70 @@
+package br.com.tasknoteapp.java_api.service.impl;
+
+import br.com.tasknoteapp.java_api.service.JwtService;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import javax.crypto.SecretKey;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JwtServiceImpl implements JwtService {
+
+ private final long SECOND = 1000;
+ private final long MINUTE = SECOND * 60;
+ private final long EXPIRATION_TIME = MINUTE * 30;
+ private final SecretKey KEY = Jwts.SIG.HS256.key().build();
+
+ @Override
+ public String getEmailFromToken(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ @Override
+ public LocalDateTime extractExpiration(String token) {
+ Date date = extractClaim(token, Claims::getExpiration);
+ return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
+ }
+
+ @Override
+ public String generateToken(String username) {
+ Map claims = new HashMap<>();
+ return createToken(claims, username);
+ }
+
+ @Override
+ public String createToken(Map claims, String email) {
+ return Jwts.builder()
+ .issuer("Java-API")
+ .subject(email)
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
+ .signWith(KEY)
+ .compact();
+ }
+
+ @Override
+ public boolean isTokenExpired(String token) {
+ return extractExpiration(token).isBefore(LocalDateTime.now());
+ }
+
+ @Override
+ public boolean validateTokenAndUser(String token, UserDetails user) {
+ final String email = user.getUsername();
+ return !isTokenExpired(token) && getEmailFromToken(token).equals(email);
+ }
+
+ private T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token).getPayload();
+ }
+}
diff --git a/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/UserServiceImpl.java b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..b485b22
--- /dev/null
+++ b/java-api/src/main/java/br/com/tasknoteapp/java_api/service/impl/UserServiceImpl.java
@@ -0,0 +1,32 @@
+package br.com.tasknoteapp.java_api.service.impl;
+
+import br.com.tasknoteapp.java_api.entity.UserEntity;
+import br.com.tasknoteapp.java_api.repository.UserRepository;
+import br.com.tasknoteapp.java_api.service.UserService;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+
+ private final UserRepository userRepository;
+
+ @Override
+ public UserDetailsService userDetailsService() {
+ return new UserDetailsService() {
+ @Override
+ public UserDetails loadUserByUsername(String email) {
+ Optional user = userRepository.findByEmail(email);
+ if (user.isEmpty()) {
+ throw new RuntimeException("User not found: " + email);
+ }
+
+ return user.get();
+ }
+ };
+ }
+}
diff --git a/java-api/src/main/resources/db/migration/V202209041850__schema_and_user_creation.sql b/java-api/src/main/resources/db/migration/V202209041850__schema_and_user_creation.sql
new file mode 100644
index 0000000..eefed85
--- /dev/null
+++ b/java-api/src/main/resources/db/migration/V202209041850__schema_and_user_creation.sql
@@ -0,0 +1,12 @@
+CREATE SCHEMA IF NOT EXISTS tasknote;
+
+CREATE TABLE IF NOT EXISTS tasknote.users (
+ id SERIAL,
+ email VARCHAR(100) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ admin BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL,
+ inactivated_at TIMESTAMP,
+ CONSTRAINT users_pk PRIMARY KEY (id),
+ CONSTRAINT users_email_uk UNIQUE (email)
+);