diff --git a/pom.xml b/pom.xml index 1a9198a..ee066d6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -46,14 +46,18 @@ org.springframework.boot spring-boot-starter-test - test + compile - - - - - + + org.flywaydb + flyway-core + + + + org.flywaydb + flyway-mysql + com.github.dozermapper @@ -72,6 +76,31 @@ ${springdoc.version} + + org.springframework.boot + spring-boot-starter-security + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + diff --git a/src/main/java/com/ws/taskmanager/common/DateUtils.java b/src/main/java/com/ws/taskmanager/common/DateUtils.java new file mode 100644 index 0000000..3a7c217 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/common/DateUtils.java @@ -0,0 +1,14 @@ +package com.ws.taskmanager.common; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +public class DateUtils { + public static String convertDateToString(Date date) { + SimpleDateFormat formatter = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + return formatter.format(date); + } +} diff --git a/src/main/java/com/ws/taskmanager/configs/AppConfig.java b/src/main/java/com/ws/taskmanager/config/AppConfig.java similarity index 79% rename from src/main/java/com/ws/taskmanager/configs/AppConfig.java rename to src/main/java/com/ws/taskmanager/config/AppConfig.java index 3f04531..c4e1048 100644 --- a/src/main/java/com/ws/taskmanager/configs/AppConfig.java +++ b/src/main/java/com/ws/taskmanager/config/AppConfig.java @@ -1,10 +1,9 @@ -package com.ws.taskmanager.configs; +package com.ws.taskmanager.config; import jakarta.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; import java.util.TimeZone; -import java.util.spi.TimeZoneNameProvider; @Configuration public class AppConfig { diff --git a/src/main/java/com/ws/taskmanager/configs/date/DateConfig.java b/src/main/java/com/ws/taskmanager/config/date/DateConfig.java similarity index 96% rename from src/main/java/com/ws/taskmanager/configs/date/DateConfig.java rename to src/main/java/com/ws/taskmanager/config/date/DateConfig.java index 6a867a2..4c0fb1d 100644 --- a/src/main/java/com/ws/taskmanager/configs/date/DateConfig.java +++ b/src/main/java/com/ws/taskmanager/config/date/DateConfig.java @@ -1,4 +1,4 @@ -package com.ws.taskmanager.configs.date; +package com.ws.taskmanager.config.date; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; diff --git a/src/main/java/com/ws/taskmanager/config/security/SecurityConfig.java b/src/main/java/com/ws/taskmanager/config/security/SecurityConfig.java new file mode 100644 index 0000000..a5b3a4a --- /dev/null +++ b/src/main/java/com/ws/taskmanager/config/security/SecurityConfig.java @@ -0,0 +1,73 @@ +package com.ws.taskmanager.config.security; + +import com.ws.taskmanager.security.CustomAccessDeniedHandler; +import com.ws.taskmanager.security.JwtAuthenticationEntrypoint; +import com.ws.taskmanager.security.JwtAuthenticationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +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.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + private final UserDetailsService userDetailsService; + private final JwtAuthenticationEntrypoint authenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private JwtAuthenticationFilter authenticationFilter; + + public SecurityConfig(JwtAuthenticationEntrypoint authenticationEntryPoint, + UserDetailsService userDetailsService, + JwtAuthenticationFilter authenticationFilter, + CustomAccessDeniedHandler customAccessDeniedHandler) { + this.authenticationEntryPoint = authenticationEntryPoint; + this.userDetailsService = userDetailsService; + this.authenticationFilter = authenticationFilter; + this.customAccessDeniedHandler = customAccessDeniedHandler; + } + + @Bean + public static PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + http.csrf().disable() + .authorizeHttpRequests(authorize -> + authorize + .requestMatchers("/api/v1/auth/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .permitAll() + .anyRequest() + .authenticated() + ) + .exceptionHandling(exception -> + exception + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ) + .sessionManagement(session -> + session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ); + + http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/com/ws/taskmanager/configs/swagger/OpenApiConfig.java b/src/main/java/com/ws/taskmanager/config/swagger/OpenApiConfig.java similarity index 86% rename from src/main/java/com/ws/taskmanager/configs/swagger/OpenApiConfig.java rename to src/main/java/com/ws/taskmanager/config/swagger/OpenApiConfig.java index 68ad851..2a24feb 100644 --- a/src/main/java/com/ws/taskmanager/configs/swagger/OpenApiConfig.java +++ b/src/main/java/com/ws/taskmanager/config/swagger/OpenApiConfig.java @@ -1,11 +1,9 @@ -package com.ws.taskmanager.configs.swagger; +package com.ws.taskmanager.config.swagger; import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; -import org.springdoc.core.properties.SwaggerUiConfigProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/ws/taskmanager/controller/AuthController.java b/src/main/java/com/ws/taskmanager/controller/AuthController.java new file mode 100644 index 0000000..0994576 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/controller/AuthController.java @@ -0,0 +1,33 @@ +package com.ws.taskmanager.controller; + +import com.ws.taskmanager.data.DTO.JWTAuthResponseDto; +import com.ws.taskmanager.data.DTO.LoginDto; +import com.ws.taskmanager.data.DTO.RegisterDto; +import com.ws.taskmanager.data.DTO.RegisterResponseDto; +import com.ws.taskmanager.services.AuthService; +import org.springframework.http.HttpStatus; +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; + +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping(value = {"/login", "/signin"}) + public ResponseEntity login(@RequestBody LoginDto loginDto) { + return ResponseEntity.ok(authService.login(loginDto)); + } + + @PostMapping(value = {"/register", "/signup"}) + public ResponseEntity register(@RequestBody RegisterDto registerDto) { + return ResponseEntity.status(HttpStatus.CREATED).body(authService.register(registerDto)); + } +} diff --git a/src/main/java/com/ws/taskmanager/controller/TaskController.java b/src/main/java/com/ws/taskmanager/controller/TaskController.java index 4816650..1b9daa3 100644 --- a/src/main/java/com/ws/taskmanager/controller/TaskController.java +++ b/src/main/java/com/ws/taskmanager/controller/TaskController.java @@ -1,11 +1,12 @@ package com.ws.taskmanager.controller; -import com.ws.taskmanager.data.DTO.TaskCreateDTO; -import com.ws.taskmanager.data.DTO.TaskDTO; -import com.ws.taskmanager.data.DTO.TaskPatchDTO; -import com.ws.taskmanager.data.DTO.TaskResponseDTO; +import com.ws.taskmanager.data.DTO.TaskCreateDto; +import com.ws.taskmanager.data.DTO.TaskDto; +import com.ws.taskmanager.data.DTO.TaskPatchDto; +import com.ws.taskmanager.data.DTO.TaskResponseDto; import com.ws.taskmanager.services.TaskService; +import com.ws.taskmanager.services.impl.TaskServiceImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -28,9 +29,9 @@ @RequestMapping("api/v1/tasks") public class TaskController { - final TaskService taskService; + private final TaskService taskService; - public TaskController(TaskService taskService) { + public TaskController(TaskServiceImpl taskService) { this.taskService = taskService; } @@ -39,7 +40,7 @@ public TaskController(TaskService taskService) { tags = {"Tasks"}, responses = { @ApiResponse(description = "Success", responseCode = "200", - content = @Content(schema = @Schema(implementation = TaskResponseDTO.class)) + content = @Content(schema = @Schema(implementation = TaskResponseDto.class)) ), @ApiResponse(description = "Bad Request", responseCode = "400", content = @Content), @ApiResponse(description = "Unauthorized", responseCode = "401", content = @Content), @@ -47,7 +48,7 @@ public TaskController(TaskService taskService) { } ) @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createTask(@RequestBody @Valid TaskCreateDTO taskDTO) + public ResponseEntity createTask(@RequestBody @Valid TaskCreateDto taskDTO) throws Exception { return ResponseEntity.status(HttpStatus.CREATED).body(taskService.createTask(taskDTO)); } @@ -61,7 +62,7 @@ public ResponseEntity createTask(@RequestBody @Valid TaskCreate content = { @Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = TaskResponseDTO.class)) + array = @ArraySchema(schema = @Schema(implementation = TaskResponseDto.class)) ) }), @ApiResponse(description = "Bad Request", responseCode = "400", content = @Content), @@ -70,7 +71,7 @@ public ResponseEntity createTask(@RequestBody @Valid TaskCreate @ApiResponse(description = "Internal Error", responseCode = "500", content = @Content), } ) - public ResponseEntity> listAllTasks(@RequestParam(value = "page", defaultValue = "0") Integer page, + public ResponseEntity> listAllTasks(@RequestParam(value = "page", defaultValue = "0") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @RequestParam(value = "direction", defaultValue = "asc") String direction) { @@ -86,7 +87,7 @@ public ResponseEntity> listAllTasks(@RequestParam(value = tags = {"Tasks"}, responses = { @ApiResponse(description = "Success", responseCode = "200", - content = @Content(schema = @Schema(implementation = TaskResponseDTO.class)) + content = @Content(schema = @Schema(implementation = TaskResponseDto.class)) ), @ApiResponse(description = "No Content", responseCode = "204", content = @Content), @ApiResponse(description = "Bad Request", responseCode = "400", content = @Content), @@ -95,7 +96,7 @@ public ResponseEntity> listAllTasks(@RequestParam(value = @ApiResponse(description = "Internal Error", responseCode = "500", content = @Content), } ) - public ResponseEntity listTaskById(@PathVariable(value = "id") UUID id) throws Exception { + public ResponseEntity listTaskById(@PathVariable(value = "id") UUID id) throws Exception { return ResponseEntity.status(HttpStatus.OK).body(taskService.listTaskById(id)); } @@ -105,7 +106,7 @@ public ResponseEntity listTaskById(@PathVariable(value = "id") tags = {"Tasks"}, responses = { @ApiResponse(description = "Updated", responseCode = "200", - content = @Content(schema = @Schema(implementation = TaskResponseDTO.class)) + content = @Content(schema = @Schema(implementation = TaskResponseDto.class)) ), @ApiResponse(description = "Bad Request", responseCode = "400", content = @Content), @ApiResponse(description = "Unauthorized", responseCode = "401", content = @Content), @@ -113,7 +114,7 @@ public ResponseEntity listTaskById(@PathVariable(value = "id") @ApiResponse(description = "Internal Error", responseCode = "500", content = @Content), } ) - public ResponseEntity updateTask(@PathVariable(value = "id") UUID id, @Valid @RequestBody TaskDTO taskDTO) + public ResponseEntity updateTask(@PathVariable(value = "id") UUID id, @Valid @RequestBody TaskDto taskDTO) throws Exception { return ResponseEntity.status(HttpStatus.OK).body(taskService.updateTask(id, taskDTO)); } @@ -124,7 +125,7 @@ public ResponseEntity updateTask(@PathVariable(value = "id") UU tags = {"Tasks"}, responses = { @ApiResponse(description = "Updated", responseCode = "200", - content = @Content(schema = @Schema(implementation = TaskResponseDTO.class)) + content = @Content(schema = @Schema(implementation = TaskResponseDto.class)) ), @ApiResponse(description = "Bad Request", responseCode = "400", content = @Content), @ApiResponse(description = "Unauthorized", responseCode = "401", content = @Content), @@ -132,7 +133,7 @@ public ResponseEntity updateTask(@PathVariable(value = "id") UU @ApiResponse(description = "Internal Error", responseCode = "500", content = @Content), } ) - public ResponseEntity updateTaskSituation(@PathVariable(value = "id") UUID id, @Valid @RequestBody TaskPatchDTO taskPatchDTO) + public ResponseEntity updateTaskSituation(@PathVariable(value = "id") UUID id, @Valid @RequestBody TaskPatchDto taskPatchDTO) throws Exception { return ResponseEntity.status(HttpStatus.OK).body(taskService.updateTaskSituation(id, taskPatchDTO)); } diff --git a/src/main/java/com/ws/taskmanager/data/DTO/JWTAuthResponseDto.java b/src/main/java/com/ws/taskmanager/data/DTO/JWTAuthResponseDto.java new file mode 100644 index 0000000..77e0a7f --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/JWTAuthResponseDto.java @@ -0,0 +1,41 @@ +package com.ws.taskmanager.data.DTO; + + +public class JWTAuthResponseDto { + private String accessToken; + private String tokenType = "Bearer"; + private UserDto user; + + public JWTAuthResponseDto() { + } + + public JWTAuthResponseDto(String accessToken, String tokenType, UserDto user) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.user = user; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public UserDto getUser() { + return user; + } + + public void setUser(UserDto user) { + this.user = user; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/LoginDto.java b/src/main/java/com/ws/taskmanager/data/DTO/LoginDto.java new file mode 100644 index 0000000..fe90d55 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/LoginDto.java @@ -0,0 +1,30 @@ +package com.ws.taskmanager.data.DTO; + +public class LoginDto { + private String usernameOrEmail; + private String password; + + public LoginDto() { + } + + public LoginDto(String usernameOrEmail, String password) { + this.usernameOrEmail = usernameOrEmail; + this.password = password; + } + + public String getUsernameOrEmail() { + return usernameOrEmail; + } + + public void setUsernameOrEmail(String usernameOrEmail) { + this.usernameOrEmail = usernameOrEmail; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/RegisterDto.java b/src/main/java/com/ws/taskmanager/data/DTO/RegisterDto.java new file mode 100644 index 0000000..93c4672 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/RegisterDto.java @@ -0,0 +1,50 @@ +package com.ws.taskmanager.data.DTO; + +public class RegisterDto { + private String name; + private String username; + private String email; + private String password; + + public RegisterDto() { + } + + public RegisterDto(String name, String username, String email, String password) { + this.name = name; + this.username = username; + this.email = email; + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/RegisterResponseDto.java b/src/main/java/com/ws/taskmanager/data/DTO/RegisterResponseDto.java new file mode 100644 index 0000000..ece01a2 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/RegisterResponseDto.java @@ -0,0 +1,50 @@ +package com.ws.taskmanager.data.DTO; + +public class RegisterResponseDto { + private Long id; + private String name; + private String username; + private String email; + + public RegisterResponseDto() { + } + + public RegisterResponseDto(Long id, String name, String username, String email) { + this.id = id; + this.name = name; + this.username = username; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDTO.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDto.java similarity index 89% rename from src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDTO.java rename to src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDto.java index 07b0c20..3cd1282 100644 --- a/src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDTO.java +++ b/src/main/java/com/ws/taskmanager/data/DTO/TaskCreateDto.java @@ -7,7 +7,7 @@ import java.io.Serializable; import java.time.LocalDateTime; -public class TaskCreateDTO extends RepresentationModel implements Serializable { +public class TaskCreateDto extends RepresentationModel implements Serializable { @NotBlank(message = "A task é obrigatória!") private String task; @@ -37,7 +37,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; - TaskCreateDTO that = (TaskCreateDTO) o; + TaskCreateDto that = (TaskCreateDto) o; if (!task.equals(that.task)) return false; return deadline.equals(that.deadline); diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskDTO.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskDto.java similarity index 94% rename from src/main/java/com/ws/taskmanager/data/DTO/TaskDTO.java rename to src/main/java/com/ws/taskmanager/data/DTO/TaskDto.java index a2919ae..566c676 100644 --- a/src/main/java/com/ws/taskmanager/data/DTO/TaskDTO.java +++ b/src/main/java/com/ws/taskmanager/data/DTO/TaskDto.java @@ -3,18 +3,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.github.dozermapper.core.Mapping; -import jakarta.persistence.Id; import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import org.springframework.hateoas.RepresentationModel; import java.io.Serializable; import java.time.LocalDateTime; import java.util.UUID; -import org.springframework.hateoas.RepresentationModel; @JsonPropertyOrder({"id", "task", "concluded", "deadline", "createdAt"}) -public class TaskDTO extends RepresentationModel implements Serializable { +public class TaskDto extends RepresentationModel implements Serializable { @JsonProperty("id") @Mapping("id") @@ -73,7 +72,7 @@ public boolean equals(Object o) { return false; } - TaskDTO taskDTO = (TaskDTO) o; + TaskDto taskDTO = (TaskDto) o; if (!getKey().equals(taskDTO.getKey())) { return false; diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDTO.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDTO.java deleted file mode 100644 index 3e1588f..0000000 --- a/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDTO.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.ws.taskmanager.data.DTO; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.dozermapper.core.Mapping; -import jakarta.validation.constraints.NotNull; -import java.io.Serializable; -import java.util.UUID; -import org.springframework.hateoas.RepresentationModel; -import org.yaml.snakeyaml.representer.Represent; - -public class TaskPatchDTO extends RepresentationModel implements Serializable { - - @JsonIgnore() - @Mapping("id") - private UUID key; - - @NotNull(message = "A situação da task é obrigatória!") - private Boolean concluded; - - public UUID getKey() { - return key; - } - - public void setKey(UUID key) { - this.key = key; - } - - public Boolean getConcluded() { - return concluded; - } - - public void setConcluded(Boolean concluded) { - this.concluded = concluded; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - - TaskPatchDTO that = (TaskPatchDTO) o; - - if (!getKey().equals(that.getKey())) { - return false; - } - return getConcluded().equals(that.getConcluded()); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + getKey().hashCode(); - result = 31 * result + getConcluded().hashCode(); - return result; - } -} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDto.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDto.java new file mode 100644 index 0000000..bc23f40 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/TaskPatchDto.java @@ -0,0 +1,63 @@ +package com.ws.taskmanager.data.DTO; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.github.dozermapper.core.Mapping; +import jakarta.validation.constraints.NotNull; +import org.springframework.hateoas.RepresentationModel; + +import java.io.Serializable; +import java.util.UUID; + +public class TaskPatchDto extends RepresentationModel implements Serializable { + + @JsonIgnore() + @Mapping("id") + private UUID key; + + @NotNull(message = "A situação da task é obrigatória!") + private Boolean concluded; + + public UUID getKey() { + return key; + } + + public void setKey(UUID key) { + this.key = key; + } + + public Boolean getConcluded() { + return concluded; + } + + public void setConcluded(Boolean concluded) { + this.concluded = concluded; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + TaskPatchDto that = (TaskPatchDto) o; + + if (!getKey().equals(that.getKey())) { + return false; + } + return getConcluded().equals(that.getConcluded()); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + getKey().hashCode(); + result = 31 * result + getConcluded().hashCode(); + return result; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDTO.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDTO.java deleted file mode 100644 index 059bcd2..0000000 --- a/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDTO.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.ws.taskmanager.data.DTO; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.github.dozermapper.core.Mapping; -import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.UUID; -import org.springframework.hateoas.RepresentationModel; - -@JsonPropertyOrder({"id", "task", "concluded", "deadline", "createdAt"}) -public class TaskResponseDTO extends RepresentationModel implements Serializable { - - @JsonProperty("id") - @Mapping("id") - private UUID key; - - @NotBlank(message = "A task é obrigatória!") - private String task; - - @FutureOrPresent(message = "O prazo deve ser a data atual ou uma data futura.") - private LocalDateTime deadline; - - @NotNull(message = "A situação da task é obrigatória!") - private Boolean concluded; - - private LocalDateTime createdAt; - - public UUID getKey() { - return key; - } - - public void setKey(UUID key) { - this.key = key; - } - - public String getTask() { - return task; - } - - public void setTask(String task) { - this.task = task; - } - - public LocalDateTime getDeadline() { - return deadline; - } - - public void setDeadline(LocalDateTime deadline) { - this.deadline = deadline; - } - - public Boolean getConcluded() { - return concluded; - } - - public void setConcluded(Boolean concluded) { - this.concluded = concluded; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - - TaskResponseDTO that = (TaskResponseDTO) o; - - if (!getKey().equals(that.getKey())) { - return false; - } - if (!getTask().equals(that.getTask())) { - return false; - } - if (!getDeadline().equals(that.getDeadline())) { - return false; - } - if (!getConcluded().equals(that.getConcluded())) { - return false; - } - return getCreatedAt().equals(that.getCreatedAt()); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + getKey().hashCode(); - result = 31 * result + getTask().hashCode(); - result = 31 * result + getDeadline().hashCode(); - result = 31 * result + getConcluded().hashCode(); - result = 31 * result + getCreatedAt().hashCode(); - return result; - } -} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDto.java b/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDto.java new file mode 100644 index 0000000..8b11fca --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/TaskResponseDto.java @@ -0,0 +1,112 @@ +package com.ws.taskmanager.data.DTO; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.github.dozermapper.core.Mapping; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.hateoas.RepresentationModel; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@JsonPropertyOrder({"id", "task", "concluded", "deadline", "createdAt"}) +public class TaskResponseDto extends RepresentationModel implements Serializable { + + @JsonProperty("id") + @Mapping("id") + private UUID key; + + @NotBlank(message = "A task é obrigatória!") + private String task; + + @FutureOrPresent(message = "O prazo deve ser a data atual ou uma data futura.") + private LocalDateTime deadline; + + @NotNull(message = "A situação da task é obrigatória!") + private Boolean concluded; + + private LocalDateTime createdAt; + + public UUID getKey() { + return key; + } + + public void setKey(UUID key) { + this.key = key; + } + + public String getTask() { + return task; + } + + public void setTask(String task) { + this.task = task; + } + + public LocalDateTime getDeadline() { + return deadline; + } + + public void setDeadline(LocalDateTime deadline) { + this.deadline = deadline; + } + + public Boolean getConcluded() { + return concluded; + } + + public void setConcluded(Boolean concluded) { + this.concluded = concluded; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + TaskResponseDto that = (TaskResponseDto) o; + + if (!getKey().equals(that.getKey())) { + return false; + } + if (!getTask().equals(that.getTask())) { + return false; + } + if (!getDeadline().equals(that.getDeadline())) { + return false; + } + if (!getConcluded().equals(that.getConcluded())) { + return false; + } + return getCreatedAt().equals(that.getCreatedAt()); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + getKey().hashCode(); + result = 31 * result + getTask().hashCode(); + result = 31 * result + getDeadline().hashCode(); + result = 31 * result + getConcluded().hashCode(); + result = 31 * result + getCreatedAt().hashCode(); + return result; + } +} diff --git a/src/main/java/com/ws/taskmanager/data/DTO/UserDto.java b/src/main/java/com/ws/taskmanager/data/DTO/UserDto.java new file mode 100644 index 0000000..57b1601 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/data/DTO/UserDto.java @@ -0,0 +1,50 @@ +package com.ws.taskmanager.data.DTO; + +public class UserDto { + private Long id; + private String name; + private String username; + private String email; + + public UserDto() { + } + + public UserDto(Long id, String name, String username, String email) { + this.id = id; + this.name = name; + this.username = username; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/ws/taskmanager/exceptions/handler/CustomizedResponseEntityExceptionHandler.java b/src/main/java/com/ws/taskmanager/exceptions/handler/CustomizedResponseEntityExceptionHandler.java index 2f6b06a..f610438 100644 --- a/src/main/java/com/ws/taskmanager/exceptions/handler/CustomizedResponseEntityExceptionHandler.java +++ b/src/main/java/com/ws/taskmanager/exceptions/handler/CustomizedResponseEntityExceptionHandler.java @@ -3,11 +3,11 @@ import com.ws.taskmanager.exceptions.BadRequestException; import com.ws.taskmanager.exceptions.ResourceNotFoundException; import com.ws.taskmanager.exceptions.model.ExceptionResponse; -import java.util.Objects; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -15,7 +15,9 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import java.util.Arrays; import java.util.Date; +import java.util.Objects; @ControllerAdvice @RestController @@ -42,13 +44,19 @@ public final ResponseEntity handleBadRequestException(BadRequ ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false)); return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND); + } + @Override + public final ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), "Metódo não suportado para o recurso: " + ex.getMethod() + ". Os metodos suportados pelo recurso são: " + + Arrays.toString(ex.getSupportedMethods()), request.getDescription(false)); + return new ResponseEntity<>(exceptionResponse, HttpStatus.METHOD_NOT_ALLOWED); } @Override protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), Objects.requireNonNull( - ex.getFieldError()).getDefaultMessage(), request.getDescription(false)); + ex.getFieldError()).getDefaultMessage(), request.getDescription(false)); return new ResponseEntity<>(exceptionResponse, HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/com/ws/taskmanager/models/RoleModel.java b/src/main/java/com/ws/taskmanager/models/RoleModel.java new file mode 100644 index 0000000..4a221a6 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/models/RoleModel.java @@ -0,0 +1,36 @@ +package com.ws.taskmanager.models; + +import jakarta.persistence.*; + +@Entity +@Table(name = "roles") +public class RoleModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + public RoleModel() { + } + + public RoleModel(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/ws/taskmanager/models/TaskModel.java b/src/main/java/com/ws/taskmanager/models/TaskModel.java index 006e134..fe199e5 100644 --- a/src/main/java/com/ws/taskmanager/models/TaskModel.java +++ b/src/main/java/com/ws/taskmanager/models/TaskModel.java @@ -12,7 +12,7 @@ @Entity @Table(name = "_task") -public class TaskModel implements Serializable { +public class TaskModel implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/src/main/java/com/ws/taskmanager/models/UserModel.java b/src/main/java/com/ws/taskmanager/models/UserModel.java new file mode 100644 index 0000000..b1877f5 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/models/UserModel.java @@ -0,0 +1,99 @@ +package com.ws.taskmanager.models; + +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +@Table(name = "users") +public class UserModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Column(nullable = false, unique = true) + private String username; + + @Column(nullable = false, unique = true) + private String email; + + private String password; + + // used Set to avoid duplicates roles + // FetchType.EAGER -> means that when user is loaded, his role will be retrieved too + // CascadeType.ALL -> means that when user is saved, his rle will be saved too + @ManyToMany( + fetch = FetchType.EAGER, + cascade = CascadeType.ALL + ) + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id") + ) + private Set roles; + + public UserModel() { + + } + + public UserModel(Long id, String name, String username, String email, String password, Set roles) { + this.id = id; + this.name = name; + this.username = username; + this.email = email; + this.password = password; + this.roles = roles; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} diff --git a/src/main/java/com/ws/taskmanager/repositories/RoleRepository.java b/src/main/java/com/ws/taskmanager/repositories/RoleRepository.java new file mode 100644 index 0000000..adef052 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/repositories/RoleRepository.java @@ -0,0 +1,10 @@ +package com.ws.taskmanager.repositories; + +import com.ws.taskmanager.models.RoleModel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/ws/taskmanager/repositories/UserRepository.java b/src/main/java/com/ws/taskmanager/repositories/UserRepository.java new file mode 100644 index 0000000..613ba49 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/repositories/UserRepository.java @@ -0,0 +1,19 @@ +package com.ws.taskmanager.repositories; + +import com.ws.taskmanager.models.UserModel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); + + Optional findByUsernameOrEmail(String username, String email); + + Optional findByUsername(String username); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); +} diff --git a/src/main/java/com/ws/taskmanager/security/CustomAccessDeniedHandler.java b/src/main/java/com/ws/taskmanager/security/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..d98948e --- /dev/null +++ b/src/main/java/com/ws/taskmanager/security/CustomAccessDeniedHandler.java @@ -0,0 +1,40 @@ +package com.ws.taskmanager.security; + +import com.ws.taskmanager.common.DateUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Date; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse res, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + + res.setContentType("application/json;charset=UTF-8"); + res.setStatus(HttpServletResponse.SC_FORBIDDEN); + + // Create response content + JSONObject obj = new JSONObject(); + + try { + obj.accumulate("timestamp", DateUtils.convertDateToString(new Date())); + obj.accumulate("status", HttpServletResponse.SC_FORBIDDEN); + obj.accumulate("title", "Acesso Negado!"); + obj.accumulate("message", "Você não possui autorização para acessar esse recurso!"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + res.getWriter().write(obj.toString()); + } +} diff --git a/src/main/java/com/ws/taskmanager/security/CustomUserDetailsService.java b/src/main/java/com/ws/taskmanager/security/CustomUserDetailsService.java new file mode 100644 index 0000000..4772109 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/security/CustomUserDetailsService.java @@ -0,0 +1,43 @@ +package com.ws.taskmanager.security; + +import com.ws.taskmanager.exceptions.ResourceNotFoundException; +import com.ws.taskmanager.repositories.UserRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { + + var user = userRepository + .findByUsernameOrEmail(usernameOrEmail, usernameOrEmail) + .orElseThrow( + () -> new ResourceNotFoundException("Usuário não encontrado com os dados informados: " + usernameOrEmail) + ); + + // Convert a set of roles into a set of SimpleGrantedAuthorities + Set authorities = user + .getRoles() + .stream() + .map(role -> new SimpleGrantedAuthority(role.getName())) + .collect(Collectors.toSet()); + + return new User(user.getEmail(), user.getPassword(), authorities); + } +} diff --git a/src/main/java/com/ws/taskmanager/security/JwtAuthenticationEntrypoint.java b/src/main/java/com/ws/taskmanager/security/JwtAuthenticationEntrypoint.java new file mode 100644 index 0000000..fa18b76 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/security/JwtAuthenticationEntrypoint.java @@ -0,0 +1,39 @@ +package com.ws.taskmanager.security; + +import com.ws.taskmanager.common.DateUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Date; + +@Component +public class JwtAuthenticationEntrypoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse res, + AuthenticationException authException) throws IOException, ServletException { + res.setContentType("application/json;charset=UTF-8"); + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + // Create response content + JSONObject obj = new JSONObject(); + + try { + obj.accumulate("timestamp", DateUtils.convertDateToString(new Date())); + obj.accumulate("status", HttpServletResponse.SC_UNAUTHORIZED); + obj.accumulate("title", "Acesso Negado!"); + obj.accumulate("message", "Você não possui autorização para acessar esse recurso!"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + res.getWriter().write(obj.toString()); + } +} diff --git a/src/main/java/com/ws/taskmanager/security/JwtAuthenticationFilter.java b/src/main/java/com/ws/taskmanager/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9487c64 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/security/JwtAuthenticationFilter.java @@ -0,0 +1,67 @@ +package com.ws.taskmanager.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private JwtTokenProvider jwtTokenProvider; + private UserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, + UserDetailsService userDetailsService) { + this.jwtTokenProvider = jwtTokenProvider; + this.userDetailsService = userDetailsService; + } + + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // get jwt from http request + String token = getTokenFromRequest(request); + + // validate token + if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + // get username from token + String username = jwtTokenProvider.getUsernameFromToken(token); + + // get user details + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + + filterChain.doFilter(request, response); + } + + private String getTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/ws/taskmanager/security/JwtTokenProvider.java b/src/main/java/com/ws/taskmanager/security/JwtTokenProvider.java new file mode 100644 index 0000000..94cc277 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/security/JwtTokenProvider.java @@ -0,0 +1,78 @@ +package com.ws.taskmanager.security; + +import com.ws.taskmanager.exceptions.BadRequestException; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + @Value("${app.jwt-secret}") + private String jwtSecret; + + @Value("${app.jwt-expiration-milliseconds}") + private long jwtExpirationDate; + + // generate JWT Token + public String generateToken(Authentication authentication) { + String username = authentication.getName(); + + Date currentDate = new Date(); + Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate); + + return Jwts + .builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(key()) + .compact(); + } + + private Key key() { + return Keys.hmacShaKeyFor( + Decoders.BASE64.decode(jwtSecret) + ); + } + + // get username from jwt token + public String getUsernameFromToken(String token) { + + Claims claims = Jwts + .parserBuilder() + .setSigningKey(key()) + .build() + .parseClaimsJws(token) + .getBody(); + + return claims.getSubject(); + } + + // validate jwt token + public boolean validateToken(String token) { + + try { + Jwts.parserBuilder() + .setSigningKey(key()) + .build() + .parse(token); + + return true; + } catch (MalformedJwtException ex) { + throw new BadRequestException("Token inválido!"); + } catch (ExpiredJwtException ex) { + throw new BadRequestException("Token expirado!"); + } catch (UnsupportedJwtException ex) { + throw new BadRequestException("O tipo do token é inválido!"); + } catch (IllegalArgumentException ex) { + throw new BadRequestException("O token é obrigatório para acessar esse recurso!"); + } + } +} diff --git a/src/main/java/com/ws/taskmanager/services/AuthService.java b/src/main/java/com/ws/taskmanager/services/AuthService.java new file mode 100644 index 0000000..2c6550a --- /dev/null +++ b/src/main/java/com/ws/taskmanager/services/AuthService.java @@ -0,0 +1,12 @@ +package com.ws.taskmanager.services; + +import com.ws.taskmanager.data.DTO.JWTAuthResponseDto; +import com.ws.taskmanager.data.DTO.LoginDto; +import com.ws.taskmanager.data.DTO.RegisterDto; +import com.ws.taskmanager.data.DTO.RegisterResponseDto; + +public interface AuthService { + JWTAuthResponseDto login(LoginDto loginDto); + + RegisterResponseDto register(RegisterDto registerDto); +} diff --git a/src/main/java/com/ws/taskmanager/services/TaskService.java b/src/main/java/com/ws/taskmanager/services/TaskService.java index 912171c..aa467af 100644 --- a/src/main/java/com/ws/taskmanager/services/TaskService.java +++ b/src/main/java/com/ws/taskmanager/services/TaskService.java @@ -1,118 +1,24 @@ package com.ws.taskmanager.services; -import com.ws.taskmanager.controller.TaskController; -import com.ws.taskmanager.data.DTO.TaskCreateDTO; -import com.ws.taskmanager.data.DTO.TaskDTO; -import com.ws.taskmanager.data.DTO.TaskPatchDTO; -import com.ws.taskmanager.data.DTO.TaskResponseDTO; -import com.ws.taskmanager.exceptions.BadRequestException; -import com.ws.taskmanager.exceptions.ResourceNotFoundException; -import com.ws.taskmanager.mapper.DozerMapper; -import com.ws.taskmanager.models.TaskModel; -import com.ws.taskmanager.repositories.TaskRepository; -import jakarta.transaction.Transactional; +import com.ws.taskmanager.data.DTO.TaskCreateDto; +import com.ws.taskmanager.data.DTO.TaskDto; +import com.ws.taskmanager.data.DTO.TaskPatchDto; +import com.ws.taskmanager.data.DTO.TaskResponseDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.chrono.ChronoLocalDateTime; import java.util.UUID; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +public interface TaskService { + TaskResponseDto createTask(TaskCreateDto taskDTO) throws Exception; -@Service -public class TaskService { + Page listAllTasks(Pageable pageable); - final TaskRepository taskRepository; + TaskResponseDto listTaskById(UUID id) throws Exception; - public TaskService(TaskRepository taskRepository) { - this.taskRepository = taskRepository; - } + TaskResponseDto updateTask(UUID id, TaskDto taskDTO) throws Exception; - @Transactional - public TaskResponseDTO createTask(TaskCreateDTO taskDTO) throws Exception { - var task = DozerMapper.parseObject(taskDTO, TaskModel.class); - task.setCreatedAt(LocalDateTime.now(ZoneId.of("UTC"))); - task.setConcluded(false); + void deleteTask(UUID id); - var dto = DozerMapper.parseObject(taskRepository.save(task), TaskResponseDTO.class); - dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); - - return dto; - } - - public Page listAllTasks(Pageable pageable) { - - var tasksPage = taskRepository.findAll(pageable); - var tasksPageDTO = tasksPage.map(entity -> DozerMapper.parseObject(entity, TaskResponseDTO.class)); - - tasksPageDTO.map(task -> { - try { - return task.add(linkTo(methodOn(TaskController.class).listTaskById(task.getKey())).withSelfRel()); - } catch (Exception e) { - throw new ResourceNotFoundException("Ocorreu um erro na listagem de tasks!"); - } - }); - - return tasksPageDTO; - } - - public TaskResponseDTO listTaskById(UUID id) throws Exception { - - var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não foi encontrado uma task com o ID informado!")); - - var task = DozerMapper.parseObject(entity, TaskModel.class); - - var dto = DozerMapper.parseObject(task, TaskResponseDTO.class); - - - dto.add(linkTo(methodOn(TaskController.class).listTaskById(id)).withSelfRel()); - - return dto; - } - - @Transactional - public TaskResponseDTO updateTask(UUID id, TaskDTO taskDTO) throws Exception { - var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não foi encontrado uma task com o ID informado!")); - - entity.setTask(taskDTO.getTask()); - entity.setConcluded(taskDTO.getConcluded()); - entity.setDeadline(taskDTO.getDeadline()); - - var task = DozerMapper.parseObject(taskRepository.save(entity), TaskModel.class); - - var dto = DozerMapper.parseObject(task, TaskResponseDTO.class); - dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); - - return dto; - } - - @Transactional - public void deleteTask(UUID id) { - var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não é possível deletar essa task pois ela não existe!")); - taskRepository.deleteById(id); - } - - @Transactional - public TaskPatchDTO updateTaskSituation(UUID id, TaskPatchDTO taskPatchDTO) throws Exception { - - var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não é possível deletar essa task pois ela não existe!")); - - if(entity.getDeadline().isBefore(LocalDateTime.now(ZoneId.of("UTC")))) { - throw new BadRequestException("Não é possível atualizar a situaçao da tarefa porque seu prazo já está expirado!"); - } - - entity.setConcluded(taskPatchDTO.getConcluded()); - - var task = DozerMapper.parseObject(taskRepository.save(entity), TaskModel.class); - - var dto = DozerMapper.parseObject(task, TaskPatchDTO.class); - dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); - - return dto; - } + TaskPatchDto updateTaskSituation(UUID id, TaskPatchDto taskPatchDTO) throws Exception; } diff --git a/src/main/java/com/ws/taskmanager/services/impl/AuthServiceImpl.java b/src/main/java/com/ws/taskmanager/services/impl/AuthServiceImpl.java new file mode 100644 index 0000000..4f25eda --- /dev/null +++ b/src/main/java/com/ws/taskmanager/services/impl/AuthServiceImpl.java @@ -0,0 +1,113 @@ +package com.ws.taskmanager.services.impl; + +import com.ws.taskmanager.data.DTO.*; +import com.ws.taskmanager.exceptions.BadRequestException; +import com.ws.taskmanager.mapper.DozerMapper; +import com.ws.taskmanager.models.RoleModel; +import com.ws.taskmanager.models.UserModel; +import com.ws.taskmanager.repositories.RoleRepository; +import com.ws.taskmanager.repositories.UserRepository; +import com.ws.taskmanager.security.JwtTokenProvider; +import com.ws.taskmanager.services.AuthService; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +@Service +public class AuthServiceImpl implements AuthService { + + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + + private final JwtTokenProvider jwtTokenProvider; + + public AuthServiceImpl(AuthenticationManager authenticationManager, + UserRepository userRepository, + RoleRepository roleRepository, + PasswordEncoder passwordEncoder, + JwtTokenProvider jwtTokenProvider) { + this.authenticationManager = authenticationManager; + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public JWTAuthResponseDto login(LoginDto loginDto) { + Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken( + loginDto.getUsernameOrEmail(), loginDto.getPassword() + )); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + String token = jwtTokenProvider.generateToken(authentication); + + var user = userRepository.findByUsernameOrEmail(loginDto.getUsernameOrEmail(), loginDto.getUsernameOrEmail()).get(); + + var userDto = copyPropertiesFromUserToUserDto(user); + + JWTAuthResponseDto jwtAuthResponseDto = new JWTAuthResponseDto(); + + jwtAuthResponseDto.setAccessToken(token); + jwtAuthResponseDto.setUser(userDto); + + return jwtAuthResponseDto; + } + + @Override + public RegisterResponseDto register(RegisterDto registerDto) { + // verify if user already registered + if (userRepository.existsByUsername(registerDto.getUsername())) { + throw new BadRequestException("Já existe um usuário cadastrado com o usuário informado"); + } + + // check if email already registered + if (userRepository.existsByEmail(registerDto.getEmail())) { + throw new BadRequestException("Já existe um usuário cadastrado com o email informado"); + } + + + Set roles = new HashSet<>(); + Optional userRole = roleRepository.findByName("ROLE_USER"); + + var user = copyPropertiesFromRegisterDtoToUser(registerDto); + + if (userRole.isPresent()) { + roles.add(userRole.get()); + user.setRoles(roles); + } + + var newUser = userRepository.save(user); + + return DozerMapper.parseObject(newUser, RegisterResponseDto.class); + } + + private UserDto copyPropertiesFromUserToUserDto(UserModel user) { + UserDto userDto = new UserDto(); + userDto.setId(user.getId()); + userDto.setEmail(user.getEmail()); + userDto.setName(user.getName()); + userDto.setUsername(user.getUsername()); + return userDto; + } + + private UserModel copyPropertiesFromRegisterDtoToUser(RegisterDto registerDto) { + UserModel user = new UserModel(); + user.setName(registerDto.getName()); + user.setEmail(registerDto.getEmail()); + user.setUsername(registerDto.getUsername()); + user.setPassword(passwordEncoder.encode(registerDto.getPassword())); + return user; + } +} diff --git a/src/main/java/com/ws/taskmanager/services/impl/TaskServiceImpl.java b/src/main/java/com/ws/taskmanager/services/impl/TaskServiceImpl.java new file mode 100644 index 0000000..42094b6 --- /dev/null +++ b/src/main/java/com/ws/taskmanager/services/impl/TaskServiceImpl.java @@ -0,0 +1,117 @@ +package com.ws.taskmanager.services.impl; + +import com.ws.taskmanager.controller.TaskController; +import com.ws.taskmanager.data.DTO.TaskCreateDto; +import com.ws.taskmanager.data.DTO.TaskDto; +import com.ws.taskmanager.data.DTO.TaskPatchDto; +import com.ws.taskmanager.data.DTO.TaskResponseDto; +import com.ws.taskmanager.exceptions.BadRequestException; +import com.ws.taskmanager.exceptions.ResourceNotFoundException; +import com.ws.taskmanager.mapper.DozerMapper; +import com.ws.taskmanager.models.TaskModel; +import com.ws.taskmanager.repositories.TaskRepository; +import com.ws.taskmanager.services.TaskService; +import jakarta.transaction.Transactional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.UUID; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@Service +public class TaskServiceImpl implements TaskService { + + final TaskRepository taskRepository; + + public TaskServiceImpl(TaskRepository taskRepository) { + this.taskRepository = taskRepository; + } + + @Transactional + public TaskResponseDto createTask(TaskCreateDto taskDTO) throws Exception { + var task = DozerMapper.parseObject(taskDTO, TaskModel.class); + task.setCreatedAt(LocalDateTime.now(ZoneId.of("UTC"))); + task.setConcluded(false); + + var dto = DozerMapper.parseObject(taskRepository.save(task), TaskResponseDto.class); + dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); + + return dto; + } + + public Page listAllTasks(Pageable pageable) { + + var tasksPage = taskRepository.findAll(pageable); + var tasksPageDTO = tasksPage.map(entity -> DozerMapper.parseObject(entity, TaskResponseDto.class)); + + tasksPageDTO.map(task -> { + try { + return task.add(linkTo(methodOn(TaskController.class).listTaskById(task.getKey())).withSelfRel()); + } catch (Exception e) { + throw new ResourceNotFoundException("Ocorreu um erro na listagem de tasks!"); + } + }); + + return tasksPageDTO; + } + + public TaskResponseDto listTaskById(UUID id) throws Exception { + + var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não foi encontrado uma task com o ID informado!")); + + var task = DozerMapper.parseObject(entity, TaskModel.class); + + var dto = DozerMapper.parseObject(task, TaskResponseDto.class); + + + dto.add(linkTo(methodOn(TaskController.class).listTaskById(id)).withSelfRel()); + + return dto; + } + + @Transactional + public TaskResponseDto updateTask(UUID id, TaskDto taskDTO) throws Exception { + var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não foi encontrado uma task com o ID informado!")); + + entity.setTask(taskDTO.getTask()); + entity.setConcluded(taskDTO.getConcluded()); + entity.setDeadline(taskDTO.getDeadline()); + + var task = DozerMapper.parseObject(taskRepository.save(entity), TaskModel.class); + + var dto = DozerMapper.parseObject(task, TaskResponseDto.class); + dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); + + return dto; + } + + @Transactional + public void deleteTask(UUID id) { + var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não é possível deletar essa task pois ela não existe!")); + taskRepository.deleteById(id); + } + + @Transactional + public TaskPatchDto updateTaskSituation(UUID id, TaskPatchDto taskPatchDTO) throws Exception { + + var entity = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Não é possível deletar essa task pois ela não existe!")); + + if (entity.getDeadline().isBefore(LocalDateTime.now(ZoneId.of("UTC")))) { + throw new BadRequestException("Não é possível atualizar a situaçao da tarefa porque seu prazo já está expirado!"); + } + + entity.setConcluded(taskPatchDTO.getConcluded()); + + var task = DozerMapper.parseObject(taskRepository.save(entity), TaskModel.class); + + var dto = DozerMapper.parseObject(task, TaskPatchDto.class); + dto.add(linkTo(methodOn(TaskController.class).listTaskById(dto.getKey())).withSelfRel()); + + return dto; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7cedaac..fbe74b0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,11 @@ spring: datasource: - url: jdbc:mysql://root:o2PThfSoQgYMvBTDAjYM@containers-us-west-194.railway.app:6058/railway?useTimezone=true&serverTimezone=UTC + url: jdbc:mysql://localhost:3306/taskmanager + # url: jdbc:mysql://root:o2PThfSoQgYMvBTDAjYM@containers-us-west-194.railway.app:6058/railway?useTimezone=true&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root - password: o2PThfSoQgYMvBTDAjYM + password: admin + # password: o2PThfSoQgYMvBTDAjYM jpa: hibernate: ddl-auto: update @@ -13,9 +15,11 @@ spring: format_sql: true time_zone: UTC database: MYSQL - database-platform: org.hibernate.dialect.MySQL8Dialect + database-platform: org.hibernate.dialect.MySQLDialect spring-doc: swagger-ui: operations-sorter: alpha tags-sorter: alpha - +app: + jwt-secret: 781dcdf14b25e5191fe614744275d51b861c39d40cf8fcb4a6bebc310ca2a6f5 + jwt-expiration-milliseconds: 604800000 diff --git a/src/main/resources/db/migration/V1__Create_Table_Tasks.sql b/src/main/resources/db/migration/V1__Create_Table_Tasks.sql new file mode 100644 index 0000000..8ca5172 --- /dev/null +++ b/src/main/resources/db/migration/V1__Create_Table_Tasks.sql @@ -0,0 +1,8 @@ +create table _task ( + id binary(16) not null, + concluded bit not null, + created_at datetime(6), + deadline datetime(6) not null, + task varchar(255) not null, + primary key (id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__Create_Table_Roles.sql b/src/main/resources/db/migration/V2__Create_Table_Roles.sql new file mode 100644 index 0000000..57f5ff7 --- /dev/null +++ b/src/main/resources/db/migration/V2__Create_Table_Roles.sql @@ -0,0 +1,5 @@ + create table roles ( + id bigint not null auto_increment, + name varchar(255), + primary key (id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__Create_Table_Users.sql b/src/main/resources/db/migration/V3__Create_Table_Users.sql new file mode 100644 index 0000000..8593b94 --- /dev/null +++ b/src/main/resources/db/migration/V3__Create_Table_Users.sql @@ -0,0 +1,8 @@ +create table users ( + id bigint not null auto_increment, + email varchar(255) not null, + name varchar(255), + password varchar(255), + username varchar(255) not null, + primary key (id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V4__Create_Table_Users_Roles.sql b/src/main/resources/db/migration/V4__Create_Table_Users_Roles.sql new file mode 100644 index 0000000..6fb5cb6 --- /dev/null +++ b/src/main/resources/db/migration/V4__Create_Table_Users_Roles.sql @@ -0,0 +1,5 @@ +create table users_roles ( + user_id bigint not null, + role_id bigint not null, + primary key (user_id, role_id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V5_Alter_Table_Users_Roles.sql b/src/main/resources/db/migration/V5_Alter_Table_Users_Roles.sql new file mode 100644 index 0000000..cc4a9c2 --- /dev/null +++ b/src/main/resources/db/migration/V5_Alter_Table_Users_Roles.sql @@ -0,0 +1,5 @@ +alter table users_roles + foreign key (role_id); + references roles (id); + foreign key (user_id); + references users (id); \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__Insert_User_Admin.sql b/src/main/resources/db/migration/V6__Insert_User_Admin.sql new file mode 100644 index 0000000..f6a79a7 --- /dev/null +++ b/src/main/resources/db/migration/V6__Insert_User_Admin.sql @@ -0,0 +1 @@ +INSERT INTO `taskmanager`.`users` (`id`, `email`, `name`, `password`, `username`) VALUES ('1', 'admin@taskmanager.com', 'admin', '$2a$10$XusUscub9x0URSDZT4TMFOzSVhqJTHhyitQ2AmqpXZmtT18ZlKX8i', 'Admin'); diff --git a/src/main/resources/db/migration/V7__Insert_Roles.sql b/src/main/resources/db/migration/V7__Insert_Roles.sql new file mode 100644 index 0000000..b40c86c --- /dev/null +++ b/src/main/resources/db/migration/V7__Insert_Roles.sql @@ -0,0 +1 @@ +INSERT INTO `taskmanager`.`roles` (`id`, `name`) VALUES ('1', 'ROLE_ADMIN'), ('2', 'ROLE_USER'); diff --git a/src/main/resources/db/migration/V8__Insert_Admin_Role_In_Role_Users.sql b/src/main/resources/db/migration/V8__Insert_Admin_Role_In_Role_Users.sql new file mode 100644 index 0000000..f6b7323 --- /dev/null +++ b/src/main/resources/db/migration/V8__Insert_Admin_Role_In_Role_Users.sql @@ -0,0 +1 @@ +INSERT INTO `taskmanager`.`users_roles` (`user_id`, `role_id`) VALUES ('1', '1');