status(ResponseStatusException exception) {
+ return Responses.fromException(exception);
+ }
+
+ @ResponseStatus(code = UNAUTHORIZED)
+ @ExceptionHandler(AccessDeniedException.class)
+ public ApiError unauthorized(AccessDeniedException exception) {
+ return new ApiError("Not authorized.", UNAUTHORIZED);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/infra/http/Responses.java b/api/src/main/java/com/github/throyer/example/modules/infra/http/Responses.java
new file mode 100644
index 00000000..5bb1cfe7
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/infra/http/Responses.java
@@ -0,0 +1,158 @@
+package com.github.throyer.example.modules.infra.http;
+
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+
+import java.io.IOException;
+import java.net.URI;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.server.ResponseStatusException;
+
+import com.github.throyer.example.modules.shared.errors.ApiError;
+import com.github.throyer.example.modules.shared.utils.JSON;
+import com.github.throyer.example.modules.ssr.toasts.Toasts;
+
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * HTTP Responses.
+ *
+ * Classe util para simplificar a geração
+ * de responses para status codes comuns
+ * utilizando ResponseEntity
.
+ */
+@Log4j2
+public class Responses {
+
+ private Responses() {
+ }
+
+ public static final ResponseEntity forbidden(T body) {
+ return ResponseEntity.status(403).body(body);
+ }
+
+ public static final ResponseEntity forbidden() {
+ return ResponseEntity.status(403).build();
+ }
+
+ public static final ResponseEntity unauthorized(T body) {
+ return ResponseEntity.status(401).body(body);
+ }
+
+ public static final ResponseEntity unauthorized() {
+ return ResponseEntity.status(401).build();
+ }
+
+ public static final ResponseEntity ok(T body) {
+ return ResponseEntity.ok(body);
+ }
+
+ public static final ResponseEntity ok() {
+ return ResponseEntity.ok()
+ .build();
+ }
+
+ public static final ResponseEntity notFound() {
+ return ResponseEntity.notFound()
+ .build();
+ }
+
+ public static final ResponseEntity badRequest(T body) {
+ return ResponseEntity.badRequest()
+ .body(body);
+ }
+
+ public static final ResponseEntity badRequest() {
+ return ResponseEntity.badRequest()
+ .build();
+ }
+
+ public static final ResponseEntity noContent() {
+ return ResponseEntity.noContent().build();
+ }
+
+ public static final ResponseEntity noContent(T entity, CrudRepository repository) {
+ repository.delete(entity);
+ return ResponseEntity
+ .noContent()
+ .build();
+ }
+
+ public static final ResponseEntity created(T body, String location, String id) {
+ return ResponseEntity.created(URI.create(String.format("/%s/%s", location, id)))
+ .body(body);
+ }
+
+ public static final ResponseEntity created(T body) {
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(body);
+ }
+
+ public static final ResponseStatusException forbidden(String reason) {
+ return new ResponseStatusException(HttpStatus.FORBIDDEN, reason);
+ }
+
+ public static final ResponseStatusException unauthorized(String reason) {
+ return new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason);
+ }
+
+ public static final ResponseStatusException notFound(String reason) {
+ return new ResponseStatusException(HttpStatus.NOT_FOUND, reason);
+ }
+
+ public static final ResponseStatusException InternalServerError(String reason) {
+ return new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, reason);
+ }
+
+ public static final ResponseEntity fromException(ResponseStatusException exception) {
+ return ResponseEntity
+ .status(exception.getStatus())
+ .body(new ApiError(exception.getReason(), exception.getStatus()));
+ }
+
+ public static void forbidden(HttpServletResponse response) {
+ if (response.getStatus() != 200) {
+ return;
+ }
+
+ try {
+ response.setStatus(FORBIDDEN.value());
+ response.setContentType("application/json");
+ response.getWriter().write(JSON.stringify(
+ new ApiError("Can't find bearer token on Authorization header.", FORBIDDEN)));
+ } catch (Exception exception) {
+ log.error("error writing forbidden response");
+ }
+ }
+
+ public static void expired(HttpServletResponse response) {
+ if (response.getStatus() != 200) {
+ return;
+ }
+
+ try {
+ response.setStatus(FORBIDDEN.value());
+ response.setContentType("application/json");
+ response.getWriter().write(JSON.stringify(
+ new ApiError("Token expired or invalid.", FORBIDDEN)));
+ } catch (IOException exception) {
+ log.error("error writing token expired/invalid response");
+ }
+ }
+
+ public static final Boolean validateAndUpdateModel(Model model, P props, String propertyName,
+ BindingResult result) {
+ if (result.hasErrors()) {
+ model.addAttribute(propertyName, props);
+ Toasts.add(model, result);
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/infra/http/context/HttpContext.java b/api/src/main/java/com/github/throyer/example/modules/infra/http/context/HttpContext.java
new file mode 100644
index 00000000..4e2b1276
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/infra/http/context/HttpContext.java
@@ -0,0 +1,11 @@
+package com.github.throyer.example.modules.infra.http.context;
+
+import com.github.throyer.example.modules.authentication.services.PublicRoutes;
+
+public class HttpContext {
+ private static final PublicRoutes publicRoutes = new PublicRoutes();
+
+ public static PublicRoutes publicRoutes() {
+ return HttpContext.publicRoutes;
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/infra/middlewares/AuthenticationMiddleware.java b/api/src/main/java/com/github/throyer/example/modules/infra/middlewares/AuthenticationMiddleware.java
new file mode 100644
index 00000000..9f2ae436
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/infra/middlewares/AuthenticationMiddleware.java
@@ -0,0 +1,28 @@
+package com.github.throyer.example.modules.infra.middlewares;
+
+import static com.github.throyer.example.modules.authentication.services.RequestAuthorizer.tryAuthorizeRequest;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@Order(1)
+@Component
+public class AuthenticationMiddleware extends OncePerRequestFilter {
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filter
+ ) throws ServletException, IOException {
+ tryAuthorizeRequest(request, response);
+ filter.doFilter(request, response);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/mail/models/Addressable.java b/api/src/main/java/com/github/throyer/example/modules/mail/models/Addressable.java
new file mode 100755
index 00000000..1e69cd9a
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/mail/models/Addressable.java
@@ -0,0 +1,5 @@
+package com.github.throyer.example.modules.mail.models;
+
+public interface Addressable {
+ String getEmail();
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java b/api/src/main/java/com/github/throyer/example/modules/mail/models/Email.java
old mode 100644
new mode 100755
similarity index 71%
rename from src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java
rename to api/src/main/java/com/github/throyer/example/modules/mail/models/Email.java
index 388a5b7e..707c07aa
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java
+++ b/api/src/main/java/com/github/throyer/example/modules/mail/models/Email.java
@@ -1,4 +1,4 @@
-package com.github.throyer.common.springboot.domain.services.email;
+package com.github.throyer.example.modules.mail.models;
import org.thymeleaf.context.Context;
diff --git a/api/src/main/java/com/github/throyer/example/modules/mail/services/MailService.java b/api/src/main/java/com/github/throyer/example/modules/mail/services/MailService.java
new file mode 100755
index 00000000..b259f673
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/mail/services/MailService.java
@@ -0,0 +1,63 @@
+package com.github.throyer.example.modules.mail.services;
+
+import static com.github.throyer.example.modules.infra.constants.MailConstants.CONTENT_IS_HTML;
+import static com.github.throyer.example.modules.infra.constants.MailConstants.EMAIL_SUCCESSFULLY_SENT_TO;
+import static com.github.throyer.example.modules.infra.constants.MailConstants.ERROR_SENDING_EMAIL_MESSAGE;
+import static com.github.throyer.example.modules.infra.constants.MailConstants.ERROR_SMTP_AUTH;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.MailAuthenticationException;
+import org.springframework.mail.MailException;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+import org.thymeleaf.TemplateEngine;
+
+import com.github.throyer.example.modules.mail.models.Email;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Service
+@Slf4j
+public class MailService {
+
+ private final TemplateEngine engine;
+ private final JavaMailSender sender;
+
+ @Autowired
+ public MailService(TemplateEngine engine, JavaMailSender sender) {
+ this.engine = engine;
+ this.sender = sender;
+ }
+
+ public void send(Email email) {
+ try {
+ var message = createMessage(email);
+ sender.send(message);
+ log.info(EMAIL_SUCCESSFULLY_SENT_TO, email.getDestination());
+ } catch (MailAuthenticationException exception) {
+ log.error(ERROR_SMTP_AUTH);
+ } catch (MessagingException | MailException exception) {
+ log.error(ERROR_SENDING_EMAIL_MESSAGE, exception);
+ throw new ResponseStatusException(INTERNAL_SERVER_ERROR, ERROR_SENDING_EMAIL_MESSAGE);
+ }
+ }
+
+ private MimeMessage createMessage(Email email) throws MessagingException {
+ var message = sender.createMimeMessage();
+ var helper = new MimeMessageHelper(message);
+
+ var html = engine.process(email.getTemplate(), email.getContext());
+
+ helper.setTo(email.getDestination());
+ helper.setSubject(email.getSubject());
+ helper.setText(html, CONTENT_IS_HTML);
+
+ return message;
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/mail/validations/EmailValidations.java b/api/src/main/java/com/github/throyer/example/modules/mail/validations/EmailValidations.java
new file mode 100755
index 00000000..5ce284b3
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/mail/validations/EmailValidations.java
@@ -0,0 +1,43 @@
+package com.github.throyer.example.modules.mail.validations;
+
+import static com.github.throyer.example.modules.infra.constants.MessagesConstants.EMAIL_ALREADY_USED_MESSAGE;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.shared.errors.ValidationError;
+import com.github.throyer.example.modules.shared.exceptions.BadRequestException;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Component
+public class EmailValidations {
+ private static UserRepository repository;
+
+ @Autowired
+ public EmailValidations(UserRepository repository) {
+ EmailValidations.repository = repository;
+ }
+
+ public static void validateEmailUniqueness(Addressable entity) {
+ if (repository.existsByEmail(entity.getEmail())) {
+ throw new BadRequestException(List.of(new ValidationError("email", message(EMAIL_ALREADY_USED_MESSAGE))));
+ }
+ }
+
+ public static void validateEmailUniquenessOnModify(Addressable newEntity, Addressable actualEntity) {
+ var newEmail = newEntity.getEmail();
+ var actualEmail = actualEntity.getEmail();
+
+ var changedEmail = !actualEmail.equals(newEmail);
+
+ var emailAlreadyUsed = repository.existsByEmail(newEmail);
+
+ if (changedEmail && emailAlreadyUsed) {
+ throw new BadRequestException(List.of(new ValidationError("email", message(EMAIL_ALREADY_USED_MESSAGE))));
+ }
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/management/entities/Auditable.java b/api/src/main/java/com/github/throyer/example/modules/management/entities/Auditable.java
new file mode 100755
index 00000000..8b9a5c67
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/management/entities/Auditable.java
@@ -0,0 +1,121 @@
+package com.github.throyer.example.modules.management.entities;
+
+import static java.time.LocalDateTime.now;
+import static java.util.Optional.ofNullable;
+import static javax.persistence.FetchType.LAZY;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+import javax.persistence.Column;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.MappedSuperclass;
+import javax.persistence.PrePersist;
+import javax.persistence.PreUpdate;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.github.throyer.example.modules.authentication.models.Authorized;
+import com.github.throyer.example.modules.management.models.Entity;
+import com.github.throyer.example.modules.users.entities.User;
+
+import lombok.EqualsAndHashCode;
+
+@MappedSuperclass
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public abstract class Auditable implements Entity {
+
+ @Override
+ @EqualsAndHashCode.Include
+ public abstract Long getId();
+
+ @JsonIgnore
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @JsonIgnore
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ @JsonIgnore
+ @Column(name = "deleted_at")
+ private LocalDateTime deletedAt;
+
+ @JoinColumn(name = "created_by")
+ @ManyToOne(optional = true, fetch = LAZY)
+ private User createdBy;
+
+ @JoinColumn(name = "updated_by")
+ @ManyToOne(optional = true, fetch = LAZY)
+ private User updatedBy;
+
+ @JoinColumn(name = "deleted_by")
+ @ManyToOne(optional = true, fetch = LAZY)
+ private User deletedBy;
+
+ @JsonIgnore
+ public Optional getCreatedBy() {
+ return ofNullable(createdBy);
+ }
+
+ @JsonIgnore
+ public Optional getUpdatedBy() {
+ return ofNullable(updatedBy);
+ }
+
+ @JsonIgnore
+ public Optional getDeletedBy() {
+ return ofNullable(deletedBy);
+ }
+
+ @Column(name = "active", nullable = false)
+ private Boolean active = true;
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public LocalDateTime getDeletedAt() {
+ return deletedAt;
+ }
+
+ public void setDeletedAt(LocalDateTime deletedAt) {
+ this.deletedAt = deletedAt;
+ }
+
+ public Boolean isActive() {
+ return this.active;
+ }
+
+ public void setActive(Boolean active) {
+ this.active = active;
+ }
+
+ @PrePersist
+ private void save() {
+ createdAt = now();
+ createdBy = Authorized.current()
+ .map(authorized -> new User(authorized.getId()))
+ .orElse(null);
+ }
+
+ @PreUpdate
+ private void update() {
+ updatedAt = now();
+ updatedBy = Authorized.current()
+ .map(authorized -> new User(authorized.getId()))
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/management/models/Entity.java b/api/src/main/java/com/github/throyer/example/modules/management/models/Entity.java
new file mode 100755
index 00000000..3b2ce699
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/management/models/Entity.java
@@ -0,0 +1,5 @@
+package com.github.throyer.example.modules.management.models;
+
+public interface Entity {
+ Long getId();
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/management/repositories/Queries.java b/api/src/main/java/com/github/throyer/example/modules/management/repositories/Queries.java
new file mode 100755
index 00000000..f620b895
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/management/repositories/Queries.java
@@ -0,0 +1,20 @@
+package com.github.throyer.example.modules.management.repositories;
+
+public class Queries {
+ public static final String DELETE_BY_ID = """
+ UPDATE
+ #{#entityName}
+ SET
+ deleted_at = CURRENT_TIMESTAMP
+ WHERE id = ?1
+ """;
+
+ public static final String DELETE_ALL = """
+ UPDATE
+ #{#entityName}
+ SET
+ deleted_at = CURRENT_TIMESTAMP
+ """;
+
+ public static final String NON_DELETED_CLAUSE = "deleted_at IS NULL";
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/management/repositories/SoftDeleteRepository.java b/api/src/main/java/com/github/throyer/example/modules/management/repositories/SoftDeleteRepository.java
new file mode 100755
index 00000000..80995a34
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/management/repositories/SoftDeleteRepository.java
@@ -0,0 +1,39 @@
+package com.github.throyer.example.modules.management.repositories;
+
+import static com.github.throyer.example.modules.management.repositories.Queries.DELETE_ALL;
+import static com.github.throyer.example.modules.management.repositories.Queries.DELETE_BY_ID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.NoRepositoryBean;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.github.throyer.example.modules.management.entities.Auditable;
+
+@NoRepositoryBean
+public interface SoftDeleteRepository extends JpaRepository {
+ @Override
+ @Modifying
+ @Transactional
+ @Query(DELETE_BY_ID)
+ void deleteById(Long id);
+
+ @Override
+ @Transactional
+ default void delete(T entity) {
+ deleteById(entity.getId());
+ }
+
+ @Override
+ @Transactional
+ default void deleteAll(Iterable extends T> entities) {
+ entities.forEach(entity -> deleteById(entity.getId()));
+ }
+
+ @Override
+ @Modifying
+ @Transactional
+ @Query(DELETE_ALL)
+ void deleteAll();
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/pagination/Page.java b/api/src/main/java/com/github/throyer/example/modules/pagination/Page.java
new file mode 100755
index 00000000..d251a87e
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/pagination/Page.java
@@ -0,0 +1,66 @@
+package com.github.throyer.example.modules.pagination;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+import static com.github.throyer.example.modules.shared.utils.JSON.stringify;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+@Getter
+public class Page {
+ @Schema(required = true)
+ private final Collection extends T> content;
+
+ @Schema(example = "3", required = true)
+ private final Integer page;
+
+ @Schema(example = "10", required = true)
+ private final Integer size;
+
+ @Schema(example = "8", required = true)
+ private final Integer totalPages;
+
+ @Schema(example = "72", required = true)
+ private final Long totalElements;
+
+ public Page(org.springframework.data.domain.Page page) {
+ this.content = page.getContent();
+ this.page = page.getNumber();
+ this.size = page.getSize();
+ this.totalPages = page.getTotalPages();
+ this.totalElements = page.getTotalElements();
+ }
+
+ public Page(Collection extends T> content, Integer page, Integer size, Long count) {
+ this.content = content;
+ this.page = page;
+ this.size = size;
+ this.totalPages = (int) Math.ceil((double) count / size);
+ this.totalElements = count;
+ }
+
+ public static Page of(org.springframework.data.domain.Page page) {
+ return new Page<>(page);
+ }
+
+ public static Page of(Collection content, Integer page, Integer size, Long count) {
+ return new Page(content, page, size, count);
+ }
+
+ public Page map(Function super T, ? extends U> converter) {
+ var content = this.content.stream().map(converter).toList();
+ return new Page(content, this.page, this.size, this.totalElements);
+ }
+
+ public static Page empty() {
+ return new Page<>(List.of(), 0, 0, 0L);
+ }
+
+ @Override
+ public String toString() {
+ return stringify(this);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/pagination/utils/Pagination.java b/api/src/main/java/com/github/throyer/example/modules/pagination/utils/Pagination.java
new file mode 100755
index 00000000..a117f41c
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/pagination/utils/Pagination.java
@@ -0,0 +1,31 @@
+package com.github.throyer.example.modules.pagination.utils;
+
+import org.springframework.data.domain.PageRequest;
+
+import java.util.Optional;
+
+import org.springframework.data.domain.Pageable;
+
+public class Pagination {
+
+ private static final int FIRST_PAGE = 0;
+ private static final int DEFAULT_SIZE = 10;
+
+ private static final int MIN_SIZE = 1;
+ private static final int MAX_SIZE = 500;
+
+ public static Pageable of(Optional page, Optional size) {
+ return Pagination.of(page.orElse(FIRST_PAGE), size.orElse(DEFAULT_SIZE));
+ }
+
+ public static Pageable of(Integer page, Integer size) {
+ if (page < FIRST_PAGE) {
+ page = FIRST_PAGE;
+ }
+
+ if (size < MIN_SIZE || size > MAX_SIZE) {
+ size = DEFAULT_SIZE;
+ }
+ return PageRequest.of(page, size);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesController.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesController.java
new file mode 100755
index 00000000..253d033a
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesController.java
@@ -0,0 +1,62 @@
+package com.github.throyer.example.modules.recoveries.controllers;
+
+import static org.springframework.http.HttpStatus.NO_CONTENT;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryConfirm;
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryRequest;
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryUpdate;
+import com.github.throyer.example.modules.recoveries.services.RecoveryConfirmService;
+import com.github.throyer.example.modules.recoveries.services.RecoveryService;
+import com.github.throyer.example.modules.recoveries.services.RecoveryUpdateService;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@RestController
+@Tag(name = "Password recovery")
+@RequestMapping("/api/recoveries")
+public class RecoveriesController {
+
+ private final RecoveryService recoveryService;
+ private final RecoveryConfirmService confirmService;
+ private final RecoveryUpdateService updateService;
+
+ @Autowired
+ public RecoveriesController(
+ RecoveryService recoveryService,
+ RecoveryConfirmService confirmService,
+ RecoveryUpdateService updateService
+ ) {
+ this.recoveryService = recoveryService;
+ this.confirmService = confirmService;
+ this.updateService = updateService;
+ }
+
+ @PostMapping
+ @ResponseStatus(NO_CONTENT)
+ @Operation(summary = "Starts recovery password process", description = "Sends a email to user with recovery code")
+ public void recovery(@RequestBody RecoveryRequest request) {
+ recoveryService.recovery(request.getEmail());
+ }
+
+ @PostMapping("/confirm")
+ @ResponseStatus(NO_CONTENT)
+ @Operation(summary = "Confirm recovery code")
+ public void confirm(@RequestBody RecoveryConfirm confirm) {
+ confirmService.confirm(confirm.getEmail(), confirm.getCode());
+ }
+
+ @PostMapping("/update")
+ @ResponseStatus(NO_CONTENT)
+ @Operation(summary = "Update user password")
+ public void update(@RequestBody RecoveryUpdate update) {
+ updateService.update(update.getEmail(), update.getCode(), update.getPassword());
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryConfirm.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryConfirm.java
new file mode 100755
index 00000000..fad92cab
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryConfirm.java
@@ -0,0 +1,23 @@
+package com.github.throyer.example.modules.recoveries.dtos;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Getter
+@Setter
+public class RecoveryConfirm {
+
+ @Schema(example = "jubileu@email.com")
+ @Email(message = "{recovery.email.is-valid}")
+ @NotEmpty(message = "{recovery.email.not-empty}")
+ private String email;
+
+ @Schema(example = "5894")
+ @NotEmpty(message = "{recovery.code.not-empty}")
+ private String code;
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryEmail.java
old mode 100644
new mode 100755
similarity index 87%
rename from src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java
rename to api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryEmail.java
index 9e085a61..13e95f46
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryEmail.java
@@ -1,9 +1,9 @@
-package com.github.throyer.common.springboot.domain.models.emails;
-
-import com.github.throyer.common.springboot.domain.services.email.Email;
+package com.github.throyer.example.modules.recoveries.dtos;
import org.thymeleaf.context.Context;
+import com.github.throyer.example.modules.mail.models.Email;
+
public class RecoveryEmail implements Email {
private final String destination;
private final String subject;
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryRequest.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryRequest.java
new file mode 100755
index 00000000..cdcd4ce6
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryRequest.java
@@ -0,0 +1,18 @@
+package com.github.throyer.example.modules.recoveries.dtos;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class RecoveryRequest {
+
+ @Schema(example = "jubileu@email.com")
+ @Email(message = "{recovery.email.is-valid}")
+ @NotEmpty(message = "{recovery.email.not-empty}")
+ private String email;
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryUpdate.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryUpdate.java
new file mode 100755
index 00000000..a6d5f913
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/dtos/RecoveryUpdate.java
@@ -0,0 +1,29 @@
+package com.github.throyer.example.modules.recoveries.dtos;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Getter
+@Setter
+public class RecoveryUpdate {
+
+ @Schema(example = "jubileu@email.com")
+ @Email(message = "{recovery.email.is-valid}")
+ @NotEmpty(message = "{recovery.email.not-empty}")
+ private String email;
+
+ @Schema(example = "5894")
+ @NotEmpty(message = "{recovery.code.not-empty}")
+ private String code;
+
+ @Schema(example = "veryStrongAndSecurePassword")
+ @NotEmpty(message = "{user.password.not-empty}")
+ @Size(min = 8, max = 155, message = "{user.password.size}")
+ private String password;
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/entities/Recovery.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/entities/Recovery.java
new file mode 100755
index 00000000..9d0f3f68
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/entities/Recovery.java
@@ -0,0 +1,97 @@
+package com.github.throyer.example.modules.recoveries.entities;
+
+import static com.github.throyer.example.modules.shared.utils.Random.code;
+
+import java.time.LocalDateTime;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+import com.github.throyer.example.modules.users.entities.User;
+
+@Entity
+@Table(name = "recovery")
+public class Recovery {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "code", nullable = false)
+ private String code;
+
+ @Column(name = "expires_at", nullable = false)
+ private LocalDateTime expiresAt;
+
+ @Column(name = "confirmed")
+ private Boolean confirmed = false;
+
+ @Column(name = "used")
+ private Boolean used = false;
+
+ @JoinColumn(name = "user_id")
+ @ManyToOne
+ private User user;
+
+ public Recovery() {
+ }
+
+ public Recovery(User user, Integer minutesToExpire) {
+ this.user = user;
+ this.expiresAt = LocalDateTime.now().plusMinutes(minutesToExpire);
+ this.code = code();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public void setExpiresIn(LocalDateTime expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public Boolean isConfirmed() {
+ return confirmed;
+ }
+
+ public void setConfirmed(Boolean confirmed) {
+ this.confirmed = confirmed;
+ }
+
+ public Boolean isUsed() {
+ return used;
+ }
+
+ public void setUsed(Boolean used) {
+ this.used = used;
+ }
+
+ public Boolean nonExpired() {
+ return expiresAt.isAfter(LocalDateTime.now());
+ }
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/repositories/RecoveryRepository.java
old mode 100644
new mode 100755
similarity index 65%
rename from src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java
rename to api/src/main/java/com/github/throyer/example/modules/recoveries/repositories/RecoveryRepository.java
index 3fe29c1e..8d6e4b60
--- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/repositories/RecoveryRepository.java
@@ -1,15 +1,15 @@
-package com.github.throyer.common.springboot.domain.repositories;
+package com.github.throyer.example.modules.recoveries.repositories;
import java.util.Optional;
-import com.github.throyer.common.springboot.domain.models.entity.Recovery;
-
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
+import com.github.throyer.example.modules.recoveries.entities.Recovery;
+
@Repository
public interface RecoveryRepository extends JpaRepository, JpaSpecificationExecutor {
- public Optional findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresInDesc(Long id);
- public Optional findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresInDesc(Long id);
+ public Optional findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresAtDesc(Long id);
+ public Optional findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresAtDesc(Long id);
}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryConfirmService.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryConfirmService.java
new file mode 100755
index 00000000..c83b84dd
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryConfirmService.java
@@ -0,0 +1,36 @@
+package com.github.throyer.example.modules.recoveries.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import com.github.throyer.example.modules.recoveries.repositories.RecoveryRepository;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class RecoveryConfirmService {
+
+ @Autowired
+ private UserRepository users;
+
+ @Autowired
+ private RecoveryRepository recoveryRepository;
+
+ public void confirm(String email, String code) {
+ var user = users.findByEmail(email)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ var recovery = recoveryRepository
+ .findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN);
+ }
+
+ recovery.setConfirmed(true);
+
+ recoveryRepository.save(recovery);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryService.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryService.java
new file mode 100755
index 00000000..45aa72ae
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryService.java
@@ -0,0 +1,68 @@
+package com.github.throyer.example.modules.recoveries.services;
+
+import static com.github.throyer.example.modules.infra.constants.MailConstants.ERROR_SENDING_EMAIL_MESSAGE_TO;
+import static com.github.throyer.example.modules.infra.environments.PasswordRecoveryEnvironments.MINUTES_TO_EXPIRE_RECOVERY_CODE;
+import static com.github.throyer.example.modules.infra.environments.PasswordRecoveryEnvironments.SUBJECT_PASSWORD_RECOVERY_CODE;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.github.throyer.example.modules.mail.services.MailService;
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryEmail;
+import com.github.throyer.example.modules.recoveries.entities.Recovery;
+import com.github.throyer.example.modules.recoveries.repositories.RecoveryRepository;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class RecoveryService {
+
+ private static final Logger LOGGER = Logger.getLogger(RecoveryService.class.getName());
+
+ private final UserRepository users;
+ private final RecoveryRepository recoveries;
+ private final MailService service;
+
+ @Autowired
+ public RecoveryService(
+ UserRepository users,
+ RecoveryRepository recoveries,
+ MailService service
+ ) {
+ this.users = users;
+ this.recoveries = recoveries;
+ this.service = service;
+ }
+
+ public void recovery(String email) {
+
+ var user = users.findByEmail(email);
+
+ if (user.isEmpty()) {
+ return;
+ }
+
+ var recovery = new Recovery(user.get(), MINUTES_TO_EXPIRE_RECOVERY_CODE);
+
+ recoveries.save(recovery);
+
+ var sendEmailInBackground = new Thread(() -> {
+ var recoveryEmail = new RecoveryEmail(
+ email,
+ SUBJECT_PASSWORD_RECOVERY_CODE,
+ user.get().getName(),
+ recovery.getCode()
+ );
+
+ try {
+ service.send(recoveryEmail);
+ } catch (Exception exception) {
+ LOGGER.log(Level.INFO, ERROR_SENDING_EMAIL_MESSAGE_TO, email);
+ }
+ });
+
+ sendEmailInBackground.start();
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryUpdateService.java b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryUpdateService.java
new file mode 100755
index 00000000..a7f6beb6
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/recoveries/services/RecoveryUpdateService.java
@@ -0,0 +1,38 @@
+package com.github.throyer.example.modules.recoveries.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import com.github.throyer.example.modules.recoveries.repositories.RecoveryRepository;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class RecoveryUpdateService {
+
+ @Autowired
+ private UserRepository users;
+
+ @Autowired
+ private RecoveryRepository recoveries;
+
+ public void update(String email, String code, String password) {
+ var user = users.findByEmail(email)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ var recovery = recoveries
+ .findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN);
+ }
+
+ user.updatePassword(password);
+ users.save(user);
+
+ recovery.setUsed(true);
+ recoveries.save(recovery);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/roles/controllers/RolesController.java b/api/src/main/java/com/github/throyer/example/modules/roles/controllers/RolesController.java
new file mode 100755
index 00000000..654679c4
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/roles/controllers/RolesController.java
@@ -0,0 +1,36 @@
+package com.github.throyer.example.modules.roles.controllers;
+
+import static com.github.throyer.example.modules.infra.http.Responses.ok;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.github.throyer.example.modules.roles.dtos.RoleInformation;
+import com.github.throyer.example.modules.roles.repositories.RoleRepository;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@RestController
+@Tag(name = "Roles")
+@RequestMapping("/api/roles")
+@SecurityRequirement(name = "token")
+@PreAuthorize("hasAnyAuthority('ADM')")
+public class RolesController {
+
+ @Autowired
+ private RoleRepository repository;
+
+ @GetMapping
+ @Operation(summary = "Returns a list of roles")
+ public ResponseEntity> index() {
+ return ok(repository.findAll().stream().map(RoleInformation::new).toList());
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/roles/dtos/RoleInformation.java b/api/src/main/java/com/github/throyer/example/modules/roles/dtos/RoleInformation.java
new file mode 100644
index 00000000..8f67c3c9
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/roles/dtos/RoleInformation.java
@@ -0,0 +1,29 @@
+package com.github.throyer.example.modules.roles.dtos;
+
+import com.github.throyer.example.modules.roles.entities.Role;
+import com.github.throyer.example.modules.shared.utils.HashIdsUtils;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+public class RoleInformation {
+ @Schema(example = "A1PLgjPPlM8x", required = true)
+ private final String id;
+
+ @Schema(example = "Administrador", required = true)
+ private final String name;
+
+ @Schema(example = "ADM", required = true)
+ private final String shortName;
+
+ @Schema(example = "Administrador do sistema", required = true)
+ private final String description;
+
+ public RoleInformation(Role role) {
+ this.id = HashIdsUtils.encode(role.getId());
+ this.name = role.getName();
+ this.shortName = role.getShortName();
+ this.description = role.getDescription();
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/roles/entities/Role.java b/api/src/main/java/com/github/throyer/example/modules/roles/entities/Role.java
new file mode 100755
index 00000000..7e7a48dc
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/roles/entities/Role.java
@@ -0,0 +1,71 @@
+package com.github.throyer.example.modules.roles.entities;
+
+import static com.github.throyer.example.modules.management.repositories.Queries.NON_DELETED_CLAUSE;
+import static javax.persistence.GenerationType.IDENTITY;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.Where;
+import org.springframework.security.core.GrantedAuthority;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.github.throyer.example.modules.management.entities.Auditable;
+
+import lombok.Getter;
+
+@Getter
+@Entity
+@Table(name = "role")
+@Where(clause = NON_DELETED_CLAUSE)
+public class Role extends Auditable implements GrantedAuthority {
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @Column(name = "name", nullable = true, unique = true)
+ private String name;
+
+ @JsonIgnore
+ @Column(name = "deleted_name")
+ private String deletedName;
+
+ @Column(name = "short_name", nullable = true, unique = true)
+ private String shortName;
+
+ @JsonIgnore
+ @Column(name = "deleted_short_name")
+ private String deletedShortName;
+
+ @Column(nullable = true, unique = true)
+ private String description;
+
+ public Role() { }
+
+ public Role(String shortName) {
+ this.shortName = shortName;
+ }
+
+ public Role(Long id) {
+ this.id = id;
+ }
+
+ public Role(Long id, String shortName) {
+ this.id = id;
+ this.shortName = shortName;
+ }
+
+ @Override
+ public String toString() {
+ return this.getAuthority();
+ }
+
+ @JsonIgnore
+ @Override
+ public String getAuthority() {
+ return this.getShortName();
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/roles/repositories/Queries.java b/api/src/main/java/com/github/throyer/example/modules/roles/repositories/Queries.java
new file mode 100755
index 00000000..23ead404
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/roles/repositories/Queries.java
@@ -0,0 +1,21 @@
+package com.github.throyer.example.modules.roles.repositories;
+
+public class Queries {
+ public static final String DELETE_ROLE_BY_ID = """
+ UPDATE
+ #{#entityName}
+ SET
+ deletedName = (
+ SELECT name FROM #{#entityName} WHERE id = ?1
+ ),
+ name = NULL,
+ deletedShortName = (
+ SELECT shortName FROM #{#entityName} WHERE id = ?1
+ ),
+ shortName = NULL,
+ deletedAt = CURRENT_TIMESTAMP,
+ active = false,
+ deletedBy = ?#{principal?.id}
+ WHERE id = ?1
+ """;
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/roles/repositories/RoleRepository.java b/api/src/main/java/com/github/throyer/example/modules/roles/repositories/RoleRepository.java
new file mode 100755
index 00000000..11fd2a70
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/roles/repositories/RoleRepository.java
@@ -0,0 +1,43 @@
+package com.github.throyer.example.modules.roles.repositories;
+
+import static com.github.throyer.example.modules.roles.repositories.Queries.DELETE_ROLE_BY_ID;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.github.throyer.example.modules.management.repositories.SoftDeleteRepository;
+import com.github.throyer.example.modules.roles.entities.Role;
+
+@Repository
+public interface RoleRepository extends SoftDeleteRepository {
+
+ @Override
+ @Modifying
+ @Transactional
+ @Query(DELETE_ROLE_BY_ID)
+ void deleteById(Long id);
+
+ @Override
+ @Transactional
+ default void delete(Role role) {
+ deleteById(role.getId());
+ }
+
+ @Override
+ @Transactional
+ default void deleteAll(Iterable extends Role> entities) {
+ entities.forEach(entity -> deleteById(entity.getId()));
+ }
+
+ Optional findOptionalByShortName(String shortName);
+
+ Optional findOptionalByName(String name);
+
+ Boolean existsByShortName(String shortName);
+
+ Boolean existsByName(String name);
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/errors/ApiError.java b/api/src/main/java/com/github/throyer/example/modules/shared/errors/ApiError.java
new file mode 100644
index 00000000..ea16151f
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/errors/ApiError.java
@@ -0,0 +1,51 @@
+package com.github.throyer.example.modules.shared.errors;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.http.HttpStatus;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+public class ApiError {
+ @Schema(example = "generic error message", required = true)
+ private final String message;
+
+ @Schema(example = "999", required = true)
+ private final Integer status;
+
+ @Schema(required = false)
+ public final Collection errors;
+
+ public ApiError(String message, Integer status) {
+ this.message = message;
+ this.status = status;
+ this.errors = List.of();
+ }
+
+ public ApiError(String message, HttpStatus status) {
+ this.message = message;
+ this.status = status.value();
+ this.errors = List.of();
+ }
+
+ public ApiError(HttpStatus status, Collection errors) {
+ this.message = "Check the 'errors' property for more details.";
+ this.status = status.value();
+ this.errors = errors;
+ }
+
+ public ApiError(HttpStatus status, String error) {
+ this.message = "Check the 'errors' property for more details.";
+ this.status = status.value();
+ this.errors = List.of(new ValidationError(error));
+ }
+
+ public ApiError(HttpStatus status, ValidationError error) {
+ this.message = "Check the 'errors' property for more details.";
+ this.status = status.value();
+ this.errors = List.of(error);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/errors/ValidationError.java b/api/src/main/java/com/github/throyer/example/modules/shared/errors/ValidationError.java
new file mode 100644
index 00000000..c4e2adec
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/errors/ValidationError.java
@@ -0,0 +1,41 @@
+package com.github.throyer.example.modules.shared.errors;
+
+import java.util.List;
+
+import org.springframework.web.bind.MethodArgumentNotValidException;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+public class ValidationError {
+
+ @Schema(example = "genericFieldName")
+ private final String field;
+
+ @Schema(example = "generic error message", required = true)
+ private final String message;
+
+ public ValidationError(String message) {
+ this.field = null;
+ this.message = message;
+ }
+
+ public ValidationError(org.springframework.validation.FieldError error) {
+ this.field = error.getField();
+ this.message = error.getDefaultMessage();
+ }
+
+ public ValidationError(String field, String message) {
+ this.field = field;
+ this.message = message;
+ }
+
+ public static List of(MethodArgumentNotValidException exception) {
+ return exception.getBindingResult()
+ .getAllErrors()
+ .stream()
+ .map((error) -> (new ValidationError((org.springframework.validation.FieldError) error)))
+ .toList();
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/exceptions/BadRequestException.java b/api/src/main/java/com/github/throyer/example/modules/shared/exceptions/BadRequestException.java
new file mode 100755
index 00000000..39143ec6
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/exceptions/BadRequestException.java
@@ -0,0 +1,51 @@
+package com.github.throyer.example.modules.shared.exceptions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.github.throyer.example.modules.shared.errors.ValidationError;
+
+public class BadRequestException extends RuntimeException {
+ Collection errors;
+
+ public BadRequestException() {
+ this.errors = new ArrayList<>();
+ }
+
+ public BadRequestException(Collection errors) {
+ this.errors = errors;
+ }
+
+ public BadRequestException(String message, Collection errors) {
+ super(message);
+ this.errors = errors;
+ }
+
+ public BadRequestException(String message, Throwable cause, Collection errors) {
+ super(message, cause);
+ this.errors = errors;
+ }
+
+ public BadRequestException(Throwable cause, Collection errors) {
+ super(cause);
+ this.errors = errors;
+ }
+
+ public BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,
+ Collection errors) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ this.errors = errors;
+ }
+
+ public Collection getErrors() {
+ return errors;
+ }
+
+ public void add(ValidationError error) {
+ this.errors.add(error);
+ }
+
+ public Boolean hasError() {
+ return !this.errors.isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/utils/HashIdsUtils.java b/api/src/main/java/com/github/throyer/example/modules/shared/utils/HashIdsUtils.java
new file mode 100644
index 00000000..657b9807
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/utils/HashIdsUtils.java
@@ -0,0 +1,18 @@
+package com.github.throyer.example.modules.shared.utils;
+
+import static com.github.throyer.example.modules.infra.environments.SecurityEnvironments.HASH_ID;
+
+import java.util.Arrays;
+
+public class HashIdsUtils {
+ public static String encode(Long id) {
+ return HASH_ID.encode(id);
+ }
+
+ public static Long decode(String id) {
+ return Arrays.stream(HASH_ID.decode(id))
+ .boxed()
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/utils/InternationalizationUtils.java b/api/src/main/java/com/github/throyer/example/modules/shared/utils/InternationalizationUtils.java
new file mode 100755
index 00000000..a7c9f57d
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/utils/InternationalizationUtils.java
@@ -0,0 +1,25 @@
+package com.github.throyer.example.modules.shared.utils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.stereotype.Component;
+
+@Component
+public class InternationalizationUtils {
+
+ public static ResourceBundleMessageSource messageSource;
+
+ @Autowired
+ public InternationalizationUtils(ResourceBundleMessageSource resourceBundleMessageSource) {
+ InternationalizationUtils.messageSource = resourceBundleMessageSource;
+ }
+
+ public static String message(String key) {
+ return messageSource.getMessage(key, null, LocaleContextHolder.getLocale());
+ }
+
+ public static String message(String key, Object... args) {
+ return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/JsonUtils.java b/api/src/main/java/com/github/throyer/example/modules/shared/utils/JSON.java
old mode 100644
new mode 100755
similarity index 73%
rename from src/main/java/com/github/throyer/common/springboot/utils/JsonUtils.java
rename to api/src/main/java/com/github/throyer/example/modules/shared/utils/JSON.java
index b9e7a511..40a74753
--- a/src/main/java/com/github/throyer/common/springboot/utils/JsonUtils.java
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/utils/JSON.java
@@ -1,16 +1,16 @@
-package com.github.throyer.common.springboot.utils;
+package com.github.throyer.example.modules.shared.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
-public class JsonUtils {
+public class JSON {
- private JsonUtils() { }
+ private JSON() { }
private static final ObjectWriter writer = new ObjectMapper().writer().withDefaultPrettyPrinter();
- public static String toJson(final T object) {
+ public static String stringify(final T object) {
try {
return writer.writeValueAsString(object);
} catch (JsonProcessingException exception) {
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/utils/Random.java b/api/src/main/java/com/github/throyer/example/modules/shared/utils/Random.java
new file mode 100755
index 00000000..6add2ca1
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/utils/Random.java
@@ -0,0 +1,94 @@
+package com.github.throyer.example.modules.shared.utils;
+
+import static com.github.throyer.example.modules.infra.environments.SecurityEnvironments.JWT;
+import static com.github.throyer.example.modules.infra.environments.SecurityEnvironments.TOKEN_SECRET;
+import static java.lang.String.format;
+import static java.time.LocalDateTime.now;
+import static java.util.List.of;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import com.github.javafaker.Faker;
+import com.github.throyer.example.modules.roles.entities.Role;
+import com.github.throyer.example.modules.users.entities.User;
+
+public class Random {
+ private Random() {
+ }
+
+ private static final java.util.Random JAVA_RANDOM = new java.util.Random();
+ public static final Faker FAKER = new Faker(new Locale("pt", "BR"));
+
+ public static Integer between(Integer min, Integer max) {
+ return JAVA_RANDOM.nextInt(max - min) + min;
+ }
+
+ public static T element(List list) {
+ return list.get(JAVA_RANDOM.nextInt(list.size()));
+ }
+
+ public static String code() {
+ return format("%s%s%s%s", between(0, 9), between(0, 9), between(0, 9), between(0, 9));
+ }
+
+ public static String name() {
+ return FAKER.name().fullName();
+ }
+
+ public static String email() {
+ return FAKER.internet().safeEmailAddress();
+ }
+
+ public static String password() {
+ return FAKER.regexify("[a-z]{5,13}[1-9]{1,5}[A-Z]{1,5}[#?!@$%^&*-]{1,5}");
+ }
+
+ public static List users(Integer size) {
+ List users = new ArrayList<>();
+ for (int index = 0; index < size; index++) {
+ users.add(user());
+ }
+ return users;
+ }
+
+ public static User user() {
+ return user(of());
+ }
+
+ public static User user(List roles) {
+ return new User(
+ name(),
+ email(),
+ password(),
+ roles
+ );
+ }
+
+ public static String token(Long id, String roles) {
+ return token(id, now().plusHours(24), TOKEN_SECRET, of(roles.split(",")));
+ }
+
+ public static String token(String roles) {
+ return token(between(1, 9999).longValue(), now().plusHours(24), TOKEN_SECRET, of(roles.split(",")));
+ }
+
+ public static String token(String roles, String secret) {
+ return token(between(1, 9999).longValue(), now().plusHours(24), secret, of(roles.split(",")));
+ }
+
+ public static String token(LocalDateTime expiration, String roles) {
+ return token(between(1, 9999).longValue(), expiration, TOKEN_SECRET, of(roles.split(",")));
+ }
+
+ public static String token(LocalDateTime expiration, String roles, String secret) {
+ return token(between(1, 9999).longValue(), expiration, secret, of(roles.split(",")));
+ }
+
+ public static String token(Long id, LocalDateTime expiration, String secret, List roles) {
+ var token = JWT.encode(HashIdsUtils.encode(id), roles, expiration, secret);
+ return format("Bearer %s", token);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/shared/utils/Strings.java b/api/src/main/java/com/github/throyer/example/modules/shared/utils/Strings.java
new file mode 100644
index 00000000..0867f335
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/shared/utils/Strings.java
@@ -0,0 +1,19 @@
+package com.github.throyer.example.modules.shared.utils;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class Strings {
+ private Strings() { }
+
+ public static Boolean notNullOrBlank(String string) {
+ if (Objects.isNull(string)) {
+ return false;
+ }
+ return !string.isBlank();
+ }
+
+ public static Boolean noneOfThenNullOrEmpty(String... strings) {
+ return Stream.of(strings).allMatch(Strings::notNullOrBlank);
+ }
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/LoginController.java b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/LoginController.java
old mode 100644
new mode 100755
similarity index 63%
rename from src/main/java/com/github/throyer/common/springboot/controllers/app/LoginController.java
rename to api/src/main/java/com/github/throyer/example/modules/ssr/controllers/LoginController.java
index 06da849e..e809bb23
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/LoginController.java
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/LoginController.java
@@ -1,4 +1,4 @@
-package com.github.throyer.common.springboot.controllers.app;
+package com.github.throyer.example.modules.ssr.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@@ -7,8 +7,8 @@
@Controller
@RequestMapping("/app/login")
public class LoginController {
- @GetMapping
- public String login() {
- return "app/login/index";
- }
+ @GetMapping
+ public String login() {
+ return "app/login/index";
+ }
}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RecoveryController.java b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RecoveryController.java
new file mode 100755
index 00000000..9f1b35ea
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RecoveryController.java
@@ -0,0 +1,104 @@
+package com.github.throyer.example.modules.ssr.controllers;
+
+import static com.github.throyer.example.modules.infra.http.Responses.validateAndUpdateModel;
+
+import javax.validation.Valid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryRequest;
+import com.github.throyer.example.modules.ssr.dtos.Codes;
+import com.github.throyer.example.modules.ssr.dtos.UpdatePasswordWithRecoveryCodeByApp;
+import com.github.throyer.example.modules.ssr.services.AppRecoveryService;
+import com.github.throyer.example.modules.ssr.toasts.Toasts;
+import com.github.throyer.example.modules.ssr.toasts.Type;
+
+@Controller
+@RequestMapping("/app/recovery")
+public class RecoveryController {
+
+ @Autowired
+ private AppRecoveryService service;
+
+ @GetMapping
+ public String index(Model model) {
+ model.addAttribute("recovery", new RecoveryRequest());
+ return "app/recovery/index";
+ }
+
+ @PostMapping
+ public String index(
+ @Valid RecoveryRequest recovery,
+ BindingResult result,
+ Model model) {
+
+ if (validateAndUpdateModel(model, recovery, "recovery", result)) {
+ return "app/recovery/index";
+ }
+
+ var email = recovery.getEmail();
+
+ service.recovery(email);
+
+ model.addAttribute("codes", new Codes(email));
+
+ return "app/recovery/confirm";
+ }
+
+ @PostMapping("/confirm")
+ public String confirm(
+ @Valid Codes codes,
+ BindingResult result,
+ RedirectAttributes redirect,
+ Model model) {
+
+ if (validateAndUpdateModel(model, codes, "recovery", result)) {
+ return "app/recovery/confirm";
+ }
+
+ try {
+ service.confirm(codes.getEmail(), codes.code());
+ } catch (ResponseStatusException exception) {
+
+ Toasts.add(model, "Código expirado ou invalido.", Type.DANGER);
+ model.addAttribute("confirm", codes);
+ return "app/recovery/confirm";
+ }
+
+ model.addAttribute("update", new UpdatePasswordWithRecoveryCodeByApp(codes));
+
+ return "app/recovery/update";
+ }
+
+ @PostMapping("/update")
+ public String update(
+ @Valid UpdatePasswordWithRecoveryCodeByApp update,
+ BindingResult result,
+ RedirectAttributes redirect,
+ Model model) {
+ update.validate(result);
+
+ if (validateAndUpdateModel(model, update, "update", result)) {
+ return "app/recovery/update";
+ }
+
+ try {
+ service.update(update.getEmail(), update.code(), update.getPassword());
+ } catch (ResponseStatusException exception) {
+ Toasts.add(model, "Código expirado ou invalido.", Type.DANGER);
+ model.addAttribute("update", update);
+ return "app/recovery/update";
+ }
+
+ Toasts.add(redirect, "Sua senha foi atualizada com sucesso.", Type.SUCCESS);
+ return "redirect:/app/login";
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RegisterController.java b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RegisterController.java
new file mode 100755
index 00000000..06d52ae9
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/RegisterController.java
@@ -0,0 +1,50 @@
+package com.github.throyer.example.modules.ssr.controllers;
+
+import javax.validation.Valid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import com.github.throyer.example.modules.ssr.dtos.CreateUserByApp;
+import com.github.throyer.example.modules.ssr.services.AppUserService;
+import com.github.throyer.example.modules.users.dtos.CreateUserProps;
+
+@Controller
+@RequestMapping("/app/register")
+public class RegisterController {
+
+ private final AppUserService service;
+
+ @Autowired
+ public RegisterController(AppUserService service) {
+ this.service = service;
+ }
+
+ @GetMapping(produces = "text/html")
+ public String index(Model model) {
+ model.addAttribute("user", new CreateUserProps());
+ return "app/register/index";
+ }
+
+ @PostMapping(produces = "text/html")
+ public String create(
+ @Valid CreateUserByApp props,
+ BindingResult validations,
+ RedirectAttributes redirect,
+ Model model
+ ) {
+ service.create(props, validations, redirect, model);
+
+ if (validations.hasErrors()) {
+ return "app/register/index";
+ }
+
+ return "redirect:/app/login";
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/UserController.java b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/UserController.java
new file mode 100755
index 00000000..a21b4de5
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/controllers/UserController.java
@@ -0,0 +1,86 @@
+package com.github.throyer.example.modules.ssr.controllers;
+
+import static com.github.throyer.example.modules.shared.utils.HashIdsUtils.decode;
+
+import java.util.Optional;
+
+import javax.validation.Valid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import com.github.throyer.example.modules.pagination.utils.Pagination;
+import com.github.throyer.example.modules.ssr.dtos.CreateOrUpdateUserByAppForm;
+import com.github.throyer.example.modules.ssr.services.AppUserService;
+import com.github.throyer.example.modules.ssr.toasts.Toasts;
+import com.github.throyer.example.modules.ssr.toasts.Type;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Controller
+@PreAuthorize("hasAnyAuthority('ADM')")
+@RequestMapping("/app/users")
+public class UserController {
+ @Autowired
+ private UserRepository repository;
+
+ @Autowired
+ private AppUserService service;
+
+ @GetMapping
+ public String index(
+ Model model,
+ Optional page,
+ Optional size
+ ) {
+ var pageable = Pagination.of(page, size);
+ var content = repository.findAll(pageable);
+
+ model.addAttribute("page", content);
+
+ return "app/users/index";
+ }
+
+ @GetMapping("/form")
+ public String createForm(Model model) {
+ model.addAttribute("create", true);
+ return "app/users/form";
+ }
+
+ @GetMapping("/form/{user_id}")
+ public String editForm(@PathVariable("user_id") String id, Model model) {
+ model.addAttribute("create", false);
+ return "app/users/form";
+ }
+
+ @PostMapping(produces = "text/html")
+ public String create(
+ @Valid CreateOrUpdateUserByAppForm props,
+ BindingResult validations,
+ RedirectAttributes redirect,
+ Model model
+ ) {
+ if (validations.hasErrors()) {
+ return "app/users/index";
+ }
+
+ return "redirect:/app/users/form";
+ }
+
+ @PostMapping("/delete/{user_id}")
+ public String delete(@PathVariable("user_id") String id, RedirectAttributes redirect) {
+
+ service.remove(decode(id));
+
+ Toasts.add(redirect, "Usuário deletado com sucesso.", Type.SUCCESS);
+
+ return "redirect:/app/users";
+ }
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/Codes.java
old mode 100644
new mode 100755
similarity index 89%
rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java
rename to api/src/main/java/com/github/throyer/example/modules/ssr/dtos/Codes.java
index 8b0b945c..f69fc8c4
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/Codes.java
@@ -1,4 +1,4 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
+package com.github.throyer.example.modules.ssr.dtos;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateOrUpdateUserByAppForm.java b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateOrUpdateUserByAppForm.java
new file mode 100755
index 00000000..d927af7a
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateOrUpdateUserByAppForm.java
@@ -0,0 +1,51 @@
+package com.github.throyer.example.modules.ssr.dtos;
+
+import static com.github.throyer.example.modules.ssr.validation.AppEmailValidations.validateEmailUniqueness;
+
+import java.util.List;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+import org.springframework.validation.BindingResult;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.roles.entities.Role;
+import com.github.throyer.example.modules.shared.utils.JSON;
+import com.github.throyer.example.modules.users.entities.User;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class CreateOrUpdateUserByAppForm implements Addressable {
+
+ private Long id;
+
+ @NotEmpty(message = "${user.name.not-empty}")
+ private String name;
+
+ @NotEmpty(message = "{user.email.not-empty}")
+ @Email(message = "{user.email.is-valid}")
+ private String email;
+
+ @NotNull
+ private List roles;
+
+ public void validate(BindingResult result) {
+ validateEmailUniqueness(this, result);
+ }
+
+ public User user() {
+ var user = new User(name, email, null, roles);
+ user.setId(id);
+ return user;
+ }
+
+ @Override
+ public String toString() {
+ return JSON.stringify(this);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateUserByApp.java b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateUserByApp.java
new file mode 100644
index 00000000..0112e8c6
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/CreateUserByApp.java
@@ -0,0 +1,44 @@
+package com.github.throyer.example.modules.ssr.dtos;
+
+import static com.github.throyer.example.modules.ssr.validation.AppEmailValidations.validateEmailUniqueness;
+
+import java.util.List;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+import org.springframework.validation.BindingResult;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.users.entities.User;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class CreateUserByApp implements Addressable {
+ @Schema(example = "Jubileu da silva")
+ @NotEmpty(message = "${user.name.not-empty}")
+ private String name;
+
+ @Schema(example = "jubileu@email.com")
+ @NotEmpty(message = "{user.email.not-empty}")
+ @Email(message = "{user.email.is-valid}")
+ private String email;
+
+ @Schema(example = "veryStrongAndSecurePassword")
+ @NotEmpty(message = "{user.password.not-empty}")
+ @Size(min = 8, max = 155, message = "{user.password.size}")
+ private String password;
+
+ public void validate(BindingResult result) {
+ validateEmailUniqueness(this, result);
+ }
+
+ public User user() {
+ return new User(name, email, password, List.of());
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/RecoveryRequestByApp.java b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/RecoveryRequestByApp.java
new file mode 100644
index 00000000..d2176656
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/RecoveryRequestByApp.java
@@ -0,0 +1,18 @@
+package com.github.throyer.example.modules.ssr.dtos;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class RecoveryRequestByApp {
+
+ @Schema(example = "jubileu@email.com")
+ @Email(message = "{recovery.email.is-valid}")
+ @NotEmpty(message = "{recovery.email.not-empty}")
+ private String email;
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/UpdatePasswordWithRecoveryCodeByApp.java b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/UpdatePasswordWithRecoveryCodeByApp.java
new file mode 100755
index 00000000..be07156f
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/dtos/UpdatePasswordWithRecoveryCodeByApp.java
@@ -0,0 +1,42 @@
+package com.github.throyer.example.modules.ssr.dtos;
+
+import static java.lang.String.format;
+import static org.springframework.beans.BeanUtils.copyProperties;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+import org.springframework.validation.BindingResult;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class UpdatePasswordWithRecoveryCodeByApp {
+
+ @Email(message = "{recovery.email.is-valid}")
+ @NotEmpty(message = "{recovery.email.not-empty}")
+ private String email;
+
+ private String first = "";
+ private String second = "";
+ private String third = "";
+ private String fourth = "";
+
+ @NotEmpty(message = "{user.password.not-empty}")
+ @Size(min = 8, max = 155, message = "{user.password.size}")
+ private String password;
+
+ public UpdatePasswordWithRecoveryCodeByApp(Codes codes) {
+ copyProperties(codes, this);
+ }
+
+ public void validate(BindingResult result) {
+ }
+
+ public String code() {
+ return format("%s%s%s%s", first, second, third, fourth);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppRecoveryService.java b/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppRecoveryService.java
new file mode 100644
index 00000000..1e21a1fd
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppRecoveryService.java
@@ -0,0 +1,98 @@
+package com.github.throyer.example.modules.ssr.services;
+
+import static com.github.throyer.example.modules.infra.constants.MailConstants.ERROR_SENDING_EMAIL_MESSAGE_TO;
+import static com.github.throyer.example.modules.infra.environments.PasswordRecoveryEnvironments.MINUTES_TO_EXPIRE_RECOVERY_CODE;
+import static com.github.throyer.example.modules.infra.environments.PasswordRecoveryEnvironments.SUBJECT_PASSWORD_RECOVERY_CODE;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import com.github.throyer.example.modules.mail.services.MailService;
+import com.github.throyer.example.modules.recoveries.dtos.RecoveryEmail;
+import com.github.throyer.example.modules.recoveries.entities.Recovery;
+import com.github.throyer.example.modules.recoveries.repositories.RecoveryRepository;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Service
+@Slf4j
+public class AppRecoveryService {
+
+ @Autowired
+ private UserRepository users;
+
+ @Autowired
+ private RecoveryRepository recoveryRepository;
+
+ @Autowired
+ private MailService mailService;
+
+ public void recovery(String email) {
+
+ var user = users.findByEmail(email);
+
+ if (user.isEmpty()) {
+ return;
+ }
+
+ var recovery = new Recovery(user.get(), MINUTES_TO_EXPIRE_RECOVERY_CODE);
+
+ recoveryRepository.save(recovery);
+
+ var sendEmailInBackground = new Thread(() -> {
+ var recoveryEmail = new RecoveryEmail(
+ email,
+ SUBJECT_PASSWORD_RECOVERY_CODE,
+ user.get().getName(),
+ recovery.getCode()
+ );
+
+ try {
+ mailService.send(recoveryEmail);
+ } catch (Exception exception) {
+ log.info(ERROR_SENDING_EMAIL_MESSAGE_TO, email);
+ }
+ });
+
+ sendEmailInBackground.start();
+ }
+
+ public void confirm(String email, String code) {
+ var user = users.findByEmail(email)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ var recovery = recoveryRepository
+ .findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN);
+ }
+
+ recovery.setConfirmed(true);
+
+ recoveryRepository.save(recovery);
+ }
+
+ public void update(String email, String code, String password) {
+ var user = users.findByEmail(email)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ var recovery = recoveryRepository
+ .findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
+
+ if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN);
+ }
+
+ user.updatePassword(password);
+ users.save(user);
+
+ recovery.setUsed(true);
+ recoveryRepository.save(recovery);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppUserService.java b/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppUserService.java
new file mode 100644
index 00000000..fc23d0c9
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/services/AppUserService.java
@@ -0,0 +1,71 @@
+package com.github.throyer.example.modules.ssr.services;
+
+import static com.github.throyer.example.modules.infra.constants.ToastConstants.TOAST_SUCCESS_MESSAGE;
+import static com.github.throyer.example.modules.infra.http.Responses.notFound;
+import static com.github.throyer.example.modules.ssr.validation.AppEmailValidations.validateEmailUniqueness;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+import static com.github.throyer.example.modules.ssr.toasts.Type.SUCCESS;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import com.github.throyer.example.modules.roles.repositories.RoleRepository;
+import com.github.throyer.example.modules.ssr.dtos.CreateUserByApp;
+import com.github.throyer.example.modules.ssr.toasts.Toasts;
+import com.github.throyer.example.modules.users.entities.User;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class AppUserService {
+ private final UserRepository userRepository;
+ private final RoleRepository roleRepository;
+
+ @Autowired
+ public AppUserService(
+ UserRepository userRepository,
+ RoleRepository roleRepository
+ ) {
+ this.userRepository = userRepository;
+ this.roleRepository = roleRepository;
+ }
+
+ public void create(
+ CreateUserByApp props,
+ BindingResult validations,
+ RedirectAttributes redirect,
+ Model model
+ ) {
+ validateEmailUniqueness(props, validations);
+
+ if (validations.hasErrors()) {
+ model.addAttribute("user", props);
+ Toasts.add(model, validations);
+ return;
+ }
+
+ Toasts.add(redirect, message(TOAST_SUCCESS_MESSAGE), SUCCESS);
+
+ var roles = roleRepository.findOptionalByShortName("USER")
+ .map(List::of)
+ .orElseGet(List::of);
+
+ var name = props.getName();
+ var email = props.getEmail();
+ var password = props.getPassword();
+
+ userRepository.save(new User(name, email, password, roles));
+ }
+
+ public void remove(Long id) {
+ var user = userRepository
+ .findById(id)
+ .orElseThrow(() -> notFound("User not found"));
+
+ userRepository.delete(user);
+ }
+}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toast.java
old mode 100644
new mode 100755
similarity index 59%
rename from src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java
rename to api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toast.java
index e8e42bc3..f2e8a449
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toast.java
@@ -1,6 +1,7 @@
-package com.github.throyer.common.springboot.domain.models.shared;
+package com.github.throyer.example.modules.ssr.toasts;
import lombok.Data;
+import org.springframework.validation.ObjectError;
@Data
public class Toast {
@@ -16,4 +17,8 @@ private Toast(String message, String type) {
public static Toast of(String message, Type type) {
return new Toast(message, type.name);
}
+
+ public static Toast of(ObjectError error) {
+ return of(error.getDefaultMessage(), Type.DANGER);
+ }
}
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toasts.java
old mode 100644
new mode 100755
similarity index 80%
rename from src/main/java/com/github/throyer/common/springboot/utils/Toasts.java
rename to api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toasts.java
index e6d2a6f7..8c15af7f
--- a/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Toasts.java
@@ -1,11 +1,8 @@
-package com.github.throyer.common.springboot.utils;
+package com.github.throyer.example.modules.ssr.toasts;
-import com.github.throyer.common.springboot.domain.models.shared.Toast;
-import java.util.List;
+import static com.github.throyer.example.modules.ssr.toasts.Type.*;
-import static com.github.throyer.common.springboot.domain.models.shared.Type.*;
-import com.github.throyer.common.springboot.domain.models.shared.Type;
-import java.util.stream.Collectors;
+import java.util.List;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@@ -54,7 +51,7 @@ public static void add(Model model, BindingResult result) {
private static List toasts(List errors) {
return errors.stream()
- .map(error -> Toast.of(error.getDefaultMessage(), DANGER))
- .collect(Collectors.toList());
+ .map(Toast::of)
+ .toList();
}
}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Type.java b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Type.java
old mode 100644
new mode 100755
similarity index 84%
rename from src/main/java/com/github/throyer/common/springboot/domain/models/shared/Type.java
rename to api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Type.java
index 9e11b23a..6826dc47
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Type.java
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/toasts/Type.java
@@ -1,4 +1,4 @@
-package com.github.throyer.common.springboot.domain.models.shared;
+package com.github.throyer.example.modules.ssr.toasts;
public enum Type {
diff --git a/api/src/main/java/com/github/throyer/example/modules/ssr/validation/AppEmailValidations.java b/api/src/main/java/com/github/throyer/example/modules/ssr/validation/AppEmailValidations.java
new file mode 100644
index 00000000..7bd7be17
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/ssr/validation/AppEmailValidations.java
@@ -0,0 +1,44 @@
+package com.github.throyer.example.modules.ssr.validation;
+
+import static com.github.throyer.example.modules.infra.constants.MessagesConstants.EMAIL_ALREADY_USED_MESSAGE;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Component
+public class AppEmailValidations {
+ private static UserRepository repository;
+
+ @Autowired
+ public AppEmailValidations(UserRepository repository) {
+ AppEmailValidations.repository = repository;
+ }
+
+ public static void validateEmailUniqueness(Addressable entity, BindingResult validations) {
+ if (repository.existsByEmail(entity.getEmail())) {
+ validations.addError(new ObjectError("email", message(EMAIL_ALREADY_USED_MESSAGE)));
+ }
+ }
+
+ public static void validateEmailUniquenessOnModify(
+ Addressable newEntity,
+ Addressable actualEntity,
+ BindingResult validations) {
+ var newEmail = newEntity.getEmail();
+ var actualEmail = actualEntity.getEmail();
+
+ var changedEmail = !actualEmail.equals(newEmail);
+
+ var emailAlreadyUsed = repository.existsByEmail(newEmail);
+
+ if (changedEmail && emailAlreadyUsed) {
+ validations.addError(new ObjectError("email", message(EMAIL_ALREADY_USED_MESSAGE)));
+ }
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/controllers/UsersController.java b/api/src/main/java/com/github/throyer/example/modules/users/controllers/UsersController.java
new file mode 100755
index 00000000..d5f7ed92
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/controllers/UsersController.java
@@ -0,0 +1,110 @@
+package com.github.throyer.example.modules.users.controllers;
+
+import static com.github.throyer.example.modules.infra.http.Responses.created;
+import static com.github.throyer.example.modules.infra.http.Responses.ok;
+import static com.github.throyer.example.modules.shared.utils.HashIdsUtils.decode;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
+
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.users.dtos.CreateUserProps;
+import com.github.throyer.example.modules.users.dtos.UpdateUserProps;
+import com.github.throyer.example.modules.users.dtos.UserInformation;
+import com.github.throyer.example.modules.users.service.CreateUserService;
+import com.github.throyer.example.modules.users.service.FindUserService;
+import com.github.throyer.example.modules.users.service.RemoveUserService;
+import com.github.throyer.example.modules.users.service.UpdateUserService;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@RestController
+@Tag(name = "Users")
+@RequestMapping("/api/users")
+public class UsersController {
+ private final CreateUserService createService;
+ private final UpdateUserService updateService;
+ private final RemoveUserService removeService;
+ private final FindUserService findService;
+
+ @Autowired
+ public UsersController(
+ CreateUserService createService,
+ UpdateUserService updateService,
+ RemoveUserService removeService,
+ FindUserService findService
+ ) {
+ this.createService = createService;
+ this.updateService = updateService;
+ this.removeService = removeService;
+ this.findService = findService;
+ }
+
+ @GetMapping
+ @SecurityRequirement(name = "token")
+ @PreAuthorize("hasAnyAuthority('ADM')")
+ @Operation(summary = "Returns a list of users")
+ public ResponseEntity> index(
+ Optional page,
+ Optional size
+ ) {
+ var response = findService.find(page, size);
+ return ok(response.map(UserInformation::new));
+ }
+
+ @GetMapping("/{user_id}")
+ @SecurityRequirement(name = "token")
+ @PreAuthorize("hasAnyAuthority('ADM', 'USER')")
+ @Operation(summary = "Show user info")
+ public ResponseEntity show(@PathVariable("user_id") String id) {
+ var user = findService.find(decode(id));
+ return ok(new UserInformation(user));
+ }
+
+ @PostMapping
+ @ResponseStatus(CREATED)
+ @Operation(summary = "Register a new user", description = "Returns the new user")
+ public ResponseEntity save(
+ @Validated @RequestBody CreateUserProps props
+ ) {
+ var user = new UserInformation(createService.create(props));
+ return created(user, "api/users", user.getId());
+ }
+
+ @PutMapping("/{user_id}")
+ @SecurityRequirement(name = "token")
+ @PreAuthorize("hasAnyAuthority('ADM', 'USER')")
+ @Operation(summary = "Update user data")
+ public ResponseEntity update(
+ @PathVariable("user_id") String id,
+ @RequestBody @Validated UpdateUserProps body
+ ) {
+ var user = updateService.update(decode(id), body);
+ return ok(new UserInformation(user));
+ }
+
+ @DeleteMapping("/{user_id}")
+ @ResponseStatus(NO_CONTENT)
+ @SecurityRequirement(name = "token")
+ @Operation(summary = "Delete user")
+ public void destroy(@PathVariable("user_id") String id) {
+ removeService.remove(decode(id));
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/dtos/CreateUserProps.java b/api/src/main/java/com/github/throyer/example/modules/users/dtos/CreateUserProps.java
new file mode 100755
index 00000000..5863ed46
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/dtos/CreateUserProps.java
@@ -0,0 +1,45 @@
+package com.github.throyer.example.modules.users.dtos;
+
+import static com.github.throyer.example.modules.mail.validations.EmailValidations.validateEmailUniqueness;
+
+import java.util.List;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.users.entities.User;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class CreateUserProps implements Addressable {
+
+ @Schema(example = "Jubileu da silva")
+ @NotEmpty(message = "${user.name.not-empty}")
+ private String name;
+
+ @Schema(example = "jubileu@email.com")
+ @NotEmpty(message = "{user.email.not-empty}")
+ @Email(message = "{user.email.is-valid}")
+ private String email;
+
+ @Schema(example = "veryStrongAndSecurePassword")
+ @NotEmpty(message = "{user.password.not-empty}")
+ @Size(min = 8, max = 155, message = "{user.password.size}")
+ private String password;
+
+ public void validate() {
+ validateEmailUniqueness(this);
+ }
+
+ public User user() {
+ return new User(name, email, password, List.of());
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/dtos/UpdateUserProps.java b/api/src/main/java/com/github/throyer/example/modules/users/dtos/UpdateUserProps.java
new file mode 100755
index 00000000..50993daf
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/dtos/UpdateUserProps.java
@@ -0,0 +1,24 @@
+package com.github.throyer.example.modules.users.dtos;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+
+import com.github.throyer.example.modules.mail.models.Addressable;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class UpdateUserProps implements Addressable {
+
+ @Schema(example = "Jubileu da Silva Sauro")
+ @NotEmpty(message = "{user.name.not-empty}")
+ private String name;
+
+ @Schema(example = "jubileu.sauro@email.com")
+ @NotEmpty(message = "{user.email.not-empty}")
+ @Email(message = "{user.email.is-valid}")
+ private String email;
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/dtos/UserInformation.java b/api/src/main/java/com/github/throyer/example/modules/users/dtos/UserInformation.java
new file mode 100644
index 00000000..1a9194fc
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/dtos/UserInformation.java
@@ -0,0 +1,36 @@
+package com.github.throyer.example.modules.users.dtos;
+
+import java.util.List;
+
+import com.github.throyer.example.modules.shared.utils.HashIdsUtils;
+import com.github.throyer.example.modules.users.entities.User;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+public class UserInformation {
+
+ @Schema(example = "BVnl07r1Joz3", required = true)
+ private final String id;
+
+ @Schema(example = "Jubileu da Silva", required = true)
+ private final String name;
+
+ @Schema(example = "jubileu@email.com", required = true)
+ private final String email;
+
+ @Schema(example = "true", required = true)
+ private Boolean active;
+
+ @Schema(example = "[\"ADM\"]", required = true)
+ private final List roles;
+
+ public UserInformation(User user) {
+ this.id = HashIdsUtils.encode(user.getId());
+ this.name = user.getName();
+ this.email = user.getEmail();
+ this.active = user.isActive();
+ this.roles = user.getAuthorities();
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/entities/User.java b/api/src/main/java/com/github/throyer/example/modules/users/entities/User.java
new file mode 100755
index 00000000..81e101d4
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/entities/User.java
@@ -0,0 +1,160 @@
+package com.github.throyer.example.modules.users.entities;
+
+import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
+import static com.github.throyer.example.modules.infra.environments.SecurityEnvironments.ENCODER;
+import static com.github.throyer.example.modules.management.repositories.Queries.NON_DELETED_CLAUSE;
+import static com.github.throyer.example.modules.shared.utils.JSON.stringify;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Stream.of;
+import static javax.persistence.CascadeType.DETACH;
+import static javax.persistence.FetchType.LAZY;
+import static javax.persistence.GenerationType.IDENTITY;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.PrePersist;
+import javax.persistence.Table;
+import javax.persistence.Tuple;
+
+import org.hibernate.annotations.Where;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.throyer.example.modules.mail.models.Addressable;
+import com.github.throyer.example.modules.management.entities.Auditable;
+import com.github.throyer.example.modules.roles.entities.Role;
+import com.github.throyer.example.modules.users.dtos.UpdateUserProps;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "user")
+@Where(clause = NON_DELETED_CLAUSE)
+public class User extends Auditable implements Serializable, Addressable {
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "email", unique = true)
+ private String email;
+
+ @JsonIgnore
+ @Column(name = "deleted_email")
+ private String deletedEmail;
+
+ @JsonProperty(access = WRITE_ONLY)
+ @Column(name = "password", nullable = false)
+ private String password;
+
+ @ManyToMany(cascade = DETACH, fetch = LAZY)
+ @JoinTable(name = "user_role", joinColumns = {
+ @JoinColumn(name = "user_id") }, inverseJoinColumns = {
+ @JoinColumn(name = "role_id") })
+ private List roles;
+
+ public User() {
+ }
+
+ public User(Long id) {
+ this.id = id;
+ }
+
+ public User(String name, String email, String password, List roles) {
+ this.name = name;
+ this.email = email;
+ this.password = password;
+ this.roles = roles;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setId(BigInteger id) {
+ this.id = ofNullable(id)
+ .map(BigInteger::longValue)
+ .orElse(null);
+ }
+
+ public List getAuthorities() {
+ return ofNullable(roles)
+ .map(roles -> roles
+ .stream()
+ .map(Role::getAuthority)
+ .toList())
+ .orElseGet(() -> List.of());
+ }
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List roles) {
+ this.roles = roles;
+ }
+
+ @Override
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public String getEmail() {
+ return email;
+ }
+
+ public void merge(UpdateUserProps props) {
+ this.name = props.getName();
+ this.email = props.getEmail();
+ }
+
+ public void updatePassword(String newPassword) {
+ this.password = ENCODER.encode(newPassword);
+ }
+
+ public Boolean validatePassword(String password) {
+ return ENCODER.matches(password, this.password);
+ }
+
+ @PrePersist
+ private void created() {
+ this.password = ENCODER.encode(password);
+ }
+
+ @Override
+ public String toString() {
+ return stringify(Map.of(
+ "name", ofNullable(this.name).orElse(""),
+ "email", ofNullable(this.email).orElse("")));
+ }
+
+ public static User from(Tuple tuple) {
+ var user = new User();
+ user.setId(tuple.get("id", BigInteger.class));
+ user.setName(tuple.get("name", String.class));
+ user.setActive(tuple.get("active", Boolean.class));
+ user.setEmail(tuple.get("email", String.class));
+ user.setPassword(tuple.get("password", String.class));
+ user.setRoles(ofNullable(tuple.get("roles", String.class))
+ .map(roles -> of(roles.split(","))
+ .map(Role::new).toList())
+ .orElse(List.of()));
+ return user;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/Queries.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/Queries.java
new file mode 100755
index 00000000..0f79ecde
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/Queries.java
@@ -0,0 +1,74 @@
+package com.github.throyer.example.modules.users.repositories;
+
+public class Queries {
+ public static final String DELETE_USER_BY_ID = """
+ UPDATE
+ #{#entityName}
+ SET
+ deleted_email = (
+ SELECT
+ email
+ FROM
+ #{#entityName}
+ WHERE id = ?1),
+ email = NULL,
+ deleted_at = CURRENT_TIMESTAMP,
+ active = false,
+ deleted_by = ?#{principal?.id}
+ WHERE id = ?1
+ """;
+
+ public static final String COUNT_ENABLED_USERS = """
+ select
+ count(id)
+ from
+ "user"
+ where deleted_at is null
+ """;
+
+ public static final String FIND_ALL_USER_FETCH_ROLES = """
+ with user_roles as (
+ select
+ ur.user_id, string_agg(r.short_name, ',') roles
+ from "role" r
+ left join user_role ur on r.id = ur.role_id
+ group by ur.user_id
+ )
+
+ select
+ u.id,
+ u."name",
+ u.email,
+ u.password,
+ u.active,
+ urs.roles
+ from
+ "user" u
+ left join user_roles as urs on urs.user_id = u.id
+ where u.deleted_at is null
+ order by u.created_at desc
+ """;
+
+ public static final String FIND_BY_FIELD_FETCH_ROLES = """
+ with user_roles as (
+ select
+ ur.user_id, string_agg(r.short_name, ',') roles
+ from "role" r
+ left join user_role ur on r.id = ur.role_id
+ group by ur.user_id
+ )
+
+ select
+ u.id,
+ u."name",
+ u.email,
+ u.password,
+ u.active,
+ urs.roles
+ from
+ "user" u
+ left join user_roles as urs on urs.user_id = u.id
+ where u.deleted_at is null and %s
+ order by u.created_at desc
+ """;
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepository.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepository.java
new file mode 100755
index 00000000..227333fc
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepository.java
@@ -0,0 +1,24 @@
+package com.github.throyer.example.modules.users.repositories;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.users.entities.User;
+
+import java.util.Collection;
+import java.util.Optional;
+
+@Repository
+public interface UserRepository {
+ Page findAll(Pageable pageable);
+ Optional findById(Long id);
+ Optional findByIdFetchRoles(Long id);
+ Optional findByEmail(String email);
+ Boolean existsByEmail(String email);
+ void deleteById(Long id);
+ void deleteAll(Iterable extends User> entities);
+ void delete(User user);
+ User save(User user);
+ Collection saveAll(Collection users);
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepositoryImpl.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepositoryImpl.java
new file mode 100755
index 00000000..fb3a4c34
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/UserRepositoryImpl.java
@@ -0,0 +1,79 @@
+package com.github.throyer.example.modules.users.repositories;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.users.entities.User;
+import com.github.throyer.example.modules.users.repositories.custom.NativeQueryUserRepository;
+import com.github.throyer.example.modules.users.repositories.springdata.SpringDataUserRepository;
+
+import java.util.Collection;
+import java.util.Optional;
+
+@Repository
+public class UserRepositoryImpl implements UserRepository {
+
+ private final SpringDataUserRepository springData;
+ private final NativeQueryUserRepository queryNative;
+
+ @Autowired
+ public UserRepositoryImpl(
+ SpringDataUserRepository springData,
+ NativeQueryUserRepository queryNative
+ ) {
+ this.springData = springData;
+ this.queryNative = queryNative;
+ }
+
+ @Override
+ public Page findAll(Pageable pageable) {
+ return queryNative.findAll(pageable);
+ }
+
+ @Override
+ public Optional findById(Long id) {
+ return queryNative.findById(id);
+ }
+
+ @Override
+ public Optional findByIdFetchRoles(Long id) {
+ return this.springData.findByIdFetchRoles(id);
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ return queryNative.findByEmail(email);
+ }
+
+ @Override
+ public Boolean existsByEmail(String email) {
+ return springData.existsByEmail(email);
+ }
+
+ @Override
+ public void deleteById(Long id) {
+ springData.deleteById(id);
+ }
+
+ @Override
+ public void delete(User user) {
+ springData.delete(user);
+ }
+
+ @Override
+ public User save(User user) {
+ return springData.save(user);
+ }
+
+ @Override
+ public Collection saveAll(Collection users) {
+ return springData.saveAll(users);
+ }
+
+ @Override
+ public void deleteAll(Iterable extends User> users) {
+ springData.deleteAll(users);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepository.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepository.java
new file mode 100755
index 00000000..40714f05
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepository.java
@@ -0,0 +1,16 @@
+package com.github.throyer.example.modules.users.repositories.custom;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.users.entities.User;
+
+import java.util.Optional;
+
+@Repository
+public interface NativeQueryUserRepository {
+ Optional findById(Long id);
+ Optional findByEmail(String email);
+ Page findAll(Pageable pageable);
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepositoryImpl.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepositoryImpl.java
new file mode 100755
index 00000000..b8045330
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/custom/NativeQueryUserRepositoryImpl.java
@@ -0,0 +1,80 @@
+package com.github.throyer.example.modules.users.repositories.custom;
+
+import static com.github.throyer.example.modules.users.repositories.Queries.COUNT_ENABLED_USERS;
+import static com.github.throyer.example.modules.users.repositories.Queries.FIND_ALL_USER_FETCH_ROLES;
+import static com.github.throyer.example.modules.users.repositories.Queries.FIND_BY_FIELD_FETCH_ROLES;
+import static java.lang.String.format;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Optional;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.Tuple;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.users.entities.User;
+
+@Repository
+public class NativeQueryUserRepositoryImpl implements NativeQueryUserRepository {
+
+ @Autowired
+ EntityManager manager;
+
+ @Override
+ public Optional findById(Long id) {
+ var query = manager
+ .createNativeQuery(format(FIND_BY_FIELD_FETCH_ROLES, "u.id = :user_id"), Tuple.class)
+ .setParameter("user_id", id);
+ try {
+ var tuple = (Tuple) query.getSingleResult();
+ return of(User.from(tuple));
+ } catch (NoResultException exception) {
+ return empty();
+ }
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ var query = manager
+ .createNativeQuery(format(FIND_BY_FIELD_FETCH_ROLES, "u.email = :user_email"), Tuple.class)
+ .setParameter("user_email", email);
+ try {
+ var tuple = (Tuple) query.getSingleResult();
+ return of(User.from(tuple));
+ } catch (NoResultException exception) {
+ return empty();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Page findAll(Pageable pageable) {
+ var query = manager
+ .createNativeQuery(FIND_ALL_USER_FETCH_ROLES, Tuple.class);
+
+ var count = ((BigInteger) manager
+ .createNativeQuery(COUNT_ENABLED_USERS)
+ .getSingleResult())
+ .longValue();
+
+ var pageNumber = pageable.getPageNumber();
+ var pageSize = pageable.getPageSize();
+
+ query.setFirstResult(pageNumber * pageSize);
+ query.setMaxResults(pageSize);
+
+ List content = query.getResultList();
+
+ var users = content.stream().map(User::from).toList();
+
+ return Page.of(users, pageNumber, pageSize, count);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/repositories/springdata/SpringDataUserRepository.java b/api/src/main/java/com/github/throyer/example/modules/users/repositories/springdata/SpringDataUserRepository.java
new file mode 100755
index 00000000..171491f5
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/repositories/springdata/SpringDataUserRepository.java
@@ -0,0 +1,43 @@
+package com.github.throyer.example.modules.users.repositories.springdata;
+
+import static com.github.throyer.example.modules.users.repositories.Queries.DELETE_USER_BY_ID;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.github.throyer.example.modules.management.repositories.SoftDeleteRepository;
+import com.github.throyer.example.modules.users.entities.User;
+
+@Repository
+public interface SpringDataUserRepository extends SoftDeleteRepository {
+ @Override
+ @Transactional
+ @Modifying
+ @Query(DELETE_USER_BY_ID)
+ void deleteById(Long id);
+
+ @Override
+ @Transactional
+ default void delete(User user) {
+ deleteById(user.getId());
+ }
+
+ @Override
+ @Transactional
+ default void deleteAll(Iterable extends User> entities) {
+ entities.forEach(entity -> deleteById(entity.getId()));
+ }
+
+ Boolean existsByEmail(String email);
+
+ @Query("""
+ select user from #{#entityName} user
+ left join fetch user.roles
+ where user.id = ?1
+ """)
+ Optional findByIdFetchRoles(Long id);
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/service/CreateUserService.java b/api/src/main/java/com/github/throyer/example/modules/users/service/CreateUserService.java
new file mode 100755
index 00000000..18aed815
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/service/CreateUserService.java
@@ -0,0 +1,42 @@
+package com.github.throyer.example.modules.users.service;
+
+import static com.github.throyer.example.modules.mail.validations.EmailValidations.validateEmailUniqueness;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.github.throyer.example.modules.roles.repositories.RoleRepository;
+import com.github.throyer.example.modules.users.dtos.CreateUserProps;
+import com.github.throyer.example.modules.users.entities.User;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class CreateUserService {
+ private final UserRepository userRepository;
+ private final RoleRepository roleRepository;
+
+ @Autowired
+ public CreateUserService(
+ UserRepository userRepository,
+ RoleRepository roleRepository
+ ) {
+ this.userRepository = userRepository;
+ this.roleRepository = roleRepository;
+ }
+
+ public User create(CreateUserProps props) {
+ validateEmailUniqueness(props);
+
+ var roles = roleRepository.findOptionalByShortName("USER")
+ .map(List::of)
+ .orElseGet(List::of);
+
+ var name = props.getName();
+ var email = props.getEmail();
+ var password = props.getPassword();
+
+ return userRepository.save(new User(name, email, password, roles));
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/service/FindUserService.java b/api/src/main/java/com/github/throyer/example/modules/users/service/FindUserService.java
new file mode 100755
index 00000000..ce9bd202
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/service/FindUserService.java
@@ -0,0 +1,42 @@
+package com.github.throyer.example.modules.users.service;
+
+import static com.github.throyer.example.modules.infra.constants.MessagesConstants.NOT_AUTHORIZED_TO_READ;
+import static com.github.throyer.example.modules.infra.http.Responses.notFound;
+import static com.github.throyer.example.modules.infra.http.Responses.unauthorized;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.github.throyer.example.modules.authentication.models.Authorized;
+import com.github.throyer.example.modules.pagination.Page;
+import com.github.throyer.example.modules.pagination.utils.Pagination;
+import com.github.throyer.example.modules.users.entities.User;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class FindUserService {
+ private final UserRepository repository;
+
+ @Autowired
+ public FindUserService(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ public User find(Long id) {
+ Authorized.current()
+ .filter(authorized -> authorized.itsMeOrSessionIsADM(id))
+ .orElseThrow(() -> unauthorized(message(NOT_AUTHORIZED_TO_READ, "'user'")));
+
+ return repository
+ .findById(id)
+ .orElseThrow(() -> notFound("Not found"));
+ }
+
+ public Page find(Optional page, Optional size) {
+ var pageable = Pagination.of(page, size);
+ return repository.findAll(pageable);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/service/RemoveUserService.java b/api/src/main/java/com/github/throyer/example/modules/users/service/RemoveUserService.java
new file mode 100755
index 00000000..1bf2407f
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/service/RemoveUserService.java
@@ -0,0 +1,31 @@
+package com.github.throyer.example.modules.users.service;
+
+import static com.github.throyer.example.modules.infra.constants.MessagesConstants.NOT_AUTHORIZED_TO_MODIFY;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+import static com.github.throyer.example.modules.infra.http.Responses.notFound;
+import static com.github.throyer.example.modules.infra.http.Responses.unauthorized;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.github.throyer.example.modules.authentication.models.Authorized;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class RemoveUserService {
+ @Autowired
+ UserRepository repository;
+
+ public void remove(Long id) {
+ Authorized
+ .current()
+ .filter(authorized -> authorized.itsMeOrSessionIsADM(id))
+ .orElseThrow(() -> unauthorized(message(NOT_AUTHORIZED_TO_MODIFY, "'user'")));
+
+ var user = repository
+ .findById(id)
+ .orElseThrow(() -> notFound("User not found"));
+
+ repository.delete(user);
+ }
+}
diff --git a/api/src/main/java/com/github/throyer/example/modules/users/service/UpdateUserService.java b/api/src/main/java/com/github/throyer/example/modules/users/service/UpdateUserService.java
new file mode 100755
index 00000000..344ee8e7
--- /dev/null
+++ b/api/src/main/java/com/github/throyer/example/modules/users/service/UpdateUserService.java
@@ -0,0 +1,44 @@
+package com.github.throyer.example.modules.users.service;
+
+import static com.github.throyer.example.modules.infra.constants.MessagesConstants.NOT_AUTHORIZED_TO_MODIFY;
+import static com.github.throyer.example.modules.infra.http.Responses.notFound;
+import static com.github.throyer.example.modules.infra.http.Responses.unauthorized;
+import static com.github.throyer.example.modules.mail.validations.EmailValidations.validateEmailUniquenessOnModify;
+import static com.github.throyer.example.modules.shared.utils.InternationalizationUtils.message;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.github.throyer.example.modules.authentication.models.Authorized;
+import com.github.throyer.example.modules.users.dtos.UpdateUserProps;
+import com.github.throyer.example.modules.users.entities.User;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@Service
+public class UpdateUserService {
+ private final UserRepository repository;
+
+ @Autowired
+ public UpdateUserService(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ public User update(Long id, UpdateUserProps body) {
+ Authorized
+ .current()
+ .filter(authorized -> authorized.itsMeOrSessionIsADM(id))
+ .orElseThrow(() -> unauthorized(message(NOT_AUTHORIZED_TO_MODIFY, "'user'")));
+
+ var actual = repository
+ .findByIdFetchRoles(id)
+ .orElseThrow(() -> notFound("User not found"));
+
+ validateEmailUniquenessOnModify(body, actual);
+
+ actual.merge(body);
+
+ repository.save(actual);
+
+ return actual;
+ }
+}
diff --git a/api/src/main/java/db/migration/V1639097360419__CreateTableUser.java b/api/src/main/java/db/migration/V1639097360419__CreateTableUser.java
new file mode 100755
index 00000000..c51b5d30
--- /dev/null
+++ b/api/src/main/java/db/migration/V1639097360419__CreateTableUser.java
@@ -0,0 +1,38 @@
+package db.migration;
+
+import static org.jooq.impl.DSL.*;
+import static org.jooq.impl.SQLDataType.*;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+
+/**
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
+*/
+public class V1639097360419__CreateTableUser extends BaseJavaMigration {
+ public void migrate(Context context) throws Exception {
+ var create = using(context.getConnection());
+ create.transaction(configuration -> {
+ using(configuration)
+ .createTableIfNotExists("user")
+ .column("id", BIGINT.identity(true))
+ .column("name", VARCHAR(100).nullable(false))
+ .column("email", VARCHAR(100).nullable(true))
+ .column("deleted_email", VARCHAR(100).nullable(true))
+ .column("password", VARCHAR(100).nullable(false))
+ .column("active", BOOLEAN.defaultValue(true))
+ .column("created_at", TIMESTAMP.defaultValue(currentTimestamp()))
+ .column("updated_at", TIMESTAMP.nullable(true))
+ .column("deleted_at", TIMESTAMP.nullable(true))
+ .column("created_by", BIGINT.nullable(true))
+ .column("updated_by", BIGINT.nullable(true))
+ .column("deleted_by", BIGINT.nullable(true))
+ .constraints(
+ constraint("user_pk").primaryKey("id"),
+ constraint("user_unique_email").unique("email"),
+ constraint("user_created_by_fk").foreignKey("created_by").references("user", "id"),
+ constraint("user_updated_by_fk").foreignKey("updated_by").references("user", "id"),
+ constraint("user_deleted_by_fk").foreignKey("deleted_by").references("user", "id"))
+ .execute();
+ });
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/db/migration/V1639097454131__CreateTableRole.java b/api/src/main/java/db/migration/V1639097454131__CreateTableRole.java
new file mode 100755
index 00000000..e18bff3e
--- /dev/null
+++ b/api/src/main/java/db/migration/V1639097454131__CreateTableRole.java
@@ -0,0 +1,40 @@
+package db.migration;
+
+import static org.jooq.impl.DSL.*;
+import static org.jooq.impl.SQLDataType.*;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+
+/**
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
+*/
+public class V1639097454131__CreateTableRole extends BaseJavaMigration {
+ public void migrate(Context context) throws Exception {
+ var create = using(context.getConnection());
+ create.transaction(configuration -> {
+ using(configuration)
+ .createTableIfNotExists("role")
+ .column("id", BIGINT.identity(true))
+ .column("name", VARCHAR(100).nullable(false))
+ .column("deleted_name", VARCHAR(100).nullable(true))
+ .column("short_name", VARCHAR(100).nullable(true))
+ .column("deleted_short_name", VARCHAR(100).nullable(true))
+ .column("description", VARCHAR(100).nullable(true))
+ .column("active", BOOLEAN.defaultValue(true))
+ .column("created_at", TIMESTAMP.defaultValue(currentTimestamp()))
+ .column("updated_at", TIMESTAMP.nullable(true))
+ .column("deleted_at", TIMESTAMP.nullable(true))
+ .column("created_by", BIGINT.nullable(true))
+ .column("updated_by", BIGINT.nullable(true))
+ .column("deleted_by", BIGINT.nullable(true))
+ .constraints(
+ constraint("role_pk").primaryKey("id"),
+ constraint("role_unique_name").unique("name"),
+ constraint("role_unique_short_name").unique("short_name"),
+ constraint("role_created_by_fk").foreignKey("created_by").references("user", "id"),
+ constraint("role_updated_by_fk").foreignKey("updated_by").references("user", "id"),
+ constraint("role_deleted_by_fk").foreignKey("deleted_by").references("user", "id"))
+ .execute();
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java b/api/src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java
old mode 100644
new mode 100755
similarity index 61%
rename from src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java
rename to api/src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java
index 309f4a2e..2cf7551e
--- a/src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java
+++ b/api/src/main/java/db/migration/V1639097544500__CreateTableUserRoleManyToMany.java
@@ -6,7 +6,7 @@
import org.flywaydb.core.api.migration.Context;
/**
-* @see https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
*/
public class V1639097544500__CreateTableUserRoleManyToMany extends BaseJavaMigration {
public void migrate(Context context) throws Exception {
@@ -14,11 +14,11 @@ public void migrate(Context context) throws Exception {
create.transaction(configuration -> {
using(configuration)
.createTableIfNotExists("user_role")
- .column("user_id", BIGINT.nullable(true))
- .column("role_id", BIGINT.nullable(true))
+ .column("user_id", BIGINT.nullable(true))
+ .column("role_id", BIGINT.nullable(true))
.constraints(
- foreignKey("user_id").references("user", "id"),
- foreignKey("role_id").references("role", "id"))
+ constraint("user_role_fk").foreignKey("user_id").references("user", "id"),
+ constraint("role_user_fk").foreignKey("role_id").references("role", "id"))
.execute();
});
}
diff --git a/api/src/main/java/db/migration/V1639097619108__CreateTableRefreshToken.java b/api/src/main/java/db/migration/V1639097619108__CreateTableRefreshToken.java
new file mode 100755
index 00000000..70200ae6
--- /dev/null
+++ b/api/src/main/java/db/migration/V1639097619108__CreateTableRefreshToken.java
@@ -0,0 +1,29 @@
+package db.migration;
+
+import static org.jooq.impl.DSL.*;
+import static org.jooq.impl.SQLDataType.*;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+
+/**
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
+*/
+public class V1639097619108__CreateTableRefreshToken extends BaseJavaMigration {
+ public void migrate(Context context) throws Exception {
+ var create = using(context.getConnection());
+ create.transaction(configuration -> {
+ using(configuration)
+ .createTableIfNotExists("refresh_token")
+ .column("id", BIGINT.identity(true))
+ .column("code", VARCHAR(40).nullable(false))
+ .column("available", BOOLEAN.defaultValue(true))
+ .column("expires_at", TIMESTAMP.nullable(false))
+ .column("user_id", BIGINT.nullable(false))
+ .constraints(
+ constraint("refresh_token_pk").primaryKey("id"),
+ constraint("refresh_token_unique_code").unique("code"),
+ constraint("refresh_token_user_fk").foreignKey("user_id").references("user", "id"))
+ .execute();
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/db/migration/V1639097688772__CreateTableRecovery.java b/api/src/main/java/db/migration/V1639097688772__CreateTableRecovery.java
old mode 100644
new mode 100755
similarity index 51%
rename from src/main/java/db/migration/V1639097688772__CreateTableRecovery.java
rename to api/src/main/java/db/migration/V1639097688772__CreateTableRecovery.java
index 3bb18fe9..6f3d9a57
--- a/src/main/java/db/migration/V1639097688772__CreateTableRecovery.java
+++ b/api/src/main/java/db/migration/V1639097688772__CreateTableRecovery.java
@@ -6,7 +6,7 @@
import org.flywaydb.core.api.migration.Context;
/**
-* @see https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
*/
public class V1639097688772__CreateTableRecovery extends BaseJavaMigration {
public void migrate(Context context) throws Exception {
@@ -14,14 +14,14 @@ public void migrate(Context context) throws Exception {
create.transaction(configuration -> {
using(configuration)
.createTableIfNotExists("recovery")
- .column("id", BIGINT.identity(true))
- .column("code", VARCHAR(4).nullable(false))
- .column("expires_in", TIMESTAMP.nullable(false))
- .column("user_id", BIGINT.nullable(false))
+ .column("id", BIGINT.identity(true))
+ .column("code", VARCHAR(4).nullable(false))
+ .column("expires_at", TIMESTAMP.nullable(false))
+ .column("user_id", BIGINT.nullable(false))
.constraints(
- primaryKey("id"),
- unique("code"),
- foreignKey("user_id").references("user", "id"))
+ constraint("recovery_pk").primaryKey("id"),
+ constraint("recovery_unique_code").unique("code"),
+ constraint("recovery_user_fk").foreignKey("user_id").references("user", "id"))
.execute();
});
}
diff --git a/src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java b/api/src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java
old mode 100644
new mode 100755
similarity index 76%
rename from src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java
rename to api/src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java
index aa168e1d..710764cf
--- a/src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java
+++ b/api/src/main/java/db/migration/V1639098081278__UpdateTableRecovery.java
@@ -6,7 +6,7 @@
import org.flywaydb.core.api.migration.Context;
/**
-* @see https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/
+* @see "https://www.jooq.org/doc/3.1/manual/sql-building/ddl-statements/"
*/
public class V1639098081278__UpdateTableRecovery extends BaseJavaMigration {
public void migrate(Context context) throws Exception {
@@ -15,14 +15,12 @@ public void migrate(Context context) throws Exception {
using(configuration)
.alterTable("recovery")
.addColumn("confirmed", BOOLEAN.nullable(true))
- .after("code")
- .execute();
+ .execute();
using(configuration)
.alterTable("recovery")
.addColumn("used", BOOLEAN.nullable(true))
- .after("confirmed")
- .execute();
+ .execute();
});
}
}
\ No newline at end of file
diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties
new file mode 100755
index 00000000..d0bb8ecc
--- /dev/null
+++ b/api/src/main/resources/application.properties
@@ -0,0 +1,86 @@
+# todas funcionalidades: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html
+# Mais configuracoes: https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+# Porta do sistema.
+server.port=${SERVER_PORT:8080}
+
+# logger
+logging.level.root=info
+logging.level.org.springframework.security=error
+spring.output.ansi.enabled=always
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.show-sql=${DB_SHOW_SQL:true}
+
+# Banco de dados
+spring.datasource.hikari.maximum-pool-size=${DB_MAX_CONNECTIONS:5}
+spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:example}
+spring.datasource.username=${DB_USERNAME:root}
+spring.datasource.password=${DB_PASSWORD:root}
+spring.jpa.hibernate.ddl-auto=none
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.properties.javax.persistence.validation.mode=none
+spring.jpa.properties.hibernate.globally_quoted_identifiers=true
+spring.h2.console.enabled=false
+spring.jpa.open-in-view=false
+
+# swagger
+springdoc.swagger-ui.path=${SWAGGER_URL:/docs}
+swagger.username=${SWAGGER_USERNAME:#{null}}
+swagger.password=${SWAGGER_PASSWORD:#{null}}
+springdoc.default-produces-media-type=application/json
+springdoc.default-consumes-media-type=application/json
+
+# security
+token.expiration-in-hours=${TOKEN_EXPIRATION_IN_HOURS:24}
+token.refresh.expiration-in-days=${REFRESH_TOKEN_EXPIRATION_IN_DAYS:7}
+token.secret=${TOKEN_SECRET:secret}
+hashid.secret=${HASHID_SECRET:secret}
+cookie.secret=${COOKIE_SECRET:secret}
+server.servlet.session.cookie.name=${COOKIE_NAME:CONSESSIONARIA_SESSION_ID}
+server.servlet.session.cookie.path=/app
+
+# smtp configurations
+spring.mail.host=${SMTP_HOST:smtp.gmail.com}
+spring.mail.port=${SMTP_PORT:587}
+spring.mail.username=${SMTP_USERNAME:user@gmail}
+spring.mail.password=${SMTP_PASSWORD:secret}
+
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.connectiontimeout=5000
+spring.mail.properties.mail.smtp.timeout=5000
+spring.mail.properties.mail.smtp.writetimeout=5000
+spring.mail.properties.mail.smtp.starttls.enable=true
+
+# recovery email
+recovery.minutes-to-expire=${MINUTES_TO_EXPIRE_RECOVERY_CODE:20}
+
+# templates
+spring.resources.cache.period=0
+
+# encoding
+server.servlet.encoding.charset=UTF-8
+server.servlet.encoding.force-response=true
+
+# locale
+spring.web.locale=en
+spring.messages.encoding=UTF-8
+spring.messages.fallback-to-system-locale=false
+
+# rate limit
+spring.cache.jcache.config=classpath:ehcache.xml
+bucket4j.enabled=true
+bucket4j.filters[0].cache-name=buckets
+bucket4j.filters[0].filter-method=servlet
+bucket4j.filters[0].http-response-body={ "status": 249, "message": "Too many requests" }
+bucket4j.filters[0].url=/api/*
+bucket4j.filters[0].metrics.enabled=true
+bucket4j.filters[0].metrics.tags[0].key=IP
+bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[0].strategy=first
+bucket4j.filters[0].rate-limits[0].skip-condition=getRequestURI().contains('/swagger-ui') || getRequestURI().contains('/documentation')
+bucket4j.filters[0].rate-limits[0].expression=getRemoteAddr()
+bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=${MAX_REQUESTS_PER_MINUTE:50}
+bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
+bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
+bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval=0
+bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval-unit=minutes
\ No newline at end of file
diff --git a/api/src/main/resources/db/migration/V1639098485493__insert_admin_and_roles.sql b/api/src/main/resources/db/migration/V1639098485493__insert_admin_and_roles.sql
new file mode 100755
index 00000000..4718cbe6
--- /dev/null
+++ b/api/src/main/resources/db/migration/V1639098485493__insert_admin_and_roles.sql
@@ -0,0 +1,25 @@
+-- insert admin
+INSERT INTO "user"
+ ("name", "email", "password")
+VALUES
+ ('admin', 'admin@email.com', '$2a$10$QBuMJLbVmHzgvTwpxDynSetACNdCBjU5zgo.81RWEDzH46aUrgcNK');
+
+-- insert roles
+INSERT INTO "role"
+ ("name", "short_name", "description", "created_by")
+VALUES
+ ('ADMINISTRADOR', 'ADM', 'Administrador do sistema', (SELECT "id" FROM "user" WHERE "email" = 'admin@email.com')),
+ ('USER', 'USER', 'Usuário do sistema', (SELECT "id" FROM "user" WHERE "email" = 'admin@email.com'));
+
+-- put roles into admin
+INSERT INTO user_role
+ ("user_id", "role_id")
+VALUES
+ (
+ (SELECT id FROM "user" WHERE "email" = 'admin@email.com'),
+ (SELECT id FROM "role" WHERE "short_name" = 'ADM')
+ ),
+ (
+ (SELECT id FROM "user" WHERE "email" = 'admin@email.com'),
+ (SELECT id FROM "role" WHERE "short_name" = 'USER')
+ );
\ No newline at end of file
diff --git a/api/src/main/resources/ehcache.xml b/api/src/main/resources/ehcache.xml
new file mode 100755
index 00000000..d43802ca
--- /dev/null
+++ b/api/src/main/resources/ehcache.xml
@@ -0,0 +1,14 @@
+
+
+
+ 3600
+
+ 1000000
+
+
+
+
\ No newline at end of file
diff --git a/api/src/main/resources/messages.properties b/api/src/main/resources/messages.properties
new file mode 100755
index 00000000..ad627ee0
--- /dev/null
+++ b/api/src/main/resources/messages.properties
@@ -0,0 +1,38 @@
+# session
+create.session.error.message=Invalid password or username.
+refresh.session.error.message=Refresh token expired or invalid.
+invalid.username.error.message=Username invalid.
+
+# toast messages
+registration.success=Registration successfully completed.
+
+# email
+email.already-used.error.message=This email has already been used by another user. Please use a different email.
+
+# exception
+type.mismatch.message=Type mismatch error.
+
+# not authorized
+not.authorized.list=Not authorized to list this user's {0}.
+not.authorized.read=Not authorized to read this user's {0}.
+not.authorized.create=Not authorized to create {0} for this user's.
+not.authorized.modify=Not authorized to modify this user's {0}.
+
+# recovery
+recovery.email.not-empty=Email is a required field.
+recovery.email.is-valid=Email is not valid.
+recovery.code.not-empty=Code is a required field.
+
+# user
+user.email.not-empty=Email is a required field.
+user.email.is-valid=Email is not valid.
+user.name.not-empty=Name is a required field.
+user.password.not-empty=Password is a required field.
+user.password.size=The password must contain at least {min} characters.
+
+# token
+token.refresh-token.not-null=refresh_token is a required field.
+token.email.not-null=Email is a required field.
+token.password.not-null=Password is a required field.
+token.expired-or-invalid=Token expired or invalid.
+token.header.missing=Can't find token on Authorization header.
\ No newline at end of file
diff --git a/api/src/main/resources/messages_pt_BR.properties b/api/src/main/resources/messages_pt_BR.properties
new file mode 100755
index 00000000..13fa7184
--- /dev/null
+++ b/api/src/main/resources/messages_pt_BR.properties
@@ -0,0 +1,38 @@
+# session
+create.session.error.message=Senha ou usuário invalido.
+refresh.session.error.message=Refresh token expirado ou invalido.
+invalid.username.error.message=Nome de usuário invalido.
+
+# exceptions
+type.mismatch.message=Erro de compatibilidade de tipo.
+
+# email
+email.already-used.error.message=Este e-mail já foi usado por outro usuário. Por favor, use um e-mail diferente.
+
+# not authorized
+not.authorized.list=Não autorizado a listar {0} deste usuário.
+not.authorized.read=Não autorizado a visualizar {0} deste usuário.
+not.authorized.create=Não autorizado a criar {0} para este usuário.
+not.authorized.modify=Não autorizado a modificar {0} deste usuário.
+
+# toast messages
+registration.success=Cadastro realizado com sucesso.
+
+# recovery
+recovery.email.not-empty=E-mail é obrigatório.
+recovery.email.is-valid=E-mail invalido.
+recovery.code.not-empty=O Código é obrigatório.
+
+# user
+user.email.not-empty=E-mail é obrigatório.
+user.email.is-valid=E-mail invalido.
+user.name.not-empty=Nome é obrigatório.
+user.password.not-empty=A senha é obrigatória.
+user.password.size=A senha deve conter no mínimo {min} caracteres.
+
+# token
+token.refresh-token.not-null=O campo refresh_token é obrigatório.
+token.email.not-null=Email é obrigatório.
+token.password.not-null=A senha é obrigatória.
+token.expired-or-invalid=Token expirado or invalido.
+token.header.missing=Não é possível encontrar o token no cabeçalho de autorização.
\ No newline at end of file
diff --git a/src/main/resources/static/css/center-forms.css b/api/src/main/resources/static/css/center-forms.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/center-forms.css
rename to api/src/main/resources/static/css/center-forms.css
diff --git a/src/main/resources/static/css/codes.css b/api/src/main/resources/static/css/codes.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/codes.css
rename to api/src/main/resources/static/css/codes.css
diff --git a/src/main/resources/static/css/forms.css b/api/src/main/resources/static/css/forms.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/forms.css
rename to api/src/main/resources/static/css/forms.css
diff --git a/src/main/resources/static/css/home.css b/api/src/main/resources/static/css/home.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/home.css
rename to api/src/main/resources/static/css/home.css
diff --git a/src/main/resources/static/css/login.css b/api/src/main/resources/static/css/login.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/login.css
rename to api/src/main/resources/static/css/login.css
diff --git a/api/src/main/resources/static/css/styles.css b/api/src/main/resources/static/css/styles.css
new file mode 100755
index 00000000..6866b93d
--- /dev/null
+++ b/api/src/main/resources/static/css/styles.css
@@ -0,0 +1,79 @@
+body {
+ -webkit-font-smoothing: antialiased;
+}
+
+body,
+input,
+button {
+ font-family: 'Poppins', sans-serif;
+}
+
+@media (min-width: 768px) {
+ .animate {
+ animation-duration: 0.3s;
+ -webkit-animation-duration: 0.3s;
+ animation-fill-mode: both;
+ -webkit-animation-fill-mode: both;
+ }
+}
+
+@keyframes slideIn {
+ 0% {
+ transform: translateY(1rem);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0rem);
+ opacity: 1;
+ }
+
+ 0% {
+ transform: translateY(1rem);
+ opacity: 0;
+ }
+}
+
+@-webkit-keyframes slideIn {
+ 0% {
+ -webkit-transform: transform;
+ -webkit-opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform: translateY(0);
+ -webkit-opacity: 1;
+ }
+
+ 0% {
+ -webkit-transform: translateY(1rem);
+ -webkit-opacity: 0;
+ }
+}
+
+.slideIn {
+ -webkit-animation-name: slideIn;
+ animation-name: slideIn;
+}
+
+.pagination-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: 2rem;
+}
+
+#sizes_input {
+ cursor: pointer;
+}
+
+.dropdown-menu {
+ gap: .25rem;
+ padding: .5rem;
+ border-radius: .5rem;
+}
+
+.dropdown-menu .dropdown-item {
+ border-radius: .25rem;
+}
\ No newline at end of file
diff --git a/src/main/resources/static/css/users.css b/api/src/main/resources/static/css/users.css
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/css/users.css
rename to api/src/main/resources/static/css/users.css
diff --git a/src/main/resources/static/favicon.ico b/api/src/main/resources/static/favicon.ico
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/favicon.ico
rename to api/src/main/resources/static/favicon.ico
diff --git a/src/main/resources/static/js/codes.js b/api/src/main/resources/static/js/codes.js
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/js/codes.js
rename to api/src/main/resources/static/js/codes.js
diff --git a/src/main/resources/static/js/forms.js b/api/src/main/resources/static/js/forms.js
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/js/forms.js
rename to api/src/main/resources/static/js/forms.js
diff --git a/src/main/resources/static/js/main.js b/api/src/main/resources/static/js/main.js
old mode 100644
new mode 100755
similarity index 60%
rename from src/main/resources/static/js/main.js
rename to api/src/main/resources/static/js/main.js
index 02f958ca..d9de9289
--- a/src/main/resources/static/js/main.js
+++ b/api/src/main/resources/static/js/main.js
@@ -1,5 +1,5 @@
const form = document.querySelector("#sizes_form");
-form.addEventListener("change", () => {
+form && form.addEventListener("change", () => {
form.submit();
});
\ No newline at end of file
diff --git a/src/main/resources/static/js/users-modal.js b/api/src/main/resources/static/js/users-modal.js
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/js/users-modal.js
rename to api/src/main/resources/static/js/users-modal.js
diff --git a/src/main/resources/static/robots.txt b/api/src/main/resources/static/robots.txt
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/static/robots.txt
rename to api/src/main/resources/static/robots.txt
diff --git a/src/main/resources/templates/app/fragments/footer.html b/api/src/main/resources/templates/app/fragments/footer.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/fragments/footer.html
rename to api/src/main/resources/templates/app/fragments/footer.html
diff --git a/src/main/resources/templates/app/fragments/imports.html b/api/src/main/resources/templates/app/fragments/imports.html
old mode 100644
new mode 100755
similarity index 85%
rename from src/main/resources/templates/app/fragments/imports.html
rename to api/src/main/resources/templates/app/fragments/imports.html
index d3fa6a18..99f245d2
--- a/src/main/resources/templates/app/fragments/imports.html
+++ b/api/src/main/resources/templates/app/fragments/imports.html
@@ -1,12 +1,12 @@
-
+
-
+
diff --git a/api/src/main/resources/templates/app/fragments/layout.html b/api/src/main/resources/templates/app/fragments/layout.html
new file mode 100755
index 00000000..9e54a391
--- /dev/null
+++ b/api/src/main/resources/templates/app/fragments/layout.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Spring Boot example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/app/fragments/me.html b/api/src/main/resources/templates/app/fragments/me.html
old mode 100644
new mode 100755
similarity index 83%
rename from src/main/resources/templates/app/fragments/me.html
rename to api/src/main/resources/templates/app/fragments/me.html
index f6d26c22..519bf712
--- a/src/main/resources/templates/app/fragments/me.html
+++ b/api/src/main/resources/templates/app/fragments/me.html
@@ -5,10 +5,10 @@
About me
-
+
Name:
-
+
Roles:
diff --git a/src/main/resources/templates/app/fragments/navbar.html b/api/src/main/resources/templates/app/fragments/navbar.html
old mode 100644
new mode 100755
similarity index 96%
rename from src/main/resources/templates/app/fragments/navbar.html
rename to api/src/main/resources/templates/app/fragments/navbar.html
index 8b5bc5f4..c7bffdab
--- a/src/main/resources/templates/app/fragments/navbar.html
+++ b/api/src/main/resources/templates/app/fragments/navbar.html
@@ -31,6 +31,7 @@
Swagger docs
@@ -40,6 +41,7 @@
Repository
@@ -66,7 +68,7 @@
Show more
-
+
Me
diff --git a/src/main/resources/templates/app/fragments/toasts.html b/api/src/main/resources/templates/app/fragments/toasts.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/fragments/toasts.html
rename to api/src/main/resources/templates/app/fragments/toasts.html
diff --git a/api/src/main/resources/templates/app/index.html b/api/src/main/resources/templates/app/index.html
new file mode 100755
index 00000000..72743f9b
--- /dev/null
+++ b/api/src/main/resources/templates/app/index.html
@@ -0,0 +1,173 @@
+
+ Home
+
+
+
+
+
+
+ Simple Spring Boot API
+
+ C.R.U.D
+
+
+
+ This is a simple API and WEB APP.
+
+
+ Project with a crud of users with as
+ many good practices as I can.
+
+
+
+
Features
+
+
+
+
+
+
+
JWT
+
+ Spring security configuration,
+ roles and session.
+ JSON web token and refresh token
+
+
+
+
+
+
+
+
+
Tests
+
+ Tests with JUnity, Test coverage report with Jacoco.
+ Memory database tests H2 and Spring mock mvc
+ integration tests
+
+
+
+
+
+
+
+
+
Database migrations
+
+ Database migrations using flywaydb.
+ They are are created using
+
+ Java
+
+ through JOOQ query builder
+
+
+
+
+
+
+
+
+
Swagger docs
+
+ Swagger documentation generated
+ automatically from api controllers
+
+
+
+
+
+
+
+
+
Internationalization
+
+ support for 'Accept-Language' header, Portuguese and English
+
+
+
+
+
+
+
+
+
SMTP integration
+
+ Integration with SMTP
+ service for sending emails
+
+
+
+
+
+
+
+
+
Soft delete
+
+ Soft delete implementation using spring data repository
+
+
+
+
+
+
+
+
+
Lazy load
+
+ Lazy load relationships on entities
+
+
+
+
+
+
+
+
+
Audit fields
+
+ Audit fields in entities like created_at, updated_at etc ...
+
+
+
+
+
+
+
+
+
Docker
+
+ Docker compose configuration, back-end and database
+
+
+
+
+
+
+
+
+
Password recovery
+
+ Password recovery with code and validation
+
+
+
+
+
+
+
+
+
CRUD
+
+ User registration and management screen
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/src/main/resources/templates/app/login/index.html b/api/src/main/resources/templates/app/login/index.html
new file mode 100755
index 00000000..78d652e6
--- /dev/null
+++ b/api/src/main/resources/templates/app/login/index.html
@@ -0,0 +1,68 @@
+
+ Login
+
+
+
+
+
+
+
+
Lorem ipsum dolor sit amet
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit.
+ In excepturi deleniti asperiores? Quasi asperiores voluptatum
+ aut placeat ad nesciunt ut explicabo? Itaque impedit
+ inventore debitis velit, reiciendis facilis obcaecati
+ voluptatibus?
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/app/recovery/confirm.html b/api/src/main/resources/templates/app/recovery/confirm.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/recovery/confirm.html
rename to api/src/main/resources/templates/app/recovery/confirm.html
diff --git a/src/main/resources/templates/app/recovery/index.html b/api/src/main/resources/templates/app/recovery/index.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/recovery/index.html
rename to api/src/main/resources/templates/app/recovery/index.html
diff --git a/src/main/resources/templates/app/recovery/update.html b/api/src/main/resources/templates/app/recovery/update.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/recovery/update.html
rename to api/src/main/resources/templates/app/recovery/update.html
diff --git a/src/main/resources/templates/app/register/fragments/form-register.html b/api/src/main/resources/templates/app/register/fragments/form-register.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/register/fragments/form-register.html
rename to api/src/main/resources/templates/app/register/fragments/form-register.html
diff --git a/src/main/resources/templates/app/register/index.html b/api/src/main/resources/templates/app/register/index.html
old mode 100644
new mode 100755
similarity index 82%
rename from src/main/resources/templates/app/register/index.html
rename to api/src/main/resources/templates/app/register/index.html
index cd49fdfc..6a29c772
--- a/src/main/resources/templates/app/register/index.html
+++ b/api/src/main/resources/templates/app/register/index.html
@@ -6,7 +6,7 @@
diff --git a/api/src/main/resources/templates/app/users/form.html b/api/src/main/resources/templates/app/users/form.html
new file mode 100755
index 00000000..8e877b7b
--- /dev/null
+++ b/api/src/main/resources/templates/app/users/form.html
@@ -0,0 +1,19 @@
+
+ Users
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/app/users/fragments/modal.html b/api/src/main/resources/templates/app/users/fragments/modal.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/app/users/fragments/modal.html
rename to api/src/main/resources/templates/app/users/fragments/modal.html
diff --git a/api/src/main/resources/templates/app/users/fragments/table.html b/api/src/main/resources/templates/app/users/fragments/table.html
new file mode 100755
index 00000000..7c3e8279
--- /dev/null
+++ b/api/src/main/resources/templates/app/users/fragments/table.html
@@ -0,0 +1,114 @@
+
+
+
+
+ Name
+ Email
+ Status
+ Roles
+ Actions
+
+
+
+
+ name
+ email
+
+ active
+
+
+ role
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/src/main/resources/templates/app/users/index.html b/api/src/main/resources/templates/app/users/index.html
new file mode 100755
index 00000000..53de6c79
--- /dev/null
+++ b/api/src/main/resources/templates/app/users/index.html
@@ -0,0 +1,24 @@
+
+ Users
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/recovery-password.html b/api/src/main/resources/templates/recovery-password.html
old mode 100644
new mode 100755
similarity index 100%
rename from src/main/resources/templates/recovery-password.html
rename to api/src/main/resources/templates/recovery-password.html
diff --git a/src/test/java/com/github/throyer/common/springboot/CommonApiApplicationTests.java b/api/src/test/java/com/github/throyer/example/MainTests.java
old mode 100644
new mode 100755
similarity index 64%
rename from src/test/java/com/github/throyer/common/springboot/CommonApiApplicationTests.java
rename to api/src/test/java/com/github/throyer/example/MainTests.java
index 87eb838b..53d878a9
--- a/src/test/java/com/github/throyer/common/springboot/CommonApiApplicationTests.java
+++ b/api/src/test/java/com/github/throyer/example/MainTests.java
@@ -1,12 +1,10 @@
-package com.github.throyer.common.springboot;
+package com.github.throyer.example;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
-class CommonApiApplicationTests {
-
+class MainTests {
@Test
void contextLoads() { }
-
}
diff --git a/api/src/test/java/com/github/throyer/example/modules/authentication/controllers/AuthenticationControllerTests.java b/api/src/test/java/com/github/throyer/example/modules/authentication/controllers/AuthenticationControllerTests.java
new file mode 100755
index 00000000..ad962d34
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/authentication/controllers/AuthenticationControllerTests.java
@@ -0,0 +1,161 @@
+package com.github.throyer.example.modules.authentication.controllers;
+
+import static com.github.throyer.example.modules.shared.utils.Random.token;
+import static com.github.throyer.example.modules.shared.utils.Random.user;
+import static java.time.LocalDateTime.now;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.github.throyer.example.modules.shared.utils.JSON;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
+@AutoConfigureDataJpa
+@AutoConfigureMockMvc
+class AuthenticationControllerTests {
+
+ @Autowired
+ private MockMvc api;
+
+ @Autowired
+ UserRepository repository;
+
+ @Test
+ @DisplayName("deve retornar OK quando a senha estiver correta")
+ void should_return_OK_when_password_is_correct() throws Exception {
+
+ var user = user();
+
+ var body = JSON.stringify(Map.of(
+ "email", user.getEmail(),
+ "password", user.getPassword()));
+
+ repository.save(user);
+
+ api.perform(post("/api/authentication")
+ .content(body)
+ .header(CONTENT_TYPE, APPLICATION_JSON))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @DisplayName("deve retornar FORBIDDEN quando a senha estiver incorreta")
+ void should_return_FORBIDDEN_when_password_is_incorrect() throws Exception {
+ var user = repository.save(user());
+
+ var body = JSON.stringify(Map.of(
+ "email", user.getEmail(),
+ "password", "Írineu! você não sabe, nem eu!"));
+
+ api.perform(post("/api/authentication")
+ .content(body)
+ .header(CONTENT_TYPE, APPLICATION_JSON))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @DisplayName("deve retornar FORBIDDEN quando o usuário não existir")
+ void should_return_FORBIDDEN_when_user_does_not_exist() throws Exception {
+ var body = JSON.stringify(Map.of(
+ "email", "this.not.exist@email.com",
+ "password", "Írineu! você não sabe, nem eu!"));
+
+ api.perform(post("/api/authentication")
+ .content(body)
+ .header(CONTENT_TYPE, APPLICATION_JSON))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @DisplayName("não deve aceitar requisições sem o token no cabeçalho quando as rotas forem privadas")
+ void should_not_accept_requests_without_token_in_header_when_routes_are_private() throws Exception {
+ api.perform(get("/api/users"))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Can't find bearer token on Authorization header."));
+ }
+
+ @Test
+ @DisplayName("não deve aceitar requisições com o token expirado")
+ void should_not_accept_requests_with_token_expired() throws Exception {
+ var expiredToken = token(now().minusHours(24), "ADM");
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, expiredToken))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Token expired or invalid."));
+ }
+
+ @Test
+ @DisplayName("deve aceitar requisições com o token um válido")
+ void should_accept_requests_with_token_valid() throws Exception {
+ var token = token("ADM");
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, token))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content").isArray());
+ }
+
+ @Test
+ @DisplayName("não deve aceitar requisições com o token um inválido")
+ void must_not_accept_requests_with_an_invalid_token() throws Exception {
+ var token = token(now().plusHours(24), "ADM", "this_is_not_my_secret");
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, token))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Token expired or invalid."));
+ }
+
+ @Test
+ @DisplayName("não deve aceitar requisições com o token usando um hash inválido")
+ void must_not_accept_requests_with_an_invalid_hash_token() throws Exception {
+ var token = "Bearer abububle_das_ideia";
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, token))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Token expired or invalid."));
+ }
+
+ @Test
+ @DisplayName("não deve aceitar requisições com o token sem a role correta")
+ void must_not_accept_requests_with_token_without_the_correct_role() throws Exception {
+ var token = token("THIS_IS_NOT_CORRECT_ROLE");
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, token))
+ .andExpect(status().isUnauthorized())
+ .andExpect(jsonPath("$.message").value("Not authorized."));
+ }
+
+ @Test
+ @DisplayName("deve aceitar requisições sem token em rotas publicas")
+ void must_accept_requests_without_token_on_public_routes() throws Exception {
+ api.perform(get("/api"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @DisplayName("deve aceitar requisições sem token em rotas publicas porem com um token invalido no header")
+ void must_accept_requests_without_token_on_public_routes_but_with_invalid_token_on_header() throws Exception {
+ api.perform(get("/api").header(AUTHORIZATION, token("ADM", "this_is_not_my_secret")))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/api/src/test/java/com/github/throyer/example/modules/infra/InternationalizationTests.java b/api/src/test/java/com/github/throyer/example/modules/infra/InternationalizationTests.java
new file mode 100755
index 00000000..c9339438
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/infra/InternationalizationTests.java
@@ -0,0 +1,53 @@
+package com.github.throyer.example.modules.infra;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@AutoConfigureDataJpa
+@AutoConfigureMockMvc
+class InternationalizationTests {
+
+ @Autowired
+ private MockMvc api;
+
+ @Test
+ @DisplayName("Deve retornar as mensagens de erro em pt BR.")
+ void should_return_error_messages_in_pt_BR() throws Exception {
+ var body = "{ \"password\": \"senha_bem_segura_1234\", \"email\": \"email@email.com\" }";
+
+ api.perform(post("/api/authentication")
+ .content(body)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.ACCEPT_LANGUAGE, "pt-BR"))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Senha ou usuário invalido."));
+ }
+
+ @Test
+ @DisplayName("Deve retornar as mensagens de erro em Inglês.")
+ void should_return_error_messages_in_english() throws Exception {
+ var body = "{ \"password\": \"senha_bem_segura_1234\", \"email\": \"email@email.com\" }";
+
+ api.perform(post("/api/authentication")
+ .content(body)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.ACCEPT_LANGUAGE, "en-US"))
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value("Invalid password or username."));
+ ;
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/github/throyer/example/modules/infra/RateLimitTests.java b/api/src/test/java/com/github/throyer/example/modules/infra/RateLimitTests.java
new file mode 100755
index 00000000..ae266368
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/infra/RateLimitTests.java
@@ -0,0 +1,46 @@
+package com.github.throyer.example.modules.infra;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static com.github.throyer.example.modules.infra.environments.RateLimitEnvironments.MAX_REQUESTS_PER_MINUTE;
+import static java.lang.String.valueOf;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@TestInstance(PER_CLASS)
+@SpringBootTest(webEnvironment = MOCK)
+@DirtiesContext(classMode = BEFORE_CLASS)
+class RateLimitTests {
+ @Autowired
+ private MockMvc api;
+
+ @Test
+ @DisplayName("Deve retornar TOO_MANY_REQUESTS quando a quantidade de requests passar do limite.")
+ void should_return_TOO_MANY_REQUESTS_when_number_of_requests_exceeds_the_limit() throws Exception {
+ var request = get("/api")
+ .header(CONTENT_TYPE, APPLICATION_JSON);
+
+ for (int index = MAX_REQUESTS_PER_MINUTE; index > 0; index--) {
+ api.perform(request)
+ .andExpect(header().string("X-Rate-Limit-Remaining", valueOf(index - 1)))
+ .andExpect(status().isOk());
+ }
+
+ api.perform(request)
+ .andExpect(status().isTooManyRequests());
+ }
+}
diff --git a/api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerAuthTests.java b/api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerAuthTests.java
new file mode 100755
index 00000000..62de3df3
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerAuthTests.java
@@ -0,0 +1,47 @@
+package com.github.throyer.example.modules.infra.swagger;
+
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
+@AutoConfigureMockMvc
+@TestPropertySource(properties = {
+ "swagger.username=test",
+ "swagger.password=test"
+})
+class SwaggerAuthTests {
+
+ @Autowired
+ private MockMvc api;
+
+ @Test
+ @DisplayName("Deve exibir a documentação com basic auth valido")
+ void should_display_the_swagger_docs_ui_with_valid_credentials() throws Exception {
+
+ var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config")
+ .header(AUTHORIZATION, "Basic dGVzdDp0ZXN0");
+
+ api.perform(request)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @DisplayName("Não deve exibir a documentação com basic auth invalido")
+ void must_not_display_the_swagger_docs_ui_with_invalid_credentials() throws Exception {
+ var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config")
+ .header(AUTHORIZATION, "Basic anViaWxldTppcmluZXU=");
+
+ api.perform(request)
+ .andExpect(status().isUnauthorized());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/github/throyer/common/springboot/SwaggerTests.java b/api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerTests.java
old mode 100644
new mode 100755
similarity index 55%
rename from src/test/java/com/github/throyer/common/springboot/SwaggerTests.java
rename to api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerTests.java
index ea5a80ce..eb61eb8b
--- a/src/test/java/com/github/throyer/common/springboot/SwaggerTests.java
+++ b/api/src/test/java/com/github/throyer/example/modules/infra/swagger/SwaggerTests.java
@@ -1,7 +1,6 @@
-package com.github.throyer.common.springboot;
+package com.github.throyer.example.modules.infra.swagger;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.DisplayName;
@@ -14,19 +13,16 @@
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
-public class SwaggerTests {
+class SwaggerTests {
- @Autowired
- private MockMvc api;
+ @Autowired
+ private MockMvc api;
- @Test
- @DisplayName("Deve exibir a documentação | swagger ui")
- public void should_show_swagger_docs_ui() throws Exception {
-
- var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config");
-
- api.perform(request)
- .andDo(print())
- .andExpect(status().isOk());
- }
+ @Test
+ @DisplayName("Deve exibir a documentação | swagger ui")
+ void should_show_swagger_docs_ui() throws Exception {
+ var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config");
+ api.perform(request)
+ .andExpect(status().isOk());
+ }
}
\ No newline at end of file
diff --git a/api/src/test/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesControllerTests.java b/api/src/test/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesControllerTests.java
new file mode 100644
index 00000000..a517d625
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/recoveries/controllers/RecoveriesControllerTests.java
@@ -0,0 +1,114 @@
+package com.github.throyer.example.modules.recoveries.controllers;
+
+import static com.github.throyer.example.modules.shared.utils.Random.user;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.github.throyer.example.modules.recoveries.repositories.RecoveryRepository;
+import com.github.throyer.example.modules.shared.utils.JSON;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
+@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
+@AutoConfigureDataJpa
+@AutoConfigureMockMvc
+class RecoveriesControllerTests {
+
+ @Autowired
+ private RecoveryRepository recoveryRepository;
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private MockMvc api;
+
+ @Test
+ @DisplayName("Deve responder No Content quando email for invalido.")
+ void should_return_no_content_on_invalid_email() throws Exception {
+
+ var json = JSON.stringify(Map.of(
+ "email", "jubileu.da.silva@email.fake.com"));
+
+ api.perform(
+ post("/api/recoveries")
+ .header(CONTENT_TYPE, APPLICATION_JSON)
+ .content(json))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @DisplayName("Deve responder No Content quando email for valido.")
+ void should_return_no_content_on_valid_email() throws Exception {
+
+ var user = user();
+
+ userRepository.save(user);
+
+ var json = JSON.stringify(Map.of(
+ "email", user.getEmail()));
+
+ api.perform(
+ post("/api/recoveries")
+ .header(CONTENT_TYPE, APPLICATION_JSON)
+ .content(json))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @DisplayName("Deve confirmar o código quando ele for valido for valido.")
+ void should_confirm_code_when_is_valid() throws Exception {
+
+ var user = userRepository.save(user());
+
+ var recoveryBody = JSON.stringify(Map.of(
+ "email", user.getEmail()));
+
+ api.perform(
+ post("/api/recoveries")
+ .header(CONTENT_TYPE, APPLICATION_JSON)
+ .content(recoveryBody))
+ .andExpect(status().isNoContent());
+
+ var unconfirmedRecovery = recoveryRepository
+ .findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new RuntimeException("recovery code not found"));
+
+ assertFalse(unconfirmedRecovery.isConfirmed());
+ assertFalse(unconfirmedRecovery.isUsed());
+
+ var confirmBody = JSON.stringify(Map.of(
+ "email", user.getEmail(),
+ "code", unconfirmedRecovery.getCode()));
+
+ api.perform(
+ post("/api/recoveries/confirm")
+ .header(CONTENT_TYPE, APPLICATION_JSON)
+ .content(confirmBody))
+ .andExpect(status().isNoContent());
+
+ var confirmedRecovery = recoveryRepository
+ .findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresAtDesc(user.getId())
+ .orElseThrow(() -> new RuntimeException("recovery code not found"));
+
+ assertTrue(confirmedRecovery.isConfirmed());
+ assertFalse(unconfirmedRecovery.isUsed());
+ }
+}
diff --git a/api/src/test/java/com/github/throyer/example/modules/users/controllers/UsersControllerTests.java b/api/src/test/java/com/github/throyer/example/modules/users/controllers/UsersControllerTests.java
new file mode 100755
index 00000000..eb83d50b
--- /dev/null
+++ b/api/src/test/java/com/github/throyer/example/modules/users/controllers/UsersControllerTests.java
@@ -0,0 +1,172 @@
+package com.github.throyer.example.modules.users.controllers;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.github.throyer.example.modules.shared.utils.HashIdsUtils;
+import com.github.throyer.example.modules.shared.utils.JSON;
+import com.github.throyer.example.modules.users.repositories.UserRepository;
+
+import java.util.Map;
+
+import static com.github.throyer.example.modules.shared.utils.JSON.stringify;
+import static com.github.throyer.example.modules.shared.utils.Random.*;
+import static java.lang.String.format;
+import static org.hamcrest.Matchers.*;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Transactional
+@AutoConfigureDataJpa
+@AutoConfigureMockMvc
+@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
+@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class UsersControllerTests {
+
+ @Autowired
+ UserRepository repository;
+
+ @Autowired
+ private MockMvc api;
+
+ @Test
+ @DisplayName("Deve salvar um novo usuário.")
+ void should_save_a_new_user() throws Exception {
+ var json = stringify(Map.of(
+ "name", name(),
+ "email", email(),
+ "password", password()));
+
+ api.perform(post("/api/users")
+ .content(json)
+ .header(CONTENT_TYPE, APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id").exists())
+ .andExpect(jsonPath("$.id").isNotEmpty());
+ }
+
+ @Test
+ @DisplayName("Deve retornar status code 400 caso faltar algum campo requerido.")
+ void should_return_400_saving_user_without_required_fields() throws Exception {
+
+ var payload = stringify(Map.of(
+ "name", name(),
+ "password", "123"));
+
+ api.perform(post("/api/users")
+ .content(payload)
+ .header(CONTENT_TYPE, APPLICATION_JSON))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$").isArray())
+ .andExpect(jsonPath("$", hasSize(greaterThan(0))));
+ }
+
+ @Test
+ @DisplayName("Deve listar os usuários.")
+ void should_list_users() throws Exception {
+
+ repository.saveAll(users(4));
+
+ api.perform(get("/api/users")
+ .header(AUTHORIZATION, token("ADM"))
+ .queryParam("page", "0")
+ .queryParam("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(greaterThan(3))));
+ }
+
+ @Test
+ @DisplayName("Must not list deleted users.")
+ void must_not_list_deleted_users() throws Exception {
+
+ var user = repository.save(user());
+ var id = HashIdsUtils.encode(user.getId());
+
+ var token = token(user.getId(), "ADM,USER");
+
+ var expression = format("$.content[?(@.id == '%s')].id", id);
+
+ var index = get("/api/users")
+ .header(AUTHORIZATION, token)
+ .queryParam("page", "0")
+ .queryParam("size", "10");
+
+ api.perform(index)
+ .andExpect(status().isOk())
+ .andExpect(jsonPath(expression, hasItem(id)));
+
+ api.perform(delete(format("/api/users/%s", id))
+ .header(AUTHORIZATION, token))
+ .andExpect(status().isNoContent());
+
+ api.perform(index)
+ .andExpect(status().isOk())
+ .andExpect(jsonPath(expression).isEmpty());
+ }
+
+ @Test
+ @DisplayName("Deve deletar usuário.")
+ void should_delete_user() throws Exception {
+ var user = repository.save(user());
+ var id = user.getId();
+
+ api.perform(delete(format("/api/users/%s", HashIdsUtils.encode(id)))
+ .header(AUTHORIZATION, token(id, "ADM,USER")))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @DisplayName("Deve retornar status code 404 depois de remover o usuário.")
+ void should_return_404_after_delete_user() throws Exception {
+ var user = repository.save(user());
+
+ var request = delete(format("/api/users/%s", HashIdsUtils.encode(user.getId())))
+ .header(AUTHORIZATION, token(user.getId(), "ADM,USER"));
+
+ api.perform(request)
+ .andExpect(status().isNoContent());
+
+ api.perform(request)
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @DisplayName("Deve retornar status code 400 quando salvar um usuário com o mesmo email.")
+ void should_return_400_after_save_same_email() throws Exception {
+
+ var body = JSON.stringify(Map.of(
+ "name", FAKER.name().fullName(),
+ "email", FAKER.internet().safeEmailAddress(),
+ "password", password()));
+
+ var request = post("/api/users")
+ .content(body)
+ .header(CONTENT_TYPE, APPLICATION_JSON);
+
+ api.perform(request)
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id").exists())
+ .andExpect(jsonPath("$.id").isNotEmpty());
+
+ api.perform(request)
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$").isArray())
+ .andExpect(jsonPath("$", hasSize(1)));
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/resources/application.properties b/api/src/test/resources/application.properties
new file mode 100755
index 00000000..7a8673bc
--- /dev/null
+++ b/api/src/test/resources/application.properties
@@ -0,0 +1,71 @@
+# configurations from database and jpa.
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.url=jdbc:h2:mem:test;mode=PostgreSQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1
+spring.datasource.username=sa
+spring.datasource.password=sa
+spring.jpa.properties.hibernate.globally_quoted_identifiers=true
+spring.jpa.properties.javax.persistence.validation.mode=none
+spring.jpa.open-in-view=false
+spring.jpa.hibernate.ddl-auto=none
+
+# logger
+spring.main.banner-mode=off
+logging.level.root=error
+spring.output.ansi.enabled=always
+
+# swagger
+springdoc.swagger-ui.path=/docs
+springdoc.default-produces-media-type=application/json
+springdoc.default-consumes-media-type=application/json
+swagger.username=
+swagger.password=
+
+# security
+token.expiration-in-hours=24
+token.refresh.expiration-in-days=7
+recovery.minutes-to-expire=20
+token.secret=secret
+hashid.secret=secret
+cookie.secret=secret
+server.servlet.session.cookie.name=JSESSIONID
+server.servlet.session.cookie.path=/app
+
+# smtp configurations
+spring.mail.host=smtp.gmail.com
+spring.mail.port=587
+spring.mail.username=user
+spring.mail.password=secret
+
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.connectiontimeout=5000
+spring.mail.properties.mail.smtp.timeout=5000
+spring.mail.properties.mail.smtp.writetimeout=5000
+spring.mail.properties.mail.smtp.starttls.enable=true
+
+# encoding
+server.servlet.encoding.charset=UTF-8
+server.servlet.encoding.force-response=true
+
+# locale
+spring.web.locale=en
+spring.messages.encoding=UTF-8
+spring.messages.fallback-to-system-locale=false
+
+# rate limit
+spring.cache.jcache.config=classpath:ehcache.xml
+bucket4j.enabled=true
+bucket4j.filters[0].cache-name=buckets
+bucket4j.filters[0].filter-method=servlet
+bucket4j.filters[0].http-response-body={ "status": 249, "message": "Too many requests" }
+bucket4j.filters[0].url=/api
+bucket4j.filters[0].metrics.enabled=true
+bucket4j.filters[0].metrics.tags[0].key=IP
+bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[0].strategy=first
+bucket4j.filters[0].rate-limits[0].skip-condition=getRequestURI().contains('/swagger-ui') || getRequestURI().contains('/documentation')
+bucket4j.filters[0].rate-limits[0].expression=getRemoteAddr()
+bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=${MAX_REQUESTS_PER_MINUTE:10}
+bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
+bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
+bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval=0
+bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval-unit=minutes
\ No newline at end of file
diff --git a/system.properties b/api/system.properties
old mode 100644
new mode 100755
similarity index 100%
rename from system.properties
rename to api/system.properties
diff --git a/database_diagram/spring_boot_crud_database_diagram.drawio b/assets/database/diagram.drawio
old mode 100644
new mode 100755
similarity index 100%
rename from database_diagram/spring_boot_crud_database_diagram.drawio
rename to assets/database/diagram.drawio
diff --git a/database_diagram/spring_boot_crud_database_diagram.png b/assets/database/diagram.png
old mode 100644
new mode 100755
similarity index 100%
rename from database_diagram/spring_boot_crud_database_diagram.png
rename to assets/database/diagram.png
diff --git a/assets/demo.gif b/assets/images/demo.gif
similarity index 100%
rename from assets/demo.gif
rename to assets/images/demo.gif
diff --git a/assets/images/features.jpeg b/assets/images/features.jpeg
new file mode 100644
index 00000000..f53a74ba
Binary files /dev/null and b/assets/images/features.jpeg differ
diff --git a/assets/images/features.png b/assets/images/features.png
new file mode 100644
index 00000000..f32a0dc6
Binary files /dev/null and b/assets/images/features.png differ
diff --git a/assets/mobile.jpg b/assets/images/mobile.jpg
similarity index 100%
rename from assets/mobile.jpg
rename to assets/images/mobile.jpg
diff --git a/assets/spring-boot_logo.png b/assets/images/spring-boot_logo.png
similarity index 100%
rename from assets/spring-boot_logo.png
rename to assets/images/spring-boot_logo.png
diff --git a/assets/images/tecnologias.png b/assets/images/tecnologias.png
new file mode 100644
index 00000000..06e2b57a
Binary files /dev/null and b/assets/images/tecnologias.png differ
diff --git a/postman/crud_api.postman_collection.json b/assets/postman/postman_collection.json
similarity index 100%
rename from postman/crud_api.postman_collection.json
rename to assets/postman/postman_collection.json
diff --git a/assets/readme.md b/assets/pt-br/readme.md
similarity index 96%
rename from assets/readme.md
rename to assets/pt-br/readme.md
index c26f9d30..49f75190 100644
--- a/assets/readme.md
+++ b/assets/pt-br/readme.md
@@ -1,5 +1,6 @@
-[Em Inglês](../README.md)
-
+> [🇺🇸 Em Inglês](../README.md)
+>
+> [🐬 Implementação com MySQL/MariaDB](https://github.com/Throyer/springboot-api-crud/tree/mariadb#readme)
@@ -40,7 +41,7 @@
## Requisitos
-- MariaDB: `^10.6.1`
+- Postgres: `^13`
- Java: `^17`
> recomendo a instalação do maven localmente, mas o projeto tem uma versão portatil nos arquivos [`mvnw`](./mvnw) e [`mvnw.cmd`](./mvnw.cmd)
@@ -144,7 +145,7 @@ Criando arquivos de arquivos de migração
> ```shell
> # para mudar o valor de alguma variável de ambiente
> # na execução basta passar ela como parâmetro. (como --SERVER_PORT=80 por exemplo).
-> $ java -jar api-3.0.3.RELEASE.jar --SERVER_PORT=80
+> $ java -jar api-4.0.0.RELEASE.jar --SERVER_PORT=80
> ```
>
> > [Todas opções do `aplication.properties` **padrões** no Spring Boot](https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html).
diff --git a/assets/tecnologias.png b/assets/tecnologias.png
deleted file mode 100644
index 6842e414..00000000
Binary files a/assets/tecnologias.png and /dev/null differ
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 8667d6ec..00000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-version: '3'
-services:
-
- mariadb:
- image: mariadb:10.6.1
- container_name: mariadb-container
- ports:
- - 3306:3306
- environment:
- MYSQL_ROOT_PASSWORD: "root"
- MYSQL_DATABASE: "common_app"
- volumes:
- - ./.volumes/database:/var/lib/mysql
-
- api:
- image: maven:3.8.3
- links:
- - mariadb
- depends_on:
- - mariadb
- ports:
- - 8080:8080
- - 8000:8000
- environment:
- DB_URL: "mariadb:3306/common_app"
- DB_USERNAME: "root"
- DB_PASSWORD: "root"
- volumes:
- - ./:/app
- - ./.volumes/maven:/root/.m2
- working_dir: /app
- command: mvn spring-boot:run
\ No newline at end of file
diff --git a/lib/migration-maven-plugin-1.0.0.jar b/lib/migration-maven-plugin-1.0.0.jar
deleted file mode 100644
index 481a2b24..00000000
Binary files a/lib/migration-maven-plugin-1.0.0.jar and /dev/null differ
diff --git a/logs.sh b/logs.sh
deleted file mode 100755
index 55be5d7a..00000000
--- a/logs.sh
+++ /dev/null
@@ -1 +0,0 @@
-docker logs $(docker ps -a | grep -i springboot-api-crud | awk {'print $1'}) -f
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 355b8648..00000000
--- a/pom.xml
+++ /dev/null
@@ -1,263 +0,0 @@
-
-
- 4.0.0
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.6.2
-
-
- com.github.throyer.common.spring-boot
- api
- 3.0.4
- Common API
- Exemplo de api simples com Spring Boot
-
-
- 17
-
-
-
-
- jitpack.io
- https://jitpack.io
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-validation
-
-
- org.springframework.boot
- spring-boot-starter-mail
-
-
- org.springframework.boot
- spring-boot-starter-thymeleaf
-
-
- org.projectlombok
- lombok
- true
-
-
-
-
- com.github.javafaker
- javafaker
- 1.0.2
-
-
-
-
-
- org.springframework.security
- spring-security-data
-
-
-
-
-
- org.flywaydb
- flyway-core
-
-
- org.springframework.boot
- spring-boot-starter-jooq
-
-
-
-
- org.springdoc
- springdoc-openapi-ui
- 1.5.13
-
-
- org.springdoc
- springdoc-openapi-webmvc-core
- 1.5.13
-
-
- org.springdoc
- springdoc-openapi-security
- 1.5.13
-
-
-
-
- io.jsonwebtoken
- jjwt
- 0.9.1
-
-
-
-
- org.webjars
- webjars-locator
- 0.42
-
-
-
-
- org.webjars
- font-awesome
- 5.15.4
-
-
-
-
- org.webjars
- bootstrap
- 5.1.3
-
-
-
- org.thymeleaf.extras
- thymeleaf-extras-java8time
-
-
- org.springframework.security
- spring-security-taglibs
-
-
- org.thymeleaf.extras
- thymeleaf-extras-springsecurity5
-
-
-
- org.springframework.boot
- spring-boot-devtools
- runtime
- true
-
-
-
- com.h2database
- h2
- runtime
-
-
- mysql
- mysql-connector-java
- runtime
-
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
- org.junit.vintage
- junit-vintage-engine
-
-
-
-
- org.springframework.security
- spring-security-test
- test
-
-
-
-
-
-
- com.github.throyer
- migration-maven-plugin
- 1.2.7
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
- -agentlib:jdwp=transport=dt_socket,server=y,address=*:8000,suspend=n
-
-
-
-
-
- build-info
-
-
-
-
-
- org.sonarsource.scanner.maven
- sonar-maven-plugin
- 3.9.0.2155
-
-
- org.jacoco
- jacoco-maven-plugin
- 0.8.7
-
-
-
- prepare-agent
-
-
-
- report
- prepare-package
-
- report
-
-
-
-
-
- db/migration/**/*
-
-
-
-
- com.google.cloud.tools
- jib-maven-plugin
- 3.1.4
-
-
- openjdk:16
-
-
- com.github.throyer.common.spring-boot.${project.artifactId}:${project.version}
-
-
- ${maven.build.timestamp}
-
- 8080
-
-
- true
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
-
- config/sun_checks.xml
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/CommonApiApplication.java b/src/main/java/com/github/throyer/common/springboot/CommonApiApplication.java
deleted file mode 100644
index 41648494..00000000
--- a/src/main/java/com/github/throyer/common/springboot/CommonApiApplication.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.throyer.common.springboot;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class CommonApiApplication {
-
- public static void main(String... args) {
- SpringApplication.run(CommonApiApplication.class, args);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java b/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java
deleted file mode 100644
index 97e9cd63..00000000
--- a/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java
+++ /dev/null
@@ -1,173 +0,0 @@
-package com.github.throyer.common.springboot.configurations;
-
-import static com.github.throyer.common.springboot.errors.ValidationHandlers.forbidden;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.ACESSO_NEGADO_URL;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.DAY_MILLISECONDS;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.HOME_URL;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.LOGIN_ERROR_URL;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.LOGIN_URL;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.LOGOUT_URL;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.PASSWORD_PARAMETER;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.SESSION_COOKIE_NAME;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.STATIC_FILES;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.USERNAME_PARAMETER;
-import static org.springframework.http.HttpMethod.GET;
-import static org.springframework.http.HttpMethod.POST;
-import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
-
-import com.github.throyer.common.springboot.domain.services.security.SecurityService;
-import com.github.throyer.common.springboot.middlewares.AuthorizationMiddleware;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.annotation.Order;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.builders.WebSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.stereotype.Component;
-import org.springframework.web.cors.CorsConfiguration;
-
-@Component
-@EnableWebSecurity
-@EnableGlobalMethodSecurity(prePostEnabled = true)
-public class SpringSecurityConfiguration {
-
- @Autowired
- private SecurityService securityService;
-
- @Autowired
- private BCryptPasswordEncoder encoder;
-
- @Autowired
- private AuthorizationMiddleware filter;
-
- private static String SECRET;
-
- public SpringSecurityConfiguration(@Value("${token.secret}") String secret) {
- SpringSecurityConfiguration.SECRET = secret;
- }
-
- @Order(1)
- @Configuration
- public class Api extends WebSecurityConfigurerAdapter {
-
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(securityService)
- .passwordEncoder(encoder);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
- .antMatcher("/api/**")
- .authorizeRequests()
- .antMatchers(
- GET,
- "/api",
- "/api/documentation/**"
- )
- .permitAll()
- .antMatchers(
- POST,
- "/api/users",
- "/api/sessions/**",
- "/api/recoveries/**",
- "/api/documentation/**"
- )
- .permitAll()
- .anyRequest()
- .authenticated()
- .and()
- .csrf()
- .disable()
- .exceptionHandling()
- .authenticationEntryPoint((request, response, exception) -> forbidden(response))
- .and()
- .sessionManagement()
- .sessionCreationPolicy(STATELESS)
- .and()
- .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
- .cors()
- .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
- }
-
- @Override
- public void configure(WebSecurity web) throws Exception {
- web
- .ignoring()
- .antMatchers(STATIC_FILES);
- }
-
- @Bean
- @Override
- protected AuthenticationManager authenticationManager() throws Exception {
- return super.authenticationManager();
- }
- }
-
- @Order(2)
- @Configuration
- public class App extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.
- userDetailsService(securityService)
- .passwordEncoder(encoder);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
-
- http
- .antMatcher("/app/**")
- .authorizeRequests()
- .antMatchers(
- GET,
- LOGIN_URL,
- "/app",
- "/app/register",
- "/app/recovery/**"
- )
- .permitAll()
- .antMatchers(
- POST,
- "/app/register",
- "/app/recovery/**"
- )
- .permitAll()
- .anyRequest()
- .authenticated()
- .and()
- .csrf()
- .disable()
- .formLogin()
- .loginPage(LOGIN_URL)
- .failureUrl(LOGIN_ERROR_URL)
- .defaultSuccessUrl(HOME_URL)
- .usernameParameter(USERNAME_PARAMETER)
- .passwordParameter(PASSWORD_PARAMETER)
- .and()
- .rememberMe()
- .key(SECRET)
- .tokenValiditySeconds(DAY_MILLISECONDS)
- .and()
- .logout()
- .deleteCookies(SESSION_COOKIE_NAME)
- .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_URL))
- .logoutSuccessUrl(LOGIN_URL)
- .and()
- .exceptionHandling()
- .accessDeniedPage(ACESSO_NEGADO_URL);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/configurations/SpringWebConfiguration.java b/src/main/java/com/github/throyer/common/springboot/configurations/SpringWebConfiguration.java
deleted file mode 100644
index e7210028..00000000
--- a/src/main/java/com/github/throyer/common/springboot/configurations/SpringWebConfiguration.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.github.throyer.common.springboot.configurations;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
-import org.springframework.web.servlet.config.annotation.CorsRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
-import io.swagger.v3.oas.annotations.OpenAPIDefinition;
-import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
-import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
-import io.swagger.v3.oas.annotations.info.Contact;
-import io.swagger.v3.oas.annotations.info.Info;
-import io.swagger.v3.oas.annotations.info.License;
-import io.swagger.v3.oas.annotations.security.SecurityScheme;
-
-@Configuration
-@OpenAPIDefinition(info = @Info(
- title = "Common CRUD API",
- version = "v3.0.4",
- description = """
- A complete user registry, with access permissions,
- JWT token, integration and unit tests, using
- the RESTful API pattern.
- """,
- license = @License(
- name = "GNU General Public License v3.0",
- url = "https://github.com/Throyer/springboot-api-crud/blob/master/LICENSE"
- ),
- contact = @Contact(
- name = "Throyer",
- email = "throyer.dev@gmail.com",
- url = "https://github.com/Throyer"
- )
-))
-@SecurityScheme(
- name = "token",
- type = SecuritySchemeType.HTTP,
- bearerFormat = "JWT",
- in = SecuritySchemeIn.HEADER,
- scheme = "bearer"
-)
-public class SpringWebConfiguration implements WebMvcConfigurer {
-
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry
- .addMapping("/**")
- .allowedOrigins("*")
- .allowedHeaders("*");
- }
-
- @Bean
- public BCryptPasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- @Bean
- public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
- return new SecurityEvaluationContextExtension();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/api/ApiController.java b/src/main/java/com/github/throyer/common/springboot/controllers/api/ApiController.java
deleted file mode 100644
index 04f8d875..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/api/ApiController.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.throyer.common.springboot.controllers.api;
-
-import com.github.throyer.common.springboot.utils.Hello;
-
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api")
-public class ApiController {
-
- @GetMapping
- public Hello index() {
- return () -> "Is a live!";
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/api/RecoveriesController.java b/src/main/java/com/github/throyer/common/springboot/controllers/api/RecoveriesController.java
deleted file mode 100644
index e54ce961..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/api/RecoveriesController.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.github.throyer.common.springboot.controllers.api;
-
-import static org.springframework.http.HttpStatus.NO_CONTENT;
-
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryConfirmService;
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryService;
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryUpdateService;
-import com.github.throyer.common.springboot.domain.services.user.dto.RecoveryConfirm;
-import com.github.throyer.common.springboot.domain.services.user.dto.RecoveryRequest;
-import com.github.throyer.common.springboot.domain.services.user.dto.RecoveryUpdate;
-
-import org.springframework.beans.factory.annotation.Autowired;
-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.ResponseStatus;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api/recoveries")
-public class RecoveriesController {
-
- @Autowired
- private RecoveryService recoveryService;
-
- @Autowired
- private RecoveryConfirmService confirmService;
-
- @Autowired
- private RecoveryUpdateService updateService;
-
- @PostMapping
- @ResponseStatus(NO_CONTENT)
- public void index(@RequestBody RecoveryRequest request) {
- recoveryService.recovery(request.getEmail());
- }
-
- @PostMapping("/confirm")
- public void confirm(@RequestBody RecoveryConfirm confirm) {
- confirmService.confirm(confirm.getEmail(), confirm.getCode());
- }
-
- @PostMapping("/update")
- @ResponseStatus(NO_CONTENT)
- public void update(@RequestBody RecoveryUpdate update) {
- updateService.update(update.getEmail(), update.getCode(), update.getPassword());
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/api/RolesController.java b/src/main/java/com/github/throyer/common/springboot/controllers/api/RolesController.java
deleted file mode 100644
index 4ec72847..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/api/RolesController.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.throyer.common.springboot.controllers.api;
-
-import static com.github.throyer.common.springboot.utils.Responses.ok;
-
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-import com.github.throyer.common.springboot.domain.repositories.RoleRepository;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api/roles")
-@SecurityRequirement(name = "token")
-@PreAuthorize("hasAnyAuthority('ADM')")
-public class RolesController {
-
- @Autowired
- private RoleRepository repository;
-
- @GetMapping
- public ResponseEntity> index() {
- return ok(repository.findAll());
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/api/SessionsController.java b/src/main/java/com/github/throyer/common/springboot/controllers/api/SessionsController.java
deleted file mode 100644
index c059fe90..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/api/SessionsController.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.github.throyer.common.springboot.controllers.api;
-
-import javax.validation.Valid;
-
-import com.github.throyer.common.springboot.domain.services.security.SessionService;
-import com.github.throyer.common.springboot.domain.services.security.dto.RefreshTokenRequest;
-import com.github.throyer.common.springboot.domain.services.security.dto.RefreshTokenResponse;
-import com.github.throyer.common.springboot.domain.services.security.dto.TokenRequest;
-import com.github.throyer.common.springboot.domain.services.security.dto.TokenResponse;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api/sessions")
-public class SessionsController {
-
- @Autowired
- private SessionService service;
-
- @PostMapping
- public ResponseEntity create(@RequestBody @Valid TokenRequest request) {
- return service.create(request);
- }
-
- @PostMapping("/refresh")
- public ResponseEntity refresh(@RequestBody @Valid RefreshTokenRequest request) {
- return service.refresh(request);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/api/UsersController.java b/src/main/java/com/github/throyer/common/springboot/controllers/api/UsersController.java
deleted file mode 100644
index 6afd9fa6..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/api/UsersController.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.github.throyer.common.springboot.controllers.api;
-
-import static org.springframework.http.HttpStatus.CREATED;
-import static org.springframework.http.HttpStatus.NO_CONTENT;
-
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.models.pagination.Page;
-
-import com.github.throyer.common.springboot.domain.services.user.CreateUserService;
-import com.github.throyer.common.springboot.domain.services.user.FindUserService;
-import com.github.throyer.common.springboot.domain.services.user.RemoveUserService;
-import com.github.throyer.common.springboot.domain.services.user.UpdateUserService;
-import com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApi;
-import com.github.throyer.common.springboot.domain.services.user.dto.UpdateUser;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-import static com.github.throyer.common.springboot.utils.Responses.ok;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
-import java.util.Optional;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-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.ResponseStatus;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api/users")
-public class UsersController {
-
- @Autowired
- private CreateUserService createService;
-
- @Autowired
- private UpdateUserService updateService;
-
- @Autowired
- private RemoveUserService removeService;
-
- @Autowired
- private FindUserService findService;
-
- @GetMapping
- @SecurityRequirement(name = "token")
- @PreAuthorize("hasAnyAuthority('ADM')")
- public ResponseEntity> index(
- Optional page,
- Optional size
- ) {
- var result = findService.findAll(page, size);
- return ok(result);
- }
-
- @GetMapping("/{id}")
- @SecurityRequirement(name = "token")
- @PreAuthorize("hasAnyAuthority('ADM', 'USER')")
- public ResponseEntity show(@PathVariable Long id) {
- return findService.find(id);
- }
-
- @PostMapping
- @ResponseStatus(CREATED)
- public ResponseEntity save(@Validated @RequestBody CreateUserApi body) {
- return createService.create(body);
- }
-
- @PutMapping("/{id}")
- @SecurityRequirement(name = "token")
- @PreAuthorize("hasAnyAuthority('ADM', 'USER')")
- public ResponseEntity update(
- @PathVariable Long id,
- @RequestBody @Validated UpdateUser body
- ) {
- return updateService.update(id, body);
- }
-
- @DeleteMapping("/{id}")
- @ResponseStatus(NO_CONTENT)
- @SecurityRequirement(name = "token")
- @PreAuthorize("hasAnyAuthority('ADM')")
- public ResponseEntity destroy(@PathVariable Long id) {
- return removeService.remove(id);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/AppController.java b/src/main/java/com/github/throyer/common/springboot/controllers/app/AppController.java
deleted file mode 100644
index cbaf8998..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/AppController.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.throyer.common.springboot.controllers.app;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-@Controller
-@RequestMapping("/app")
-public class AppController {
-
- @GetMapping
- public String index(Model model) {
- return "app/index";
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/HomeController.java b/src/main/java/com/github/throyer/common/springboot/controllers/app/HomeController.java
deleted file mode 100644
index fe14e952..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/HomeController.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.throyer.common.springboot.controllers.app;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-@Controller
-public class HomeController {
-
- @RequestMapping("/")
- public String index() {
- return "redirect:/app";
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/RecoveryController.java b/src/main/java/com/github/throyer/common/springboot/controllers/app/RecoveryController.java
deleted file mode 100644
index 8007ec34..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/RecoveryController.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.throyer.common.springboot.controllers.app;
-
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryConfirmService;
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryService;
-import com.github.throyer.common.springboot.domain.services.recovery.RecoveryUpdateService;
-import com.github.throyer.common.springboot.domain.services.user.dto.Codes;
-import com.github.throyer.common.springboot.domain.services.user.dto.RecoveryRequest;
-import com.github.throyer.common.springboot.domain.services.user.dto.Update;
-import javax.validation.Valid;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Controller
-@RequestMapping("/app/recovery")
-public class RecoveryController {
-
- @Autowired
- private RecoveryService recoveryService;
-
- @Autowired
- private RecoveryConfirmService confirmService;
-
- @Autowired
- private RecoveryUpdateService updateService;
-
- @GetMapping
- public String index(Model model) {
- model.addAttribute("recovery", new RecoveryRequest());
- return "app/recovery/index";
- }
-
- @PostMapping
- public String index(
- @Valid RecoveryRequest recovery,
- BindingResult result,
- Model model
- ) {
- return recoveryService.recovery(recovery, result, model);
- }
-
- @PostMapping("/confirm")
- public String confirm(
- @Valid Codes codes,
- BindingResult result,
- RedirectAttributes redirect,
- Model model
- ) {
- return confirmService.confirm(codes, result, model, redirect);
- }
-
- @PostMapping("/update")
- public String update(
- @Valid Update update,
- BindingResult result,
- RedirectAttributes redirect,
- Model model
- ) {
- return updateService.update(update, result, model, redirect);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/RegisterController.java b/src/main/java/com/github/throyer/common/springboot/controllers/app/RegisterController.java
deleted file mode 100644
index 2c85ea22..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/RegisterController.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.github.throyer.common.springboot.controllers.app;
-
-import javax.validation.Valid;
-
-import com.github.throyer.common.springboot.domain.services.user.CreateUserService;
-import com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApp;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Controller
-@RequestMapping("/app/register")
-public class RegisterController {
-
- @Autowired
- private CreateUserService service;
-
- @GetMapping(produces = "text/html")
- public String index(Model model) {
- model.addAttribute("user", new CreateUserApp());
- return "app/register/index";
- }
-
- @PostMapping(produces = "text/html")
- public String create(
- @Valid CreateUserApp user,
- BindingResult result,
- RedirectAttributes redirect,
- Model model
- ) {
- return service.create(user, result, redirect, model);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/controllers/app/UserController.java b/src/main/java/com/github/throyer/common/springboot/controllers/app/UserController.java
deleted file mode 100644
index 8ae55873..00000000
--- a/src/main/java/com/github/throyer/common/springboot/controllers/app/UserController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.github.throyer.common.springboot.controllers.app;
-
-import com.github.throyer.common.springboot.domain.models.shared.Type;
-import com.github.throyer.common.springboot.domain.services.user.FindUserService;
-import com.github.throyer.common.springboot.domain.services.user.RemoveUserService;
-import com.github.throyer.common.springboot.utils.Toasts;
-import java.util.Optional;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Controller
-@PreAuthorize("hasAnyAuthority('ADM')")
-@RequestMapping("/app/users")
-public class UserController {
-
- @Autowired
- private FindUserService findService;
-
- @Autowired
- private RemoveUserService removeService;
-
- @GetMapping
- public String index(
- Model model,
- Optional page,
- Optional size
- ) {
-
- var result = findService.findAll(page, size);
-
- model.addAttribute("page", result);
-
- return "app/users/index";
- }
-
- @PostMapping("/delete/{id}")
- public String delete(@PathVariable Long id, RedirectAttributes redirect) {
- removeService.remove(id);
-
- Toasts.add(redirect, "Usuário deletado com sucesso.", Type.SUCCESS);
-
- return "redirect:/app/users";
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/builders/UserBuilder.java b/src/main/java/com/github/throyer/common/springboot/domain/builders/UserBuilder.java
deleted file mode 100644
index f47b2eb9..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/builders/UserBuilder.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.github.throyer.common.springboot.domain.builders;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-import com.github.throyer.common.springboot.domain.models.entity.User;
-
-public class UserBuilder {
-
- private final User user;
- private List roles = new ArrayList<>();
-
- public static UserBuilder createUser(String name) {
- return new UserBuilder(name);
- }
-
- public UserBuilder(String name) {
- this.user = new User();
- this.user.setName(name);
- }
-
- public UserBuilder(String name, List roles) {
- this.user = new User();
- this.roles = roles;
- this.user.setName(name);
- }
-
- public UserBuilder setEmail(String email) {
- user.setEmail(email);
- return this;
- }
-
- public UserBuilder setId(Long id) {
- user.setId(id);
- return this;
- }
-
- public UserBuilder setActive(Boolean active) {
- user.setActive(active);
- return this;
- }
-
- public UserBuilder setPassword(String password) {
- user.setPassword(password);
- return this;
- }
-
- public UserBuilder addRole(Role role) {
- roles.add(role);
- return this;
- }
-
- public UserBuilder addRole(Long id) {
- roles.add(new Role(id));
- return this;
- }
-
- public User build() {
- user.setRoles(roles);
- return user;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Auditable.java b/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Auditable.java
deleted file mode 100644
index 7a94f58e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Auditable.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.entity;
-
-import static com.github.throyer.common.springboot.domain.services.security.SecurityService.authorized;
-import static java.time.LocalDateTime.now;
-import static java.util.Optional.ofNullable;
-import static javax.persistence.FetchType.LAZY;
-
-import java.time.LocalDateTime;
-import java.util.Optional;
-
-import javax.persistence.Column;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.MappedSuperclass;
-import javax.persistence.PrePersist;
-import javax.persistence.PreUpdate;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.github.throyer.common.springboot.domain.models.shared.Entity;
-
-@MappedSuperclass
-public abstract class Auditable implements Entity {
-
- public static final String NON_DELETED_CLAUSE = "deleted_at IS NULL";
-
- @Override
- public abstract Long getId();
-
- @JsonIgnore
- @Column(name = "created_at")
- private LocalDateTime createdAt;
-
- @JsonIgnore
- @Column(name = "updated_at")
- private LocalDateTime updatedAt;
-
- @JsonIgnore
- @Column(name = "deleted_at")
- private LocalDateTime deletedAt;
-
- @JoinColumn(name = "created_by")
- @ManyToOne(optional = true, fetch = LAZY)
- private User createdBy;
-
- @JoinColumn(name = "updated_by")
- @ManyToOne(optional = true, fetch = LAZY)
- private User updatedBy;
-
- @JoinColumn(name = "deleted_by")
- @ManyToOne(optional = true, fetch = LAZY)
- private User deletedBy;
-
- @JsonIgnore
- public Optional getCreatedBy() {
- return ofNullable(createdBy);
- }
-
- @JsonIgnore
- public Optional getUpdatedBy() {
- return ofNullable(updatedBy);
- }
-
- @JsonIgnore
- public Optional getDeletedBy() {
- return ofNullable(deletedBy);
- }
-
- @Column(name = "active", nullable = false)
- private Boolean active = true;
-
- public LocalDateTime getCreatedAt() {
- return createdAt;
- }
-
- public void setCreatedAt(LocalDateTime createdAt) {
- this.createdAt = createdAt;
- }
-
- public LocalDateTime getUpdatedAt() {
- return updatedAt;
- }
-
- public void setUpdatedAt(LocalDateTime updatedAt) {
- this.updatedAt = updatedAt;
- }
-
- public LocalDateTime getDeletedAt() {
- return deletedAt;
- }
-
- public void setDeletedAt(LocalDateTime deletedAt) {
- this.deletedAt = deletedAt;
- }
-
- public Boolean isActive() {
- return this.active;
- }
-
- public void setActive(Boolean active) {
- this.active = active;
- }
-
- @PrePersist
- private void save() {
- createdAt = now();
- createdBy = authorized()
- .map(authorized -> new User(authorized.getId()))
- .orElse(null);
- }
-
- @PreUpdate
- private void update() {
- updatedAt = now();
- updatedBy = authorized()
- .map(authorized -> new User(authorized.getId()))
- .orElse(null);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Recovery.java b/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Recovery.java
deleted file mode 100644
index afcabec8..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Recovery.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.entity;
-
-import static com.github.throyer.common.springboot.utils.Random.code;
-
-import java.time.LocalDateTime;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.Table;
-
-@Entity
-@Table(name = "recovery")
-public class Recovery {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "code", nullable = false)
- private String code;
-
- @Column(name = "expires_in", nullable = false )
- private LocalDateTime expiresIn;
-
- @Column(name = "confirmed")
- private Boolean confirmed = false;
-
- @Column(name = "used")
- private Boolean used = false;
-
- @JoinColumn(name = "user_id")
- @ManyToOne
- private User user;
-
- public Recovery() { }
-
- public Recovery(User user, Integer minutesToExpire) {
- this.user = user;
- this.expiresIn = LocalDateTime.now().plusMinutes(minutesToExpire);
- this.code = code();
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public LocalDateTime getExpiresIn() {
- return expiresIn;
- }
-
- public void setExpiresIn(LocalDateTime expiresIn) {
- this.expiresIn = expiresIn;
- }
-
- public User getUser() {
- return user;
- }
-
- public void setUser(User user) {
- this.user = user;
- }
-
- public Boolean isConfirmed() {
- return confirmed;
- }
-
- public void setConfirmed(Boolean confirmed) {
- this.confirmed = confirmed;
- }
-
- public Boolean isUsed() {
- return used;
- }
-
- public void setUsed(Boolean used) {
- this.used = used;
- }
-
- public Boolean nonExpired() {
- return expiresIn.isAfter(LocalDateTime.now());
- }
-
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/RefreshToken.java b/src/main/java/com/github/throyer/common/springboot/domain/models/entity/RefreshToken.java
deleted file mode 100644
index d796dae1..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/RefreshToken.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.entity;
-
-import java.time.LocalDateTime;
-import java.util.UUID;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.Table;
-
-@Entity
-@Table(name = "refresh_token")
-public class RefreshToken {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "code", nullable = false)
- private String code;
-
- @Column(name = "expires_in", nullable = false )
- private LocalDateTime expiresIn;
-
- @Column(name = "available", nullable = false)
- private Boolean available = true;
-
- @JoinColumn(name = "user_id")
- @ManyToOne
- private User user;
-
- public RefreshToken() { }
-
- public RefreshToken(User user, Integer daysToExpire) {
- this.user = user;
- this.expiresIn = LocalDateTime.now().plusDays(daysToExpire);
- this.code = UUID.randomUUID().toString();
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public LocalDateTime getExpiresIn() {
- return expiresIn;
- }
-
- public void setExpiresIn(LocalDateTime expiresIn) {
- this.expiresIn = expiresIn;
- }
-
- public User getUser() {
- return user;
- }
-
- public void setUser(User user) {
- this.user = user;
- }
-
- public Boolean nonExpired() {
- return expiresIn.isAfter(LocalDateTime.now());
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Role.java b/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Role.java
deleted file mode 100644
index 5da6d747..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Role.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.entity;
-
-import java.util.Objects;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import lombok.Data;
-
-import org.hibernate.annotations.Where;
-import org.springframework.security.core.GrantedAuthority;
-
-@Data
-@Entity
-@Table(name = "role")
-@Where(clause = Auditable.NON_DELETED_CLAUSE)
-public class Role extends Auditable implements GrantedAuthority {
-
- private static final long serialVersionUID = -8524505911742593369L;
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "name", nullable = true, unique = true)
- private String name;
-
- @JsonIgnore
- @Column(name = "deleted_name")
- private String deletedName;
-
- @Column(name = "initials", nullable = true, unique = true)
- private String initials;
-
- @JsonIgnore
- @Column(name = "deleted_initials")
- private String deletedInitials;
-
- @Column(nullable = true, unique = true)
- private String description;
-
- public Role() { }
-
- public Role(String initials) {
- this.initials = initials;
- }
-
- public Role(Long id) {
- this.id = id;
- }
-
- public Role(Long id, String initials) {
- this.id = id;
- this.initials = initials;
- }
-
- @Override
- public boolean equals(Object object) {
- if (object == this)
- return true;
- if (!(object instanceof Role)) {
- return false;
- }
- Role role = (Role) object;
- return Objects.equals(id, role.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(id);
- }
-
- @Override
- public String toString() {
- return this.getAuthority();
- }
-
- @JsonIgnore
- @Override
- public String getAuthority() {
- return this.getInitials();
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/User.java b/src/main/java/com/github/throyer/common/springboot/domain/models/entity/User.java
deleted file mode 100644
index 207d4c7e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/User.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.entity;
-
-import java.io.Serializable;
-import java.security.Principal;
-import java.util.List;
-import java.util.Objects;
-
-import javax.persistence.CascadeType;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.JoinTable;
-import javax.persistence.ManyToMany;
-import javax.persistence.PrePersist;
-import javax.persistence.Table;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonProperty.Access;
-import com.github.throyer.common.springboot.domain.models.security.Authorized;
-import com.github.throyer.common.springboot.domain.models.shared.HasEmail;
-import com.github.throyer.common.springboot.domain.services.user.dto.UpdateUser;
-import lombok.Data;
-
-import org.hibernate.annotations.Where;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-
-@Data
-@Entity
-@Table(name = "user")
-@Where(clause = Auditable.NON_DELETED_CLAUSE)
-public class User extends Auditable implements Serializable, HasEmail {
-
- public static final Integer PASSWORD_STRENGTH = 10;
-
- private static final long serialVersionUID = -8080540494839892473L;
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "name", nullable = false)
- private String name;
-
- @Column(name = "email", unique = true)
- private String email;
-
- @JsonIgnore
- @Column(name = "deleted_email")
- private String deletedEmail;
-
- @JsonProperty(access = Access.WRITE_ONLY)
- @Column(name = "password", nullable = false)
- private String password;
-
- @ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
- @JoinTable(name = "user_role",
- joinColumns = {
- @JoinColumn(name = "user_id")},
- inverseJoinColumns = {
- @JoinColumn(name = "role_id")})
- private List roles;
-
- public User() { }
-
- public User(Long id) {
- this.id = id;
- }
-
- public User(String name, String email, String password, List roles) {
- this.name = name;
- this.email = email;
- this.password = password;
- this.roles = roles;
- }
-
- public List getRoles() {
- return roles;
- }
-
- public void setRoles(List roles) {
- this.roles = roles;
- }
-
- @Override
- public Long getId() {
- return id;
- }
-
- @Override
- public String getEmail() {
- return email;
- }
-
- public void merge(UpdateUser dto) {
- setName(dto.getName());
- setEmail(dto.getEmail());
- }
-
- public void updatePassword(String newPassword) {
- this.password = new BCryptPasswordEncoder(PASSWORD_STRENGTH)
- .encode(newPassword);
- }
-
- @Override
- public int hashCode() {
- int hash = 7;
- hash = 97 * hash + Objects.hashCode(this.id);
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final User other = (User) obj;
- return Objects.equals(this.id, other.id);
- }
-
- public boolean isPrincipal(Principal principal) {
- if (Objects.nonNull(principal) && principal instanceof Authorized authorized) {
- return getId().equals(authorized.getId());
- }
- return false;
- }
-
- public boolean isPrincipal(Authorized authorized) {
- if (Objects.nonNull(authorized)) {
- return getId().equals(authorized.getId());
- }
- return false;
- }
-
- public Boolean validatePassword(String password) {
- var encoder = new BCryptPasswordEncoder(PASSWORD_STRENGTH);
- return encoder.matches(password, this.password);
- }
-
- @PrePersist
- private void created() {
- this.password = new BCryptPasswordEncoder(PASSWORD_STRENGTH).encode(password);
- }
-
- @Override
- public String toString() {
- return Objects.nonNull(getName()) ? name : "null";
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Page.java b/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Page.java
deleted file mode 100644
index c3273ee9..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Page.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.pagination;
-
-import static com.github.throyer.common.springboot.utils.JsonUtils.toJson;
-import java.util.Collection;
-
-public class Page {
- private final Collection content;
- private final Integer page;
- private final Integer size;
- private final Integer totalPages;
- private final Long totalElements;
-
- public Page(org.springframework.data.domain.Page page) {
- this.content = page.getContent();
- this.page = page.getNumber();
- this.size = page.getSize();
- this.totalPages = page.getTotalPages();
- this.totalElements = page.getTotalElements();
- }
-
- public Page(Collection content, Integer page, Integer size, Long count) {
- this.content = content;
- this.page = page;
- this.size = size;
- this.totalPages = (int) Math.ceil((double)count / size);
- this.totalElements = count;
- }
-
-
- public Collection getContent() {
- return content;
- }
-
- public Integer getPage() {
- return page;
- }
-
- public Integer getSize() {
- return size;
- }
-
- public Long getTotalElements() {
- return totalElements;
- }
-
- public Integer getTotalPages() {
- return totalPages;
- }
-
- public static Page of(org.springframework.data.domain.Page page) {
- return new Page<>(page);
- }
-
- public static Page of(Collection content, Integer page, Integer size, Long count) {
- return new Page<>(content, page, size, count);
- }
-
- @Override
- public String toString() {
- return toJson(this);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Pagination.java b/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Pagination.java
deleted file mode 100644
index f51a0c00..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Pagination.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.pagination;
-
-import org.springframework.data.domain.PageRequest;
-
-import java.util.Optional;
-
-import org.springframework.data.domain.Pageable;
-
-public class Pagination {
-
- private static final int FIRST_PAGE = 0;
- private static final int DEFAULT_SIZE = 10;
-
- private static final int MIN_SIZE = 1;
- private static final int MAX_SIZE = 500;
-
- public static Pageable of(Optional page, Optional size) {
- return Pagination.of(page.orElse(FIRST_PAGE), size.orElse(DEFAULT_SIZE));
- }
-
- public static Pageable of(Integer page, Integer size) {
- if (page < FIRST_PAGE) {
- page = FIRST_PAGE;
- }
-
- if (size < MIN_SIZE || size > MAX_SIZE) {
- size = DEFAULT_SIZE;
- }
- return PageRequest.of(page, size);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/security/Authorized.java b/src/main/java/com/github/throyer/common/springboot/domain/models/security/Authorized.java
deleted file mode 100644
index 15e58ff6..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/security/Authorized.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.security;
-
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.userdetails.User;
-
-public class Authorized extends User {
-
- private static final long serialVersionUID = 1L;
-
- private final Long id;
- private final String name;
-
- public Authorized(Long id, List authorities) {
- super("USERNAME", "SECRET", authorities);
- this.id = id;
- this.name = "";
- }
-
- public Authorized(com.github.throyer.common.springboot.domain.models.entity.User user) {
- super(
- user.getEmail(),
- user.getPassword(),
- user.isActive(),
- true,
- true,
- true,
- user.getRoles()
- );
- this.id = user.getId();
- this.name = user.getName();
- }
-
- public Long getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public UsernamePasswordAuthenticationToken getAuthentication() {
- return new UsernamePasswordAuthenticationToken(this, null, getAuthorities());
- }
-
- public Boolean isAdmin() {
- return getAuthorities()
- .stream()
- .anyMatch((role) -> role.getAuthority().equals("ADM"));
- }
-
- public Boolean cantModify(Long id) {
- var admin = isAdmin();
- var equals = getId().equals(id);
- if (admin) {
- return true;
- }
- return equals;
- }
-
- public Boolean cantRead(Long id) {
- var admin = isAdmin();
- var equals = getId().equals(id);
- if (admin) {
- return true;
- }
- return equals;
- }
-
- @Override
- public String toString() {
- return getId().toString();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Entity.java b/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Entity.java
deleted file mode 100644
index 1a807802..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Entity.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.shared;
-
-public interface Entity {
- Long getId();
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/HasEmail.java b/src/main/java/com/github/throyer/common/springboot/domain/models/shared/HasEmail.java
deleted file mode 100644
index f3801e52..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/HasEmail.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.github.throyer.common.springboot.domain.models.shared;
-
-public interface HasEmail {
- public String getEmail();
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RefreshTokenRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/repositories/RefreshTokenRepository.java
deleted file mode 100644
index 0b6f6914..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RefreshTokenRepository.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.github.throyer.common.springboot.domain.repositories;
-
-import java.util.Optional;
-
-import javax.transaction.Transactional;
-
-import com.github.throyer.common.springboot.domain.models.entity.RefreshToken;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.stereotype.Repository;
-
-@Repository
-public interface RefreshTokenRepository extends JpaRepository, JpaSpecificationExecutor {
- @Transactional
- @Modifying
- @Query("""
- UPDATE
- RefreshToken
- SET
- available = 0
- WHERE
- user_id = ?1 AND available = 1
- """)
- public void disableOldRefreshTokens(Long id);
-
- @Query("""
- SELECT refresh FROM RefreshToken refresh
- JOIN FETCH refresh.user user
- JOIN FETCH user.roles
- WHERE refresh.code = ?1 AND refresh.available = true
- """)
- public Optional findOptionalByCodeAndAvailableIsTrue(String code);
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RoleRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/repositories/RoleRepository.java
deleted file mode 100644
index 782d2522..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RoleRepository.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.github.throyer.common.springboot.domain.repositories;
-
-import java.util.Optional;
-
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-@Repository
-public interface RoleRepository extends SoftDeleteRepository {
-
- @Override
- @Modifying
- @Transactional
- @Query("""
- UPDATE
- #{#entityName}
- SET
- deleted_name = (
- SELECT name FROM #{#entityName} WHERE id = ?1
- ),
- name = NULL,
- deleted_initials = (
- SELECT name FROM #{#entityName} WHERE id = ?1
- ),
- initials = NULL,
- deleted_at = CURRENT_TIMESTAMP,
- active = 0,
- deleted_by = ?#{principal?.id}
- WHERE id = ?1
- """)
- void deleteById(Long id);
-
- @Override
- @Transactional
- default void delete(Role role) {
- deleteById(role.getId());
- }
-
- @Override
- @Transactional
- default void deleteAll(Iterable extends Role> entities) {
- entities.forEach(entity -> deleteById(entity.getId()));
- }
-
- Optional findOptionalByInitials(String initials);
-
- Optional findOptionalByName(String name);
-
- Boolean existsByInitials(String initials);
-
- Boolean existsByName(String name);
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java
deleted file mode 100644
index faedf6ba..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.github.throyer.common.springboot.domain.repositories;
-
-import com.github.throyer.common.springboot.domain.models.entity.Auditable;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.NoRepositoryBean;
-import org.springframework.transaction.annotation.Transactional;
-
-@NoRepositoryBean
-public interface SoftDeleteRepository extends JpaRepository, JpaSpecificationExecutor {
-
- @Override
- @Modifying
- @Transactional
- @Query("""
- UPDATE
- #{#entityName}
- SET
- deleted_at = CURRENT_TIMESTAMP
- WHERE id = ?1
- """)
- void deleteById(Long id);
-
- @Override
- @Transactional
- default void delete(T entity) {
- deleteById(entity.getId());
- }
-
- @Override
- @Transactional
- default void deleteAll(Iterable extends T> entities) {
- entities.forEach(entity -> deleteById(entity.getId()));
- }
-
- @Override
- @Modifying
- @Transactional
- @Query("""
- UPDATE
- #{#entityName}
- SET
- deleted_at = CURRENT_TIMESTAMP
- """)
- void deleteAll();
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/UserRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/repositories/UserRepository.java
deleted file mode 100644
index d9c19ec6..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/UserRepository.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.github.throyer.common.springboot.domain.repositories;
-
-import java.util.Optional;
-
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-@Repository
-public interface UserRepository extends SoftDeleteRepository {
-
- @Override
- @Transactional
- @Modifying
- @Query("""
- UPDATE
- #{#entityName}
- SET
- deleted_email = (
- SELECT
- email
- FROM
- #{#entityName}
- WHERE id = ?1),
- email = NULL,
- deleted_at = CURRENT_TIMESTAMP,
- active = 0,
- deleted_by = ?#{principal?.id}
- WHERE id = ?1
- """)
- void deleteById(Long id);
-
- @Override
- @Transactional
- default void delete(User user) {
- deleteById(user.getId());
- }
-
- @Override
- @Transactional
- default void deleteAll(Iterable extends User> entities) {
- entities.forEach(entity -> deleteById(entity.getId()));
- }
-
- public Page findDistinctBy(Pageable pageable);
-
- public Boolean existsByEmail(String email);
-
- public Optional findOptionalByIdAndDeletedAtIsNull(Long id);
-
- @Query("""
- SELECT user.name FROM #{#entityName} user
- WHERE user.id = ?1
- """)
- public Optional findNameById(Long id);
-
- @Query("""
- SELECT user FROM #{#entityName} user
- LEFT JOIN FETCH user.roles
- WHERE user.id = ?1
- """)
- public Optional findOptionalByIdAndDeletedAtIsNullFetchRoles(Long id);
-
- @Query("""
- SELECT user FROM #{#entityName} user
- LEFT JOIN FETCH user.roles
- WHERE user.email = ?1
- """)
- public Optional findOptionalByEmailFetchRoles(String email);
-
- public Optional findOptionalByEmail(String email);
-
- @Query("""
- SELECT
- new com.github.throyer.common.springboot.domain.services.user.dto.UserDetails(
- user.id,
- user.name,
- user.email
- )
- FROM #{#entityName} user
- """)
- public Page findSimplifiedUsers(Pageable pageable);
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/email/MailService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/email/MailService.java
deleted file mode 100644
index 0d89b7d4..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/email/MailService.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.email;
-
-import javax.mail.MessagingException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.mail.MailException;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.mail.javamail.MimeMessageHelper;
-import org.springframework.stereotype.Service;
-import org.springframework.web.server.ResponseStatusException;
-import org.thymeleaf.TemplateEngine;
-
-@Service
-public class MailService {
- @Autowired
- private TemplateEngine engine;
-
- @Autowired
- private JavaMailSender sender;
-
- private static Logger LOGGER = LoggerFactory.getLogger(MailService.class);
- private static String ERROR_MESSAGE = "Erro ao enviar email.";
- private static final Boolean CONTENT_IS_HTML = true;
-
- public void send(Email email) {
- try {
- var message = sender.createMimeMessage();
- var helper = new MimeMessageHelper(message);
- helper.setTo(email.getDestination());
- helper.setSubject(email.getSubject());
- helper.setText(engine.process(email.getTemplate(), email.getContext()), CONTENT_IS_HTML);
- sender.send(message);
- LOGGER.info("Email enviado com sucesso para: {}", email.getDestination());
- } catch (MessagingException | MailException exception) {
- LOGGER.error(ERROR_MESSAGE, exception);
- throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, ERROR_MESSAGE);
- }
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryConfirmService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryConfirmService.java
deleted file mode 100644
index a64c4a0e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryConfirmService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.recovery;
-
-import com.github.throyer.common.springboot.domain.models.shared.Type;
-import com.github.throyer.common.springboot.domain.repositories.RecoveryRepository;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.user.dto.Codes;
-import com.github.throyer.common.springboot.domain.services.user.dto.Update;
-import com.github.throyer.common.springboot.utils.Toasts;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Service;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Service
-public class RecoveryConfirmService {
-
- @Autowired
- private UserRepository users;
-
- @Autowired
- private RecoveryRepository recoveries;
-
- public String confirm(Codes codes, BindingResult result, Model model, RedirectAttributes redirect) {
-
- if (result.hasErrors()) {
- Toasts.add(model, result);
- model.addAttribute("confirm", codes);
- return "app/recovery/confirm";
- }
-
- try {
-
- confirm(codes.getEmail(), codes.code());
- return "redirect:/app/recovery/update";
-
- } catch (ResponseStatusException exception) {
-
- if (exception.getStatus().equals(HttpStatus.FORBIDDEN)) {
-
- Toasts.add(model, "Código expirado ou invalido.", Type.DANGER);
- model.addAttribute("confirm", codes);
-
- return "app/recovery/confirm";
- }
-
- model.addAttribute("update", new Update(codes));
-
- return "app/recovery/update";
- }
- }
-
- public void confirm(String email, String code) {
- var user = users.findOptionalByEmail(email)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
-
- var recovery = recoveries
- .findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresInDesc(user.getId())
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
-
- if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
- throw new ResponseStatusException(HttpStatus.FORBIDDEN);
- }
-
- recovery.setConfirmed(true);
-
- recoveries.save(recovery);
-
- throw new ResponseStatusException(HttpStatus.NO_CONTENT);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryService.java
deleted file mode 100644
index 3b6c805b..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryService.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.recovery;
-
-import com.github.throyer.common.springboot.domain.models.emails.RecoveryEmail;
-import com.github.throyer.common.springboot.domain.models.entity.Recovery;
-import com.github.throyer.common.springboot.domain.repositories.RecoveryRepository;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.email.MailService;
-import com.github.throyer.common.springboot.domain.services.user.dto.Codes;
-import com.github.throyer.common.springboot.domain.services.user.dto.RecoveryRequest;
-import com.github.throyer.common.springboot.utils.Toasts;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-
-@Service
-public class RecoveryService {
-
- private static final Integer MINUTES_TO_EXPIRE = 20;
-
- @Autowired
- private UserRepository users;
-
- @Autowired
- private RecoveryRepository recoveries;
-
- @Autowired
- private MailService service;
-
- public String recovery(RecoveryRequest recovery, BindingResult result, Model model) {
-
- if (result.hasErrors()) {
- Toasts.add(model, result);
- model.addAttribute("recovery", recovery);
- return "app/recovery/index";
- }
-
- var email = recovery.getEmail();
-
- recovery(email);
-
- model.addAttribute("codes", new Codes(email));
-
- return "app/recovery/confirm";
- }
-
- public void recovery(String email) {
- var user = users.findOptionalByEmail(email);
-
- if (user.isEmpty()) {
- return;
- }
-
- var recovery = new Recovery(user.get(), MINUTES_TO_EXPIRE);
-
- recoveries.save(recovery);
-
- try {
- var recoveryEmail = new RecoveryEmail(
- email,
- "password recovery code",
- user.get().getName(),
- recovery.getCode()
- );
-
- service.send(recoveryEmail);
- } catch (Exception exception) { }
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryUpdateService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryUpdateService.java
deleted file mode 100644
index 4c568a3f..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/recovery/RecoveryUpdateService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.recovery;
-
-import com.github.throyer.common.springboot.domain.models.shared.Type;
-import com.github.throyer.common.springboot.domain.repositories.RecoveryRepository;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.user.dto.Update;
-import com.github.throyer.common.springboot.utils.Toasts;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Service;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Service
-public class RecoveryUpdateService {
-
- @Autowired
- private UserRepository users;
-
- @Autowired
- private RecoveryRepository recoveries;
-
- public String update(
- Update update,
- BindingResult result,
- Model model,
- RedirectAttributes redirect
- ) {
-
- update.validate(result);
-
- if (result.hasErrors()) {
- Toasts.add(model, result);
- model.addAttribute("update", update);
- return "app/recovery/update";
- }
-
- try {
- update(update.getEmail(), update.code(), update.getPassword());
- return "redirect:/app/login";
- } catch (ResponseStatusException exception) {
- if (exception.getStatus().equals(HttpStatus.FORBIDDEN)) {
- Toasts.add(model, "Código expirado ou invalido.", Type.DANGER);
- model.addAttribute("update", update);
- return "app/recovery/update";
- }
-
- Toasts.add(redirect, "Sua senha foi atualizada com sucesso.", Type.SUCCESS);
- return "redirect:/app/login";
- }
- }
-
- public void update(String email, String code, String password) {
- var user = users.findOptionalByEmail(email)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
-
- var recovery = recoveries
- .findFirstOptionalByUser_IdAndConfirmedIsTrueAndUsedIsFalseOrderByExpiresInDesc(user.getId())
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
-
- if (!recovery.nonExpired() || !recovery.getCode().equals(code)) {
- throw new ResponseStatusException(HttpStatus.FORBIDDEN);
- }
-
- user.updatePassword(password);
- users.save(user);
-
- recovery.setUsed(true);
- recoveries.save(recovery);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/JsonWebToken.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/JsonWebToken.java
deleted file mode 100644
index 04190c6f..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/JsonWebToken.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security;
-
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.ROLES_KEY_ON_JWT;
-
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.models.security.Authorized;
-
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-
-
-public class JsonWebToken {
-
- public String encode(User user, LocalDateTime expiration, String secret) {
- return encode(user.getId(), user.getRoles(), expiration, secret);
- }
-
- public String encode(Long id, List authorities, LocalDateTime expiration, String secret) {
- return Jwts.builder()
- .setSubject(id.toString())
- .claim(ROLES_KEY_ON_JWT, authorities
- .stream()
- .map(role -> role.getAuthority())
- .collect(Collectors.joining(",")))
- .setExpiration(Date.from(expiration
- .atZone(ZoneId.systemDefault())
- .toInstant()))
- .signWith(SignatureAlgorithm.HS256, secret)
- .compact();
- }
-
- public Authorized decode(String token, String secret) {
-
- var decoded = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
-
- var id = Long.parseLong(decoded.getBody().getSubject());
- var authorities = Arrays.stream(decoded.getBody().get(ROLES_KEY_ON_JWT).toString().split(",")).map(Role::new)
- .collect(Collectors.toList());
-
- return new Authorized(id, authorities);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/SecurityService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/SecurityService.java
deleted file mode 100644
index 2d9a7ba8..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/SecurityService.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security;
-
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.INVALID_USERNAME;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.JWT;
-import static java.util.Objects.nonNull;
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
-
-import java.util.Optional;
-
-import com.github.throyer.common.springboot.domain.models.security.Authorized;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-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.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-@Service
-public class SecurityService implements UserDetailsService {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(SecurityService.class);
-
- @Autowired
- UserRepository repository;
-
- private static String SECRET;
-
- public SecurityService(@Value("${token.secret}") String secret) {
- SecurityService.SECRET = secret;
- }
-
- @Override
- public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
- return new Authorized(repository.findOptionalByEmailFetchRoles(email)
- .orElseThrow(() -> new UsernameNotFoundException(INVALID_USERNAME)));
- }
-
- public static void authorize(String token) {
- try {
- var authorized = JWT.decode(token, SECRET);
- SecurityContextHolder
- .getContext()
- .setAuthentication(authorized.getAuthentication());
- } catch (Exception exception) {
- LOGGER.error("Token expired or invalid");
- }
- }
-
- public static Optional authorized() {
- try {
- var principal = getPrincipal();
-
- if (nonNull(principal) && principal instanceof Authorized authorized) {
- return of(authorized);
- }
- return empty();
- } catch (Exception exception) {
- return empty();
- }
-
- }
-
- private static Object getPrincipal() {
- var authentication = SecurityContextHolder.getContext()
- .getAuthentication();
- if (nonNull(authentication)) {
- return authentication.getPrincipal();
- }
- return null;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/SessionService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/SessionService.java
deleted file mode 100644
index c9460e58..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/SessionService.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security;
-
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.CREATE_SESSION_ERROR_MESSAGE;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.REFRESH_SESSION_ERROR_MESSAGE;
-import static com.github.throyer.common.springboot.utils.Constants.SECURITY.JWT;
-import static com.github.throyer.common.springboot.utils.Responses.ok;
-import static com.github.throyer.common.springboot.utils.Responses.forbidden;
-
-import java.time.LocalDateTime;
-
-import com.github.throyer.common.springboot.domain.models.entity.RefreshToken;
-import com.github.throyer.common.springboot.domain.repositories.RefreshTokenRepository;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.security.dto.RefreshTokenRequest;
-import com.github.throyer.common.springboot.domain.services.security.dto.RefreshTokenResponse;
-import com.github.throyer.common.springboot.domain.services.security.dto.TokenRequest;
-import com.github.throyer.common.springboot.domain.services.security.dto.TokenResponse;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-
-@Service
-public class SessionService {
-
- @Value("${token.secret}")
- private String TOKEN_SECRET;
-
- @Value("${token.expiration-in-hours}")
- private Integer TOKEN_EXPIRATION_IN_HOURS;
-
- @Value("${token.refresh.expiration-in-days}")
- private Integer REFRESH_TOKEN_EXPIRATION_IN_DAYS;
-
- @Autowired
- private UserRepository userRepository;
-
- @Autowired
- private RefreshTokenRepository refreshTokenRepository;
-
- public ResponseEntity create(TokenRequest request) {
- var user = userRepository.findOptionalByEmailFetchRoles(request.getEmail())
- .filter(session -> session.validatePassword(request.getPassword()))
- .orElseThrow(() -> forbidden(CREATE_SESSION_ERROR_MESSAGE));
-
- var now = LocalDateTime.now();
- var expiresIn = now.plusHours(TOKEN_EXPIRATION_IN_HOURS);
-
- var token = JWT.encode(user, expiresIn, TOKEN_SECRET);
- var refresh = new RefreshToken(user, REFRESH_TOKEN_EXPIRATION_IN_DAYS);
-
- refreshTokenRepository.disableOldRefreshTokens(user.getId());
-
- refreshTokenRepository.save(refresh);
-
- var response = new TokenResponse(
- user,
- token,
- refresh,
- expiresIn
- );
-
- return ok(response);
- }
-
- public ResponseEntity refresh(RefreshTokenRequest request) {
- var old = refreshTokenRepository.findOptionalByCodeAndAvailableIsTrue(request.getRefresh())
- .filter(token -> token.nonExpired())
- .orElseThrow(() -> forbidden(REFRESH_SESSION_ERROR_MESSAGE));
-
- var now = LocalDateTime.now();
- var expiresIn = now.plusHours(TOKEN_EXPIRATION_IN_HOURS);
- var token = JWT.encode(old.getUser(), expiresIn, TOKEN_SECRET);
-
- refreshTokenRepository.disableOldRefreshTokens(old.getUser().getId());
-
- var refresh = refreshTokenRepository.save(new RefreshToken(old.getUser(), REFRESH_TOKEN_EXPIRATION_IN_DAYS));
-
- var response = new RefreshTokenResponse(
- token,
- refresh,
- expiresIn
- );
-
- return ok(response);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenRequest.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenRequest.java
deleted file mode 100644
index f79ae965..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenRequest.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security.dto;
-
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class RefreshTokenRequest {
-
- @NotNull(message = "refresh_token não pode NULO.")
- @NotEmpty(message = "refresh_token invalido.")
- @JsonProperty("refresh_token")
- private String refresh;
-
- public String getRefresh() {
- return refresh;
- }
-
- public void setRefresh(String refresh) {
- this.refresh = refresh;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenResponse.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenResponse.java
deleted file mode 100644
index ec2e0a75..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenResponse.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security.dto;
-
-import java.time.LocalDateTime;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonFormat.Shape;
-import com.github.throyer.common.springboot.domain.models.entity.RefreshToken;
-
-public class RefreshTokenResponse {
- private final String token;
- private final RefreshToken refreshToken;
- private final LocalDateTime expiresIn;
- private final String type = "Bearer";
-
- public RefreshTokenResponse(
- String token,
- RefreshToken refreshToken,
- LocalDateTime expiresIn
- ) {
- this.token = token;
- this.refreshToken = refreshToken;
- this.expiresIn = expiresIn;
- }
-
- @JsonProperty("access_token")
- public String getToken() {
- return token;
- }
-
- @JsonProperty("refresh_token")
- public String getRefresh() {
- return refreshToken.getCode();
- }
-
- @JsonFormat(shape = Shape.STRING)
- @JsonProperty("expires_in")
- public LocalDateTime getExpiresIn() {
- return expiresIn;
- }
-
- @JsonProperty("token_type")
- public String getTokenType() {
- return type;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenRequest.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenRequest.java
deleted file mode 100644
index d1b0619e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenRequest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security.dto;
-
-import java.util.Objects;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
-public class TokenRequest {
-
- @NotNull(message = "O Email não pode ser NULO.")
- @NotEmpty(message = "Email invalido.")
- private String email;
-
- @NotNull(message = "A Senha não pode ser NULA.")
- @NotEmpty(message = "Senha invalida.")
- private String password;
-
- public void setEmail(String email) {
- this.email = email;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- @Override
- public int hashCode() {
- int hash = 3;
- hash = 47 * hash + Objects.hashCode(this.email);
- return hash;
- }
-
- public String getEmail() {
- return email;
- }
-
- public String getPassword() {
- return password;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final TokenRequest other = (TokenRequest) obj;
- if (!Objects.equals(this.email, other.email)) {
- return false;
- }
- return true;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenResponse.java b/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenResponse.java
deleted file mode 100644
index f65f179d..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenResponse.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.security.dto;
-
-import java.time.LocalDateTime;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonFormat.Shape;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.github.throyer.common.springboot.domain.models.entity.RefreshToken;
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-
-public class TokenResponse {
- private final UserDetails user;
- private final String token;
- private final RefreshToken refreshToken;
- private final LocalDateTime expiresIn;
- private final String type = "Bearer";
-
- public TokenResponse(
- User user,
- String token,
- RefreshToken refreshToken,
- LocalDateTime expiresIn
- ) {
- this.user = new UserDetails(user);
- this.token = token;
- this.refreshToken = refreshToken;
- this.expiresIn = expiresIn;
- }
-
- public UserDetails getUser() {
- return user;
- }
-
- @JsonProperty("access_token")
- public String getToken() {
- return token;
- }
-
- @JsonProperty("refresh_token")
- public String getRefresh() {
- return refreshToken.getCode();
- }
-
- @JsonFormat(shape = Shape.STRING)
- @JsonProperty("expires_in")
- public LocalDateTime getExpiresIn() {
- return expiresIn;
- }
-
- @JsonProperty("token_type")
- public String getTokenType() {
- return type;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/CreateUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/CreateUserService.java
deleted file mode 100644
index f88fe04e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/CreateUserService.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user;
-
-import static com.github.throyer.common.springboot.utils.Responses.created;
-
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.shared.Type;
-import com.github.throyer.common.springboot.domain.repositories.RoleRepository;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApi;
-import com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApp;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-import com.github.throyer.common.springboot.utils.Toasts;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-import org.springframework.ui.Model;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
-
-@Service
-public class CreateUserService {
-
- @Autowired
- UserRepository userRepository;
-
- @Autowired
- RoleRepository roleRepository;
-
- public ResponseEntity create(CreateUserApi create) {
-
- create.validate();
-
- var body = create.user();
-
- var role = roleRepository.findOptionalByInitials("USER")
- .orElseThrow();
-
- body.setRoles(List.of(role));
-
- var user = userRepository.save(body);
-
- return created(new UserDetails(user), "users");
- }
-
- public String create(
- CreateUserApp create,
- BindingResult result,
- RedirectAttributes redirect,
- Model model
- ) {
- create.validate(result);
-
- if (result.hasErrors()) {
- model.addAttribute("user", create);
- Toasts.add(model, result);
- return "app/register/index";
- }
-
- var user = create.user();
-
- var role = roleRepository.findOptionalByInitials("USER")
- .orElseThrow();
-
- user.setRoles(List.of(role));
-
- userRepository.save(user);
-
- Toasts.add(redirect, "Cadastro realizado com sucesso.", Type.SUCCESS);
-
- return "redirect:/app/login";
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/FindUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/FindUserService.java
deleted file mode 100644
index 4dafc803..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/FindUserService.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user;
-
-import static com.github.throyer.common.springboot.domain.services.security.SecurityService.authorized;
-import static com.github.throyer.common.springboot.utils.Responses.notFound;
-import static com.github.throyer.common.springboot.utils.Responses.ok;
-import static com.github.throyer.common.springboot.utils.Responses.unauthorized;
-
-import com.github.throyer.common.springboot.domain.models.pagination.Page;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-import com.github.throyer.common.springboot.domain.models.pagination.Pagination;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-
-import java.util.Optional;
-import java.math.BigInteger;
-import java.util.List;
-
-import javax.persistence.EntityManager;
-import javax.persistence.Tuple;
-
-@Service
-public class FindUserService {
-
- @Autowired
- UserRepository repository;
-
- @Autowired
- EntityManager manager;
-
- public Page findAll(
- Optional page,
- Optional size
- ) {
- var sql = """
- select
- u.id,
- u.name,
- u.email,
- (select
- GROUP_CONCAT(r.initials)
- from role r
- left join user_role ur on r.id = ur.role_id
- where ur.user_id = u.id) as roles
- from
- user u
- where u.deleted_at is null
- """;
-
- var query = manager.createNativeQuery(sql, Tuple.class);
- var count = ((BigInteger) manager.createNativeQuery("""
- select
- count(u.id)
- from
- user u
- where u.deleted_at is null
- """).getSingleResult()).longValue();
-
- var pageable = Pagination.of(page, size);
-
- var pageNumber = pageable.getPageNumber();
- var pageSize = pageable.getPageSize();
-
- query.setFirstResult(pageNumber * pageSize);
- query.setMaxResults(pageSize);
-
- List content = query.getResultList();
-
- var users = content.stream().map(tuple -> new UserDetails(
- tuple.get("id", BigInteger.class).longValue(),
- tuple.get("name", String.class),
- tuple.get("email", String.class),
- tuple.get("roles", String.class)
- )).toList();
-
- return Page.of(users, pageNumber, pageSize, count);
- }
-
- public ResponseEntity find(Long id) {
- return authorized()
- .filter(authorized -> authorized.cantRead(id)).map((authorized) -> repository
- .findOptionalByIdAndDeletedAtIsNullFetchRoles(id).map(user -> ok(new UserDetails(user))).orElseGet(() -> notFound()))
- .orElse(unauthorized());
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/RemoveUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/RemoveUserService.java
deleted file mode 100644
index b6dc75a1..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/RemoveUserService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user;
-
-import static com.github.throyer.common.springboot.utils.Responses.noContent;
-import static com.github.throyer.common.springboot.utils.Responses.notFound;
-
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-
-@Service
-public class RemoveUserService {
-
- @Autowired
- UserRepository repository;
-
- public ResponseEntity remove(Long id) {
- return repository.findOptionalByIdAndDeletedAtIsNull(id)
- .map(user -> noContent(user, repository))
- .orElseGet(() -> notFound());
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/UpdateUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/UpdateUserService.java
deleted file mode 100644
index 5703feaa..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/UpdateUserService.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user;
-
-import static com.github.throyer.common.springboot.domain.services.security.SecurityService.authorized;
-import static com.github.throyer.common.springboot.domain.validation.EmailValidations.validateEmailUniquenessOnModify;
-import static com.github.throyer.common.springboot.utils.Responses.notFound;
-import static com.github.throyer.common.springboot.utils.Responses.ok;
-import static com.github.throyer.common.springboot.utils.Responses.unauthorized;
-
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-import com.github.throyer.common.springboot.domain.services.user.dto.UpdateUser;
-import com.github.throyer.common.springboot.domain.services.user.dto.UserDetails;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-
-@Service
-public class UpdateUserService {
-
- @Autowired
- UserRepository repository;
-
- public ResponseEntity update(Long id, UpdateUser body) {
-
- authorized()
- .filter(authorized -> authorized.cantModify(id))
- .orElseThrow(() -> unauthorized("Permissão invalida atualização de recurso"));
-
- var actual = repository
- .findOptionalByIdAndDeletedAtIsNullFetchRoles(id)
- .orElseThrow(() -> notFound("Usuário não encontrado"));
-
- validateEmailUniquenessOnModify(body, actual);
-
- actual.merge(body);
-
- return ok(new UserDetails(repository.save(actual)));
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApi.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApi.java
deleted file mode 100644
index 7b56921a..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApi.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import static com.github.throyer.common.springboot.domain.validation.EmailValidations.validateEmailUniqueness;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.Pattern;
-
-import com.github.throyer.common.springboot.domain.builders.UserBuilder;
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.models.shared.HasEmail;
-import lombok.Data;
-
-@Data
-public class CreateUserApi implements HasEmail {
-
- public static final String DEFAULT_PASSWORD = "mudar123";
- public static final String STRONG_PASSWORD = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,}$";
- public static final String STRONG_PASSWORD_MESSAGE = "No mínimo 8 caracteres, com no mínimo um número, um caractere especial, uma letra maiúscula e uma letra minúscula.";
-
- @NotEmpty(message = "Por favor, forneça um nome.")
- private String name;
-
- @NotEmpty(message = "Por favor, forneça um e-mail.")
- @Email(message = "Por favor, forneça um e-mail valido.")
- private String email;
-
- @NotEmpty(message = "Por favor, forneça uma senha.")
- @Pattern(regexp = STRONG_PASSWORD, message = STRONG_PASSWORD_MESSAGE)
- private String password = DEFAULT_PASSWORD;
-
- public CreateUserApi(String name, String email, String password) {
- setName(name);
- setEmail(email);
- setPassword(password);
- }
-
- public void validate() {
- validateEmailUniqueness(this);
- }
-
- public User user() {
- return new UserBuilder(name)
- .setEmail(email)
- .setPassword(password)
- .build();
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApp.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApp.java
deleted file mode 100644
index 7ed43aad..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApp.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import static com.github.throyer.common.springboot.domain.validation.EmailValidations.validateEmailUniqueness;
-
-import com.github.throyer.common.springboot.domain.builders.UserBuilder;
-
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.models.shared.HasEmail;
-
-import static com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApi.STRONG_PASSWORD;
-import static com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApi.STRONG_PASSWORD_MESSAGE;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.Pattern;
-import javax.validation.constraints.Size;
-
-import lombok.Data;
-
-import org.springframework.validation.BindingResult;
-
-@Data
-public class CreateUserApp implements HasEmail {
-
- @NotEmpty(message = "Por favor, forneça um nome.")
- private String name;
-
- @NotEmpty(message = "Por favor, forneça um e-mail.")
- @Email(message = "Por favor, forneça um e-mail valido.")
- private String email;
-
- @NotEmpty(message = "Por favor, forneça uma senha.")
- @Size(min = 8, max = 255, message = "A senha deve conter no minimo {min} caracteres.")
- private String password;
-
- public void validate(BindingResult result) {
- validateEmailUniqueness(this, result);
- }
-
- public User user() {
- return new UserBuilder(name)
- .setEmail(email)
- .setPassword(password)
- .build();
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryConfirm.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryConfirm.java
deleted file mode 100644
index 1d21c0d9..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryConfirm.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
-public class RecoveryConfirm {
-
- @Email
- @NotNull
- @NotEmpty
- private String email;
-
- @Email
- @NotNull
- @NotEmpty
- private String code;
-
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
- this.email = email;
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryRequest.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryRequest.java
deleted file mode 100644
index 28a2919e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryRequest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-import lombok.Data;
-
-@Data
-public class RecoveryRequest {
-
- @Email
- @NotNull
- @NotEmpty
- private String email;
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryUpdate.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryUpdate.java
deleted file mode 100644
index 6cb3c15e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryUpdate.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
-public class RecoveryUpdate {
- @Email
- @NotNull
- @NotEmpty
- private String email;
-
- @Email
- @NotNull
- @NotEmpty
- private String code;
-
- @Email
- @NotNull
- @NotEmpty
- private String password;
-
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
- this.email = email;
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Update.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Update.java
deleted file mode 100644
index 70f1f212..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Update.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Size;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import org.springframework.beans.BeanUtils;
-import org.springframework.validation.BindingResult;
-
-@Data
-@NoArgsConstructor
-public class Update {
-
- @Email
- @NotNull
- @NotEmpty
- private String email;
-
- private String first = "";
- private String second = "";
- private String third = "";
- private String fourth = "";
-
- @NotEmpty(message = "Por favor, forneça uma senha.")
- @Size(min = 8, max = 255, message = "A senha deve conter no minimo {min} caracteres.")
- private String password;
-
- public Update(Codes codes) {
- BeanUtils.copyProperties(codes, this);
- }
-
- public void validate(BindingResult result) { }
-
- public String code() {
- return String.format("%s%s%s%s", first, second, third, fourth);
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UpdateUser.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UpdateUser.java
deleted file mode 100644
index bf59107e..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UpdateUser.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
-import com.github.throyer.common.springboot.domain.models.shared.HasEmail;
-
-public class UpdateUser implements HasEmail {
-
- @NotNull(message = "O nome não pode ser NULL.")
- @NotEmpty(message = "Por favor, forneça um nome.")
- private String name;
-
- @NotNull(message = "O e-mail não pode ser NULL.")
- @Email(message = "Por favor, forneça um e-mail valido.")
- private String email;
-
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Override
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
- this.email = email;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UserDetails.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UserDetails.java
deleted file mode 100644
index d03e76f5..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UserDetails.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.github.throyer.common.springboot.domain.services.user.dto;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.entity.User;
-import com.github.throyer.common.springboot.domain.models.shared.Entity;
-import static java.util.Optional.ofNullable;
-
-public class UserDetails implements Entity {
- private final Long id;
- private final String name;
- private final String email;
-
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private final List roles;
-
- public UserDetails(User user) {
- this.id = user.getId();
- this.name = user.getName();
- this.email = user.getEmail();
-
- this.roles = user.getRoles()
- .stream()
- .map(role -> role.getAuthority())
- .toList();
- }
-
- public UserDetails(Long id, String name, String email) {
- this.id = id;
- this.name = name;
- this.email = email;
- this.roles = null;
- }
-
- public UserDetails(Long id, String name, String email, String roles) {
- this.id = id;
- this.name = name;
- this.email = email;
-
- this.roles = ofNullable(roles)
- .map(string -> List.of(string.split(",")))
- .orElse(List.of());
- }
-
- public Long getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public String getEmail() {
- return email;
- }
-
- public List getRoles() {
- return roles;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailNotUniqueException.java b/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailNotUniqueException.java
deleted file mode 100644
index 5bfe76e2..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailNotUniqueException.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.github.throyer.common.springboot.domain.validation;
-
-import java.util.List;
-
-public class EmailNotUniqueException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- private List errors;
-
- public EmailNotUniqueException(List errors) {
- super();
- this.errors = errors;
- }
-
- public EmailNotUniqueException(String message, List errors) {
- super(message);
- this.errors = errors;
- }
-
- public EmailNotUniqueException(String message, Throwable cause, List errors) {
- super(message, cause);
- this.errors = errors;
- }
-
- public EmailNotUniqueException(Throwable cause, List errors) {
- super(cause);
- this.errors = errors;
- }
-
- public List getErrors() {
- return errors;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailValidations.java b/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailValidations.java
deleted file mode 100644
index 99085624..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailValidations.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.github.throyer.common.springboot.domain.validation;
-
-import java.util.List;
-
-import com.github.throyer.common.springboot.domain.models.shared.HasEmail;
-import com.github.throyer.common.springboot.domain.repositories.UserRepository;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.validation.BindingResult;
-import org.springframework.validation.ObjectError;
-
-@Component
-public class EmailValidations {
-
- private static UserRepository repository;
-
- private static String FIELD = "email";
- private static String MESSAGE = "Este email já foi utilizado por outro usuário. Por favor utilize um email diferente.";
-
- private static final List EMAIL_ERROR = List.of(new SimpleError(FIELD, MESSAGE));
-
- @Autowired
- public EmailValidations(UserRepository repository) {
- EmailValidations.repository = repository;
- }
-
- public static void validateEmailUniqueness(HasEmail entity) {
- if (repository.existsByEmail(entity.getEmail())) {
- throw new EmailNotUniqueException(EMAIL_ERROR);
- }
- }
-
- public static void validateEmailUniqueness(HasEmail entity, BindingResult result) {
- if (repository.existsByEmail(entity.getEmail())) {
- result.addError(new ObjectError(FIELD, MESSAGE));
- }
- }
-
- public static void validateEmailUniquenessOnModify(HasEmail newEntity, HasEmail actualEntity) {
-
- var newEmail = newEntity.getEmail();
- var actualEmail = actualEntity.getEmail();
-
- var changedEmail = !actualEmail.equals(newEmail);
-
- var emailAlreadyUsed = repository.existsByEmail(newEmail);
-
- if (changedEmail && emailAlreadyUsed) {
- throw new EmailNotUniqueException(EMAIL_ERROR);
- }
- }
-
- public static void validateEmailUniquenessOnModify(
- HasEmail newEntity,
- HasEmail actualEntity,
- BindingResult result
- ) {
-
- var newEmail = newEntity.getEmail();
- var actualEmail = actualEntity.getEmail();
-
- var changedEmail = !actualEmail.equals(newEmail);
-
- var emailAlreadyUsed = repository.existsByEmail(newEmail);
-
- if (changedEmail && emailAlreadyUsed) {
- result.addError(new ObjectError(FIELD, MESSAGE));
- }
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/validation/InvalidSortException.java b/src/main/java/com/github/throyer/common/springboot/domain/validation/InvalidSortException.java
deleted file mode 100644
index 0210f19c..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/validation/InvalidSortException.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.github.throyer.common.springboot.domain.validation;
-
-import java.util.List;
-
-public class InvalidSortException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- private List errors;
-
- public InvalidSortException(List errors) {
- super();
- this.errors = errors;
- }
-
- public InvalidSortException(String message, List errors) {
- super(message);
- this.errors = errors;
- }
-
- public InvalidSortException(String message, Throwable cause, List errors) {
- super(message, cause);
- this.errors = errors;
- }
-
- public InvalidSortException(Throwable cause, List errors) {
- super(cause);
- this.errors = errors;
- }
-
- public List getErrors() {
- return errors;
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/domain/validation/SimpleError.java b/src/main/java/com/github/throyer/common/springboot/domain/validation/SimpleError.java
deleted file mode 100644
index baf705d9..00000000
--- a/src/main/java/com/github/throyer/common/springboot/domain/validation/SimpleError.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.github.throyer.common.springboot.domain.validation;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.validation.FieldError;
-import org.springframework.validation.ObjectError;
-
-public class SimpleError {
-
- @JsonInclude(Include.NON_NULL)
- private String field;
-
- @JsonInclude(Include.NON_NULL)
- private String message;
-
- @JsonInclude(Include.NON_NULL)
- private Integer status;
-
- public SimpleError(FieldError error) {
- this.field = error.getField();
- this.message = error.getDefaultMessage();
- }
-
- public SimpleError(ObjectError error) {
- this.message = error.getDefaultMessage();
- }
-
- public SimpleError(String filed, String message) {
- this.field = filed;
- this.message = message;
- }
-
- public SimpleError(String message, Integer status) {
- this.message = message;
- this.status = status;
- }
-
- public SimpleError(String message, HttpStatus status) {
- this.message = message;
- this.status = status.value();
- }
-
- public void setField(String field) {
- this.field = field;
- }
-
- public String getField() {
- return field;
- }
-
- public void setMessage(String message) {
- this.message = message;
- }
-
- public String getMessage() {
- return message;
- }
-
- public Integer getStatus() {
- return status;
- }
-
- public void setStatus(Integer status) {
- this.status = status;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java b/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java
deleted file mode 100644
index 42f2cea6..00000000
--- a/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.github.throyer.common.springboot.errors;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.servlet.http.HttpServletResponse;
-
-import com.github.throyer.common.springboot.domain.validation.EmailNotUniqueException;
-import com.github.throyer.common.springboot.domain.validation.InvalidSortException;
-import com.github.throyer.common.springboot.domain.validation.SimpleError;
-import static com.github.throyer.common.springboot.utils.JsonUtils.toJson;
-import static org.springframework.http.HttpStatus.BAD_REQUEST;
-import static org.springframework.http.HttpStatus.FORBIDDEN;
-import static org.springframework.http.HttpStatus.UNAUTHORIZED;
-
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.AccessDeniedException;
-import org.springframework.validation.FieldError;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.ResponseStatus;
-import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.springframework.web.server.ResponseStatusException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@RestControllerAdvice
-public class ValidationHandlers {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(ValidationHandlers.class);
-
- @ResponseStatus(code = BAD_REQUEST)
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public List badRequest(MethodArgumentNotValidException exception) {
- return exception.getBindingResult()
- .getAllErrors()
- .stream()
- .map((error) -> (new SimpleError((FieldError)error)))
- .collect(Collectors.toList());
- }
-
- @ResponseStatus(code = BAD_REQUEST)
- @ExceptionHandler(EmailNotUniqueException.class)
- public List badRequest(EmailNotUniqueException exception) {
- return exception.getErrors();
- }
-
- @ExceptionHandler(ResponseStatusException.class)
- public ResponseEntity status(ResponseStatusException exception) {
- return ResponseEntity
- .status(exception.getStatus())
- .body(new SimpleError(exception.getReason(), exception.getStatus()));
- }
-
- @ResponseStatus(code = BAD_REQUEST)
- @ExceptionHandler(InvalidSortException.class)
- public List badRequest(InvalidSortException exception) {
- return exception.getErrors();
- }
-
- @ResponseStatus(code = UNAUTHORIZED)
- @ExceptionHandler(AccessDeniedException.class)
- public SimpleError unauthorized(AccessDeniedException exception) {
- return new SimpleError(exception.getMessage(), UNAUTHORIZED);
- }
-
- public static void forbidden(HttpServletResponse response) {
- try {
- response.setStatus(FORBIDDEN.value());
- response.setContentType("application/json");
- response.getWriter().write(toJson(
- new SimpleError("Can't find token on Authorization header.", FORBIDDEN)
- ));
- } catch (Exception exception) {
- LOGGER.error("can't write response error on token expired or invalid exception", exception);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/middlewares/AuthorizationMiddleware.java b/src/main/java/com/github/throyer/common/springboot/middlewares/AuthorizationMiddleware.java
deleted file mode 100644
index 3cd1b1dd..00000000
--- a/src/main/java/com/github/throyer/common/springboot/middlewares/AuthorizationMiddleware.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.github.throyer.common.springboot.middlewares;
-
-import static com.github.throyer.common.springboot.utils.TokenUtils.authorization;
-import static java.util.Optional.ofNullable;
-
-import java.io.IOException;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import com.github.throyer.common.springboot.domain.services.security.SecurityService;
-
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-@Component
-public class AuthorizationMiddleware extends OncePerRequestFilter {
-
- @Override
- protected void doFilterInternal(
- HttpServletRequest request,
- HttpServletResponse response,
- FilterChain filter
- )
- throws ServletException, IOException {
-
- ofNullable(authorization(request))
- .ifPresent(SecurityService::authorize);
-
- filter.doFilter(request, response);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Constants.java b/src/main/java/com/github/throyer/common/springboot/utils/Constants.java
deleted file mode 100644
index 2f41440c..00000000
--- a/src/main/java/com/github/throyer/common/springboot/utils/Constants.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.github.throyer.common.springboot.utils;
-
-import com.github.throyer.common.springboot.domain.services.security.JsonWebToken;
-
-public class Constants {
- public static class SECURITY {
- public static final JsonWebToken JWT = new JsonWebToken();
- public static final Long HOUR_IN_SECONDS = 3600L;
- public static final Integer DAY_MILLISECONDS = 86400;
-
- public static final String ROLES_KEY_ON_JWT = "roles";
- public static final String INVALID_USERNAME = "Nome de usuário invalido.";
- public static final String CREATE_SESSION_ERROR_MESSAGE = "Senha ou Usuário inválidos.";
- public static final String REFRESH_SESSION_ERROR_MESSAGE = "Refresh token expirado ou inválido.";
-
- public static final String USERNAME_PARAMETER = "email";
- public static final String PASSWORD_PARAMETER = "password";
-
- public static final String HOME_URL = "/app";
- public static final String LOGIN_URL = "/app/login";
- public static final String LOGIN_ERROR_URL = LOGIN_URL + "?error=true";
- public static final String ACESSO_NEGADO_URL = LOGIN_URL + "?denied=true";
- public static final String LOGOUT_URL = "/app/logout";
-
- public static final String SESSION_COOKIE_NAME = "JSESSIONID";
-
- public static final String[] STATIC_FILES = {
- "/robots.txt",
- "/font/**",
- "/css/**",
- "/webjars/**",
- "/webjars/",
- "/js/**",
- "/favicon.ico",
- "/**.html",
- "/documentation/**"
- };
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Hello.java b/src/main/java/com/github/throyer/common/springboot/utils/Hello.java
deleted file mode 100644
index 22f2ada8..00000000
--- a/src/main/java/com/github/throyer/common/springboot/utils/Hello.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.github.throyer.common.springboot.utils;
-
-public interface Hello {
- public String getMessage();
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Migrations.java b/src/main/java/com/github/throyer/common/springboot/utils/Migrations.java
deleted file mode 100644
index 64ab4d56..00000000
--- a/src/main/java/com/github/throyer/common/springboot/utils/Migrations.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.throyer.common.springboot.utils;
-
-import static org.jooq.impl.DSL.field;
-
-import java.util.Optional;
-
-import org.jooq.Condition;
-import org.jooq.DSLContext;
-import org.jooq.Record;
-import org.jooq.Table;
-
-public class Migrations {
- public static Optional findOptionalId(DSLContext dsl, Table table, Condition condition) {
- return dsl.select(field("id"))
- .from(table)
- .where(condition)
- .limit(1)
- .fetchOptional(field("id"), Long.class);
-
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Random.java b/src/main/java/com/github/throyer/common/springboot/utils/Random.java
deleted file mode 100644
index d0d4c470..00000000
--- a/src/main/java/com/github/throyer/common/springboot/utils/Random.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.throyer.common.springboot.utils;
-
-import static com.github.throyer.common.springboot.domain.builders.UserBuilder.createUser;
-
-import java.util.List;
-import java.util.Locale;
-
-import com.github.javafaker.Faker;
-import com.github.throyer.common.springboot.domain.models.entity.Role;
-import com.github.throyer.common.springboot.domain.models.entity.User;
-
-public class Random {
-
- private static final java.util.Random RANDOM = new java.util.Random();
- public static final Faker FAKER = new Faker(new Locale("pt", "BR"));
-
- public static Integer between(Integer min, Integer max) {
- return RANDOM.nextInt(max - min) + min;
- }
-
- public static T getRandomElement(List itens) {
- return itens.get(RANDOM.nextInt(itens.size()));
- }
-
- public static String code() {
- return String.format("%s%s%s%s", between(0, 9), between(0, 9), between(0, 9), between(0, 9));
- }
-
- public static String password() {
- return FAKER.regexify("[a-z]{5,13}[1-9]{1,5}[A-Z]{1,5}[#?!@$%^&*-]{1,5}");
- }
-
- public static User randomUser() {
- return randomUser(List.of());
- }
-
- public static User randomUser(List roles) {
- var builder = createUser(FAKER.name().fullName())
- .setEmail(FAKER.internet().safeEmailAddress())
- .setPassword(password());
-
- roles.forEach(builder::addRole);
-
- return builder.build();
- }
-}
diff --git a/src/main/java/com/github/throyer/common/springboot/utils/Responses.java b/src/main/java/com/github/throyer/common/springboot/utils/Responses.java
deleted file mode 100644
index a07aae71..00000000
--- a/src/main/java/com/github/throyer/common/springboot/utils/Responses.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.github.throyer.common.springboot.utils;
-
-import java.net.URI;
-
-import com.github.throyer.common.springboot.domain.models.shared.Entity;
-
-import org.springframework.data.repository.CrudRepository;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.server.ResponseStatusException;
-
-/**
- * HTTP Responses.
- *
- * Classe util para simplificar a geração
- * de responses para status codes comuns
- * utilizando ResponseEntity
.
- */
-public class Responses {
-
- private Responses() { }
-
- public static final ResponseEntity forbidden(T body) {
- return ResponseEntity.status(403).body(body);
- }
-
- public static final ResponseEntity forbidden() {
- return ResponseEntity.status(403).build();
- }
-
- public static final ResponseEntity unauthorized(T body) {
- return ResponseEntity.status(401).body(body);
- }
-
- public static final ResponseEntity unauthorized() {
- return ResponseEntity.status(401).build();
- }
-
- public static final ResponseEntity ok(T body) {
- return ResponseEntity.ok(body);
- }
-
- public static final ResponseEntity ok() {
- return ResponseEntity.ok()
- .build();
- }
-
- public static final ResponseEntity notFound() {
- return ResponseEntity.notFound()
- .build();
- }
-
- public static final ResponseEntity badRequest(T body) {
- return ResponseEntity.badRequest()
- .body(body);
- }
-
- public static final ResponseEntity badRequest() {
- return ResponseEntity.badRequest()
- .build();
- }
-
- public static final ResponseEntity noContent() {
- return ResponseEntity.noContent().build();
- }
-
- public static final ResponseEntity noContent(T entity, CrudRepository repository) {
- repository.delete(entity);
- return ResponseEntity
- .noContent()
- .build();
- }
-
- public static final ResponseEntity created(T entity, String location) {
- return ResponseEntity.created(URI.create(String.format("/%s/%s", location, entity.getId())))
- .body(entity);
- }
-
- public static final ResponseEntity