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 index 97e9cd63..d41ad8af 100644 --- a/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java +++ b/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java @@ -15,7 +15,7 @@ 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.domain.session.service.SessionService; import com.github.throyer.common.springboot.middlewares.AuthorizationMiddleware; import org.springframework.beans.factory.annotation.Autowired; @@ -42,7 +42,7 @@ public class SpringSecurityConfiguration { @Autowired - private SecurityService securityService; + private SessionService sessionService; @Autowired private BCryptPasswordEncoder encoder; @@ -62,7 +62,7 @@ public class Api extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(securityService) + auth.userDetailsService(sessionService) .passwordEncoder(encoder); } @@ -121,7 +121,7 @@ public class App extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth. - userDetailsService(securityService) + userDetailsService(sessionService) .passwordEncoder(encoder); } 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 index e54ce961..f39a6964 100644 --- 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 @@ -2,12 +2,12 @@ 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 com.github.throyer.common.springboot.domain.recovery.service.RecoveryConfirmService; +import com.github.throyer.common.springboot.domain.recovery.service.RecoveryService; +import com.github.throyer.common.springboot.domain.recovery.service.RecoveryUpdateService; +import com.github.throyer.common.springboot.domain.recovery.model.RecoveryConfirm; +import com.github.throyer.common.springboot.domain.recovery.model.RecoveryRequest; +import com.github.throyer.common.springboot.domain.recovery.model.RecoveryUpdate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; 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 index 4ec72847..b17cd13a 100644 --- 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 @@ -4,8 +4,8 @@ import java.util.List; -import com.github.throyer.common.springboot.domain.models.entity.Role; -import com.github.throyer.common.springboot.domain.repositories.RoleRepository; +import com.github.throyer.common.springboot.domain.role.entity.Role; +import com.github.throyer.common.springboot.domain.role.repository.RoleRepository; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.springframework.beans.factory.annotation.Autowired; 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 index c059fe90..a4f6bfc0 100644 --- 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 @@ -2,11 +2,13 @@ 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 com.github.throyer.common.springboot.domain.session.model.RefreshTokenRequest; +import com.github.throyer.common.springboot.domain.session.model.RefreshTokenResponse; +import com.github.throyer.common.springboot.domain.session.model.TokenRequest; +import com.github.throyer.common.springboot.domain.session.model.TokenResponse; +import com.github.throyer.common.springboot.domain.session.service.CreateTokenService; +import com.github.throyer.common.springboot.domain.session.service.RefreshTokenService; +import static com.github.throyer.common.springboot.utils.Responses.ok; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -20,15 +22,20 @@ public class SessionsController { @Autowired - private SessionService service; + private CreateTokenService createService; + + @Autowired + private RefreshTokenService refreshService; @PostMapping public ResponseEntity create(@RequestBody @Valid TokenRequest request) { - return service.create(request); + var token = createService.create(request); + return ok(token); } @PostMapping("/refresh") public ResponseEntity refresh(@RequestBody @Valid RefreshTokenRequest request) { - return service.refresh(request); + var token = refreshService.refresh(request); + return ok(token); } } 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 index 6afd9fa6..c6936571 100644 --- 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 @@ -3,18 +3,21 @@ 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.created; import static com.github.throyer.common.springboot.utils.Responses.ok; + +import com.github.throyer.common.springboot.domain.pagination.model.Page; +import com.github.throyer.common.springboot.domain.user.service.FindUserService; +import com.github.throyer.common.springboot.domain.user.service.RemoveUserService; +import com.github.throyer.common.springboot.domain.user.model.CreateUserProps; +import com.github.throyer.common.springboot.domain.user.service.CreateUserService; +import com.github.throyer.common.springboot.domain.user.model.UpdateUserProps; +import com.github.throyer.common.springboot.domain.user.service.FindUserByIdService; +import com.github.throyer.common.springboot.domain.user.service.UpdateUserService; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; + import io.swagger.v3.oas.annotations.security.SecurityRequirement; + import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; @@ -34,18 +37,27 @@ @RestController @RequestMapping("/api/users") public class UsersController { - - @Autowired - private CreateUserService createService; - - @Autowired - private UpdateUserService updateService; - - @Autowired - private RemoveUserService removeService; - + + private final CreateUserService createService; + private final UpdateUserService updateService; + private final RemoveUserService removeService; + private final FindUserService findService; + private final FindUserByIdService findByIdService; + @Autowired - private FindUserService findService; + public UsersController( + CreateUserService createService, + UpdateUserService updateService, + RemoveUserService removeService, + FindUserService findService, + FindUserByIdService findByIdService + ) { + this.createService = createService; + this.updateService = updateService; + this.removeService = removeService; + this.findService = findService; + this.findByIdService = findByIdService; + } @GetMapping @SecurityRequirement(name = "token") @@ -54,21 +66,25 @@ public ResponseEntity> index( Optional page, Optional size ) { - var result = findService.findAll(page, size); - return ok(result); + var content = findService.findAll(page, size); + return ok(content); } @GetMapping("/{id}") @SecurityRequirement(name = "token") @PreAuthorize("hasAnyAuthority('ADM', 'USER')") public ResponseEntity show(@PathVariable Long id) { - return findService.find(id); + var user = findByIdService.find(id); + return ok(user); } @PostMapping @ResponseStatus(CREATED) - public ResponseEntity save(@Validated @RequestBody CreateUserApi body) { - return createService.create(body); + public ResponseEntity save( + @Validated @RequestBody CreateUserProps body + ) { + var user = createService.create(body); + return created(user, "api/users"); } @PutMapping("/{id}") @@ -76,16 +92,16 @@ public ResponseEntity save(@Validated @RequestBody CreateUserApi bo @PreAuthorize("hasAnyAuthority('ADM', 'USER')") public ResponseEntity update( @PathVariable Long id, - @RequestBody @Validated UpdateUser body + @RequestBody @Validated UpdateUserProps body ) { - return updateService.update(id, body); + var user = updateService.update(id, body); + return ok(user); } @DeleteMapping("/{id}") @ResponseStatus(NO_CONTENT) @SecurityRequirement(name = "token") - @PreAuthorize("hasAnyAuthority('ADM')") - public ResponseEntity destroy(@PathVariable Long id) { - return removeService.remove(id); + public void destroy(@PathVariable Long id) { + removeService.remove(id); } } \ No newline at end of file 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 index 8007ec34..09a20bd6 100644 --- 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 @@ -1,12 +1,18 @@ 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 static com.github.throyer.common.springboot.utils.Responses.validate; + +import com.github.throyer.common.springboot.domain.recovery.service.RecoveryConfirmService; +import com.github.throyer.common.springboot.domain.recovery.service.RecoveryService; +import com.github.throyer.common.springboot.domain.recovery.service.RecoveryUpdateService; +import com.github.throyer.common.springboot.domain.recovery.model.Codes; +import com.github.throyer.common.springboot.domain.recovery.model.RecoveryRequest; +import com.github.throyer.common.springboot.domain.recovery.model.Update; +import com.github.throyer.common.springboot.domain.shared.Type; +import com.github.throyer.common.springboot.utils.Toasts; + import javax.validation.Valid; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -14,21 +20,22 @@ 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; @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()); @@ -41,7 +48,18 @@ public String index( BindingResult result, Model model ) { - return recoveryService.recovery(recovery, result, model); + + if (validate(model, recovery, "recovery", result)) { + return "app/recovery/index"; + } + + var email = recovery.getEmail(); + + recoveryService.recovery(email); + + model.addAttribute("codes", new Codes(email)); + + return "app/recovery/confirm"; } @PostMapping("/confirm") @@ -51,7 +69,23 @@ public String confirm( RedirectAttributes redirect, Model model ) { - return confirmService.confirm(codes, result, model, redirect); + + if (validate(model, codes, "recovery", result)) { + return "app/recovery/confirm"; + } + + try { + confirmService.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 Update(codes)); + + return "app/recovery/update"; } @PostMapping("/update") @@ -61,6 +95,21 @@ public String update( RedirectAttributes redirect, Model model ) { - return updateService.update(update, result, model, redirect); + update.validate(result); + + if (validate(model, update, "update", result)) { + return "app/recovery/update"; + } + + try { + updateService.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/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 index 2c85ea22..6ccda644 100644 --- 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 @@ -1,9 +1,13 @@ package com.github.throyer.common.springboot.controllers.app; -import javax.validation.Valid; +import static com.github.throyer.common.springboot.domain.shared.Type.SUCCESS; +import static com.github.throyer.common.springboot.utils.Responses.validate; + +import com.github.throyer.common.springboot.domain.user.model.CreateUserProps; +import com.github.throyer.common.springboot.domain.user.service.CreateUserService; +import com.github.throyer.common.springboot.utils.Toasts; -import com.github.throyer.common.springboot.domain.services.user.CreateUserService; -import com.github.throyer.common.springboot.domain.services.user.dto.CreateUserApp; +import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -17,23 +21,32 @@ @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()); + model.addAttribute("user", new CreateUserProps()); return "app/register/index"; } @PostMapping(produces = "text/html") public String create( - @Valid CreateUserApp user, + @Valid CreateUserProps props, BindingResult result, RedirectAttributes redirect, Model model ) { - return service.create(user, result, redirect, model); + + if (validate(model, props, "user", result)) { + return "app/register/index"; + } + + service.create(props); + + Toasts.add(redirect, "Cadastro realizado com sucesso.", SUCCESS); + + return "redirect:/app/login"; } } 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 index 8ae55873..4d39dc75 100644 --- 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 @@ -1,8 +1,8 @@ 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.domain.shared.Type; +import com.github.throyer.common.springboot.domain.user.service.FindUserService; +import com.github.throyer.common.springboot.domain.user.service.RemoveUserService; import com.github.throyer.common.springboot.utils.Toasts; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; 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/validation/EmailNotUniqueException.java b/src/main/java/com/github/throyer/common/springboot/domain/mail/exceptions/EmailNotUniqueException.java similarity index 85% rename from src/main/java/com/github/throyer/common/springboot/domain/validation/EmailNotUniqueException.java rename to src/main/java/com/github/throyer/common/springboot/domain/mail/exceptions/EmailNotUniqueException.java index 5bfe76e2..a457ba49 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailNotUniqueException.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/mail/exceptions/EmailNotUniqueException.java @@ -1,5 +1,6 @@ -package com.github.throyer.common.springboot.domain.validation; +package com.github.throyer.common.springboot.domain.mail.exceptions; +import com.github.throyer.common.springboot.domain.shared.SimpleError; import java.util.List; public class EmailNotUniqueException extends RuntimeException { 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/mail/exceptions/EmailValidations.java similarity index 73% rename from src/main/java/com/github/throyer/common/springboot/domain/validation/EmailValidations.java rename to src/main/java/com/github/throyer/common/springboot/domain/mail/exceptions/EmailValidations.java index b7104dfb..7a46be01 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/validation/EmailValidations.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/mail/exceptions/EmailValidations.java @@ -1,14 +1,15 @@ -package com.github.throyer.common.springboot.domain.validation; +package com.github.throyer.common.springboot.domain.mail.exceptions; import java.util.List; -import com.github.throyer.common.springboot.domain.models.shared.HasEmail; -import com.github.throyer.common.springboot.domain.repositories.UserRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; 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.common.springboot.domain.management.model.Addressable; +import com.github.throyer.common.springboot.domain.shared.SimpleError; @Component public class EmailValidations { @@ -25,19 +26,19 @@ public EmailValidations(UserRepository repository) { EmailValidations.repository = repository; } - public static void validateEmailUniqueness(HasEmail entity) { + public static void validateEmailUniqueness(Addressable entity) { if (repository.existsByEmail(entity.getEmail())) { throw new EmailNotUniqueException(EMAIL_ERROR); } } - public static void validateEmailUniqueness(HasEmail entity, BindingResult result) { + public static void validateEmailUniqueness(Addressable entity, BindingResult result) { if (repository.existsByEmail(entity.getEmail())) { result.addError(new ObjectError(FIELD, MESSAGE)); } } - public static void validateEmailUniquenessOnModify(HasEmail newEntity, HasEmail actualEntity) { + public static void validateEmailUniquenessOnModify(Addressable newEntity, Addressable actualEntity) { var newEmail = newEntity.getEmail(); var actualEmail = actualEntity.getEmail(); @@ -52,8 +53,8 @@ public static void validateEmailUniquenessOnModify(HasEmail newEntity, HasEmail } public static void validateEmailUniquenessOnModify( - HasEmail newEntity, - HasEmail actualEntity, + Addressable newEntity, + Addressable actualEntity, BindingResult result ) { diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java b/src/main/java/com/github/throyer/common/springboot/domain/mail/model/Email.java similarity index 71% rename from src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java rename to src/main/java/com/github/throyer/common/springboot/domain/mail/model/Email.java index 388a5b7e..e9b7d2f5 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/email/Email.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/mail/model/Email.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.email; +package com.github.throyer.common.springboot.domain.mail.model; import org.thymeleaf.context.Context; 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/mail/service/MailService.java similarity index 91% rename from src/main/java/com/github/throyer/common/springboot/domain/services/email/MailService.java rename to src/main/java/com/github/throyer/common/springboot/domain/mail/service/MailService.java index 5ac7cec7..5c4fb2a3 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/email/MailService.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/mail/service/MailService.java @@ -1,5 +1,6 @@ -package com.github.throyer.common.springboot.domain.services.email; +package com.github.throyer.common.springboot.domain.mail.service; +import com.github.throyer.common.springboot.domain.mail.model.Email; import javax.mail.MessagingException; import org.slf4j.Logger; @@ -15,6 +16,7 @@ @Service public class MailService { + @Autowired private TemplateEngine engine; @@ -23,6 +25,7 @@ public class MailService { private static Logger LOGGER = LoggerFactory.getLogger(MailService.class); private static String ERROR_MESSAGE = "Error sending email."; + private static final Boolean CONTENT_IS_HTML = true; public void send(Email email) { 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/management/entity/Auditable.java similarity index 89% rename from src/main/java/com/github/throyer/common/springboot/domain/models/entity/Auditable.java rename to src/main/java/com/github/throyer/common/springboot/domain/management/entity/Auditable.java index 7a94f58e..510638ca 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Auditable.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/management/entity/Auditable.java @@ -1,6 +1,7 @@ -package com.github.throyer.common.springboot.domain.models.entity; +package com.github.throyer.common.springboot.domain.management.entity; -import static com.github.throyer.common.springboot.domain.services.security.SecurityService.authorized; +import com.github.throyer.common.springboot.domain.user.entity.User; +import static com.github.throyer.common.springboot.domain.session.service.SessionService.authorized; import static java.time.LocalDateTime.now; import static java.util.Optional.ofNullable; import static javax.persistence.FetchType.LAZY; @@ -16,12 +17,10 @@ import javax.persistence.PreUpdate; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.github.throyer.common.springboot.domain.models.shared.Entity; +import com.github.throyer.common.springboot.domain.management.model.Entity; @MappedSuperclass public abstract class Auditable implements Entity { - - public static final String NON_DELETED_CLAUSE = "deleted_at IS NULL"; @Override public abstract Long getId(); diff --git a/src/main/java/com/github/throyer/common/springboot/domain/management/model/Addressable.java b/src/main/java/com/github/throyer/common/springboot/domain/management/model/Addressable.java new file mode 100644 index 00000000..c15882f6 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/management/model/Addressable.java @@ -0,0 +1,5 @@ +package com.github.throyer.common.springboot.domain.management.model; + +public interface Addressable { + public String getEmail(); +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/management/model/Entity.java b/src/main/java/com/github/throyer/common/springboot/domain/management/model/Entity.java new file mode 100644 index 00000000..6b5750a1 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/management/model/Entity.java @@ -0,0 +1,5 @@ +package com.github.throyer.common.springboot.domain.management.model; + +public interface Entity { + Long getId(); +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/management/repository/Queries.java b/src/main/java/com/github/throyer/common/springboot/domain/management/repository/Queries.java new file mode 100644 index 00000000..ab093261 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/management/repository/Queries.java @@ -0,0 +1,20 @@ +package com.github.throyer.common.springboot.domain.management.repository; + +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/src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/management/repository/SoftDeleteRepository.java similarity index 70% rename from src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java rename to src/main/java/com/github/throyer/common/springboot/domain/management/repository/SoftDeleteRepository.java index faedf6ba..fca1877f 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/SoftDeleteRepository.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/management/repository/SoftDeleteRepository.java @@ -1,6 +1,8 @@ -package com.github.throyer.common.springboot.domain.repositories; +package com.github.throyer.common.springboot.domain.management.repository; -import com.github.throyer.common.springboot.domain.models.entity.Auditable; +import com.github.throyer.common.springboot.domain.management.entity.Auditable; +import static com.github.throyer.common.springboot.domain.management.repository.Queries.DELETE_ALL; +import static com.github.throyer.common.springboot.domain.management.repository.Queries.DELETE_BY_ID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -15,13 +17,7 @@ public interface SoftDeleteRepository extends JpaRepositor @Override @Modifying @Transactional - @Query(""" - UPDATE - #{#entityName} - SET - deleted_at = CURRENT_TIMESTAMP - WHERE id = ?1 - """) + @Query(DELETE_BY_ID) void deleteById(Long id); @Override @@ -39,11 +35,6 @@ default void deleteAll(Iterable entities) { @Override @Modifying @Transactional - @Query(""" - UPDATE - #{#entityName} - SET - deleted_at = CURRENT_TIMESTAMP - """) + @Query(DELETE_ALL) void deleteAll(); } \ No newline at end of file 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/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/models/pagination/Page.java b/src/main/java/com/github/throyer/common/springboot/domain/pagination/model/Page.java similarity index 95% rename from src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Page.java rename to src/main/java/com/github/throyer/common/springboot/domain/pagination/model/Page.java index c3273ee9..737cc4f5 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Page.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/pagination/model/Page.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.models.pagination; +package com.github.throyer.common.springboot.domain.pagination.model; import static com.github.throyer.common.springboot.utils.JsonUtils.toJson; import java.util.Collection; 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/pagination/service/Pagination.java similarity index 91% rename from src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Pagination.java rename to src/main/java/com/github/throyer/common/springboot/domain/pagination/service/Pagination.java index f51a0c00..bdd86151 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/pagination/Pagination.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/pagination/service/Pagination.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.models.pagination; +package com.github.throyer.common.springboot.domain.pagination.service; import org.springframework.data.domain.PageRequest; 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/recovery/entity/Recovery.java similarity index 93% rename from src/main/java/com/github/throyer/common/springboot/domain/models/entity/Recovery.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/entity/Recovery.java index afcabec8..0cbab37a 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Recovery.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/entity/Recovery.java @@ -1,5 +1,6 @@ -package com.github.throyer.common.springboot.domain.models.entity; +package com.github.throyer.common.springboot.domain.recovery.entity; +import com.github.throyer.common.springboot.domain.user.entity.User; import static com.github.throyer.common.springboot.utils.Random.code; import java.time.LocalDateTime; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/model/Codes.java similarity index 89% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/Codes.java index 8b0b945c..a079993d 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Codes.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/model/Codes.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.recovery.model; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; 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/recovery/model/RecoveryConfirm.java similarity index 89% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryConfirm.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryConfirm.java index 1d21c0d9..ee6d3c4a 100644 --- 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/recovery/model/RecoveryConfirm.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.recovery.model; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryEmail.java similarity index 87% rename from src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryEmail.java index 9e085a61..0be13f1d 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/emails/RecoveryEmail.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryEmail.java @@ -1,6 +1,6 @@ -package com.github.throyer.common.springboot.domain.models.emails; +package com.github.throyer.common.springboot.domain.recovery.model; -import com.github.throyer.common.springboot.domain.services.email.Email; +import com.github.throyer.common.springboot.domain.mail.model.Email; import org.thymeleaf.context.Context; 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/recovery/model/RecoveryRequest.java similarity index 78% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryRequest.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryRequest.java index 28a2919e..69b581c1 100644 --- 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/recovery/model/RecoveryRequest.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.recovery.model; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; 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/recovery/model/RecoveryUpdate.java similarity index 91% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/RecoveryUpdate.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/RecoveryUpdate.java index 6cb3c15e..587b0b83 100644 --- 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/recovery/model/RecoveryUpdate.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.recovery.model; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; 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/recovery/model/Update.java similarity index 61% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/Update.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/model/Update.java index 70f1f212..3a51fa5c 100644 --- 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/recovery/model/Update.java @@ -1,4 +1,7 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.recovery.model; + +import static java.lang.String.format; +import static org.springframework.beans.BeanUtils.copyProperties; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; @@ -6,7 +9,6 @@ import javax.validation.constraints.Size; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.beans.BeanUtils; import org.springframework.validation.BindingResult; @Data @@ -23,17 +25,17 @@ public class Update { 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.") + @NotEmpty(message = "Please provide a password.") + @Size(min = 8, max = 255, message = "The password must contain at least {min} characters.") private String password; public Update(Codes codes) { - BeanUtils.copyProperties(codes, this); + copyProperties(codes, this); } public void validate(BindingResult result) { } public String code() { - return String.format("%s%s%s%s", first, second, third, fourth); + return format("%s%s%s%s", first, second, third, fourth); } } diff --git a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/repository/RecoveryRepository.java similarity index 79% rename from src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java rename to src/main/java/com/github/throyer/common/springboot/domain/recovery/repository/RecoveryRepository.java index 3fe29c1e..a7d8f4ae 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RecoveryRepository.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/repository/RecoveryRepository.java @@ -1,8 +1,8 @@ -package com.github.throyer.common.springboot.domain.repositories; +package com.github.throyer.common.springboot.domain.recovery.repository; import java.util.Optional; -import com.github.throyer.common.springboot.domain.models.entity.Recovery; +import com.github.throyer.common.springboot.domain.recovery.entity.Recovery; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryConfirmService.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryConfirmService.java new file mode 100644 index 00000000..d3a5db30 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryConfirmService.java @@ -0,0 +1,35 @@ +package com.github.throyer.common.springboot.domain.recovery.service; + +import com.github.throyer.common.springboot.domain.recovery.repository.RecoveryRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class RecoveryConfirmService { + + @Autowired + private UserRepository users; + + @Autowired + private RecoveryRepository recoveryRepository; + + public void confirm(String email, String code) { + var user = users.findOptionalByEmail(email) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN)); + + var recovery = recoveryRepository + .findFirstOptionalByUser_IdAndConfirmedIsFalseAndUsedIsFalseOrderByExpiresInDesc(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/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryService.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryService.java new file mode 100644 index 00000000..3f0f5a66 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryService.java @@ -0,0 +1,47 @@ +package com.github.throyer.common.springboot.domain.recovery.service; + +import com.github.throyer.common.springboot.domain.recovery.model.RecoveryEmail; +import com.github.throyer.common.springboot.domain.recovery.entity.Recovery; +import com.github.throyer.common.springboot.domain.recovery.repository.RecoveryRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.mail.service.MailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@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 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/recovery/service/RecoveryUpdateService.java b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryUpdateService.java new file mode 100644 index 00000000..4db40a83 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/recovery/service/RecoveryUpdateService.java @@ -0,0 +1,37 @@ +package com.github.throyer.common.springboot.domain.recovery.service; + +import com.github.throyer.common.springboot.domain.recovery.repository.RecoveryRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class RecoveryUpdateService { + + @Autowired + private UserRepository users; + + @Autowired + private RecoveryRepository recoveries; + + 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/repositories/UserRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/repositories/UserRepository.java deleted file mode 100644 index 7eeb192a..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 = false, - 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 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/models/entity/Role.java b/src/main/java/com/github/throyer/common/springboot/domain/role/entity/Role.java similarity index 86% rename from src/main/java/com/github/throyer/common/springboot/domain/models/entity/Role.java rename to src/main/java/com/github/throyer/common/springboot/domain/role/entity/Role.java index 5da6d747..61b24276 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/Role.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/role/entity/Role.java @@ -1,5 +1,6 @@ -package com.github.throyer.common.springboot.domain.models.entity; +package com.github.throyer.common.springboot.domain.role.entity; +import com.github.throyer.common.springboot.domain.management.entity.Auditable; import java.util.Objects; import javax.persistence.Column; @@ -10,6 +11,7 @@ import javax.persistence.Table; import com.fasterxml.jackson.annotation.JsonIgnore; +import static com.github.throyer.common.springboot.domain.management.repository.Queries.NON_DELETED_CLAUSE; import lombok.Data; import org.hibernate.annotations.Where; @@ -18,7 +20,7 @@ @Data @Entity @Table(name = "role") -@Where(clause = Auditable.NON_DELETED_CLAUSE) +@Where(clause = NON_DELETED_CLAUSE) public class Role extends Auditable implements GrantedAuthority { private static final long serialVersionUID = -8524505911742593369L; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/role/repository/Queries.java b/src/main/java/com/github/throyer/common/springboot/domain/role/repository/Queries.java new file mode 100644 index 00000000..d5238414 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/role/repository/Queries.java @@ -0,0 +1,21 @@ +package com.github.throyer.common.springboot.domain.role.repository; + +public class Queries { + public static final String DELETE_ROLE_BY_ID = """ + UPDATE + #{#entityName} + SET + deleted_name = ( + SELECT name FROM #{#entityName} WHERE id = ?1 + ), + name = NULL, + deleted_initials = ( + SELECT initials FROM #{#entityName} WHERE id = ?1 + ), + initials = NULL, + deleted_at = CURRENT_TIMESTAMP, + active = false, + deleted_by = ?#{principal?.id} + WHERE id = ?1 + """; +} 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/role/repository/RoleRepository.java similarity index 59% rename from src/main/java/com/github/throyer/common/springboot/domain/repositories/RoleRepository.java rename to src/main/java/com/github/throyer/common/springboot/domain/role/repository/RoleRepository.java index 6ab5ef35..379cd691 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RoleRepository.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/role/repository/RoleRepository.java @@ -1,8 +1,10 @@ -package com.github.throyer.common.springboot.domain.repositories; +package com.github.throyer.common.springboot.domain.role.repository; +import com.github.throyer.common.springboot.domain.management.repository.SoftDeleteRepository; import java.util.Optional; -import com.github.throyer.common.springboot.domain.models.entity.Role; +import com.github.throyer.common.springboot.domain.role.entity.Role; +import static com.github.throyer.common.springboot.domain.role.repository.Queries.DELETE_ROLE_BY_ID; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -15,23 +17,7 @@ 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 = false, - deleted_by = ?#{principal?.id} - WHERE id = ?1 - """) + @Query(DELETE_ROLE_BY_ID) void deleteById(Long id); @Override 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/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/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 d1abb0f0..00000000 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/FindUserService.java +++ /dev/null @@ -1,92 +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 = """ - with user_roles as ( - select - ur.user_id, string_agg(r.initials, ',') 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, - urs.roles - from - "user" u - left join user_roles as urs on urs.user_id = u.id - where u.deleted_at is null - """; - - var query = manager.createNativeQuery(sql, Tuple.class); - var count = ((BigInteger) manager.createNativeQuery(""" - select - count(id) - from - "user" - where 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 f4a3e220..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("Permission invalidates resource update")); - - var actual = repository - .findOptionalByIdAndDeletedAtIsNullFetchRoles(id) - .orElseThrow(() -> notFound("User not found")); - - 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/CreateUserApp.java b/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApp.java deleted file mode 100644 index 0e6ccef1..00000000 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApp.java +++ /dev/null @@ -1,43 +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 javax.validation.constraints.Email; -import javax.validation.constraints.NotEmpty; -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/session/entity/RefreshToken.java b/src/main/java/com/github/throyer/common/springboot/domain/session/entity/RefreshToken.java new file mode 100644 index 00000000..5195e083 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/entity/RefreshToken.java @@ -0,0 +1,57 @@ +package com.github.throyer.common.springboot.domain.session.entity; + +import com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import static java.time.LocalDateTime.now; +import java.util.Optional; +import static java.util.UUID.randomUUID; +import static javax.persistence.FetchType.EAGER; +import static javax.persistence.GenerationType.IDENTITY; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@NoArgsConstructor +public class RefreshToken implements Serializable { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String code; + + private LocalDateTime expiresIn; + + private Boolean available = true; + + @ManyToOne(fetch = EAGER) + @JoinColumn(name = "user_id") + private User user; + + public RefreshToken(User user, Integer daysToExpire) { + this.user = user; + this.expiresIn = now().plusDays(daysToExpire); + this.code = randomUUID().toString(); + } + + public RefreshToken(UserDetails user, Integer daysToExpire) { + this.expiresIn = now().plusDays(daysToExpire); + this.code = randomUUID().toString(); + this.user = Optional.ofNullable(user.getId()).map(userId -> new User(userId)).orElse(null); + } + + public Boolean nonExpired() { + return expiresIn.isAfter(now()); + } +} 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/session/model/Authorized.java similarity index 78% rename from src/main/java/com/github/throyer/common/springboot/domain/models/security/Authorized.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/model/Authorized.java index 15e58ff6..5fa8ccc7 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/security/Authorized.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/model/Authorized.java @@ -1,8 +1,8 @@ -package com.github.throyer.common.springboot.domain.models.security; +package com.github.throyer.common.springboot.domain.session.model; import java.util.List; -import com.github.throyer.common.springboot.domain.models.entity.Role; +import com.github.throyer.common.springboot.domain.role.entity.Role; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.User; @@ -20,7 +20,7 @@ public Authorized(Long id, List authorities) { this.name = ""; } - public Authorized(com.github.throyer.common.springboot.domain.models.entity.User user) { + public Authorized(com.github.throyer.common.springboot.domain.user.entity.User user) { super( user.getEmail(), user.getPassword(), @@ -52,16 +52,7 @@ public Boolean isAdmin() { .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) { + public Boolean itsMeOrSessionIsADM(Long id) { var admin = isAdmin(); var equals = getId().equals(id); if (admin) { 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/session/model/RefreshTokenRequest.java similarity index 84% rename from src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenRequest.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/model/RefreshTokenRequest.java index 45b9efe4..54c7b8e3 100644 --- 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/session/model/RefreshTokenRequest.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.security.dto; +package com.github.throyer.common.springboot.domain.session.model; import javax.validation.constraints.NotEmpty; 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/session/model/RefreshTokenResponse.java similarity index 87% rename from src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/RefreshTokenResponse.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/model/RefreshTokenResponse.java index ec2e0a75..59f9f655 100644 --- 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/session/model/RefreshTokenResponse.java @@ -1,11 +1,11 @@ -package com.github.throyer.common.springboot.domain.services.security.dto; +package com.github.throyer.common.springboot.domain.session.model; 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; +import com.github.throyer.common.springboot.domain.session.entity.RefreshToken; public class RefreshTokenResponse { private final String token; 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/session/model/TokenRequest.java similarity index 93% rename from src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenRequest.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/model/TokenRequest.java index b4f43174..fdf3590a 100644 --- 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/session/model/TokenRequest.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.services.security.dto; +package com.github.throyer.common.springboot.domain.session.model; import java.util.Objects; import javax.validation.constraints.NotEmpty; 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/session/model/TokenResponse.java similarity index 66% rename from src/main/java/com/github/throyer/common/springboot/domain/services/security/dto/TokenResponse.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/model/TokenResponse.java index f65f179d..ec8a3da4 100644 --- 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/session/model/TokenResponse.java @@ -1,21 +1,22 @@ -package com.github.throyer.common.springboot.domain.services.security.dto; +package com.github.throyer.common.springboot.domain.session.model; -import java.time.LocalDateTime; +import com.github.throyer.common.springboot.domain.session.entity.RefreshToken; +import com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; 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; +import java.time.LocalDateTime; + +public class TokenResponse { -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, @@ -27,6 +28,18 @@ public TokenResponse( this.refreshToken = refreshToken; this.expiresIn = expiresIn; } + + public TokenResponse( + UserDetails user, + String token, + RefreshToken refreshToken, + LocalDateTime expiresIn + ) { + this.user = user; + this.token = token; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + } public UserDetails getUser() { return user; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/session/repository/Queries.java b/src/main/java/com/github/throyer/common/springboot/domain/session/repository/Queries.java new file mode 100644 index 00000000..99cdbb03 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/repository/Queries.java @@ -0,0 +1,23 @@ +package com.github.throyer.common.springboot.domain.session.repository; + +import org.springframework.stereotype.Service; + +@Service +public class Queries { + + public static final String DISABLE_OLD_REFRESH_TOKENS_FROM_USER = """ + UPDATE + RefreshToken + SET + available = false + WHERE + user_id = ?1 AND available = true + """; + + public static final String FIND_REFRESH_TOKEN_BY_CODE_FETCH_USER_AND_ROLES = """ + SELECT refresh FROM RefreshToken refresh + JOIN FETCH refresh.user user + JOIN FETCH user.roles + WHERE refresh.code = ?1 AND refresh.available = true + """; +} 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/session/repository/RefreshTokenRepository.java similarity index 56% rename from src/main/java/com/github/throyer/common/springboot/domain/repositories/RefreshTokenRepository.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/repository/RefreshTokenRepository.java index 7023ce93..0539ac2c 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/repositories/RefreshTokenRepository.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/repository/RefreshTokenRepository.java @@ -1,10 +1,12 @@ -package com.github.throyer.common.springboot.domain.repositories; +package com.github.throyer.common.springboot.domain.session.repository; import java.util.Optional; import javax.transaction.Transactional; -import com.github.throyer.common.springboot.domain.models.entity.RefreshToken; +import com.github.throyer.common.springboot.domain.session.entity.RefreshToken; +import static com.github.throyer.common.springboot.domain.session.repository.Queries.DISABLE_OLD_REFRESH_TOKENS_FROM_USER; +import static com.github.throyer.common.springboot.domain.session.repository.Queries.FIND_REFRESH_TOKEN_BY_CODE_FETCH_USER_AND_ROLES; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -16,21 +18,9 @@ public interface RefreshTokenRepository extends JpaRepository, JpaSpecificationExecutor { @Transactional @Modifying - @Query(""" - UPDATE - RefreshToken - SET - available = false - WHERE - user_id = ?1 AND available = true - """) + @Query(DISABLE_OLD_REFRESH_TOKENS_FROM_USER) 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 - """) + @Query(FIND_REFRESH_TOKEN_BY_CODE_FETCH_USER_AND_ROLES) public Optional findOptionalByCodeAndAvailableIsTrue(String code); } diff --git a/src/main/java/com/github/throyer/common/springboot/domain/session/service/CreateTokenService.java b/src/main/java/com/github/throyer/common/springboot/domain/session/service/CreateTokenService.java new file mode 100644 index 00000000..e87174d0 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/service/CreateTokenService.java @@ -0,0 +1,64 @@ +package com.github.throyer.common.springboot.domain.session.service; + + +import com.github.throyer.common.springboot.domain.user.model.UserDetails; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.session.entity.RefreshToken; +import com.github.throyer.common.springboot.domain.session.repository.RefreshTokenRepository; +import com.github.throyer.common.springboot.domain.session.model.TokenRequest; +import com.github.throyer.common.springboot.domain.session.model.TokenResponse; +import static com.github.throyer.common.springboot.utils.Constants.SECURITY.CREATE_SESSION_ERROR_MESSAGE; + +import static com.github.throyer.common.springboot.utils.Constants.SECURITY.JWT; +import static com.github.throyer.common.springboot.utils.Responses.forbidden; + +import java.time.LocalDateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class CreateTokenService { + + @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 RefreshTokenRepository refreshTokenRepository; + @Autowired + private UserRepository userRepository; + + public TokenResponse create(TokenRequest request) { + var user = userRepository.findOptionalByEmailFetchRoles(request.getEmail()) + .filter(session -> session.validatePassword(request.getPassword())) + .orElseThrow(() -> forbidden(CREATE_SESSION_ERROR_MESSAGE)); + return create(new UserDetails(user)); + } + + public TokenResponse create(UserDetails user) { + + 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); + + return new TokenResponse( + user, + token, + refresh, + expiresIn + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/throyer/common/springboot/domain/session/service/JsonWebToken.java b/src/main/java/com/github/throyer/common/springboot/domain/session/service/JsonWebToken.java new file mode 100644 index 00000000..55953b95 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/service/JsonWebToken.java @@ -0,0 +1,64 @@ +package com.github.throyer.common.springboot.domain.session.service; + + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import com.github.throyer.common.springboot.domain.role.entity.Role; +import com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; +import com.github.throyer.common.springboot.domain.session.model.Authorized; + +import io.jsonwebtoken.Jwts; +import static io.jsonwebtoken.SignatureAlgorithm.HS256; +import static java.util.Arrays.stream; + + +public class JsonWebToken { + + public static final String ROLES_KEY_ON_JWT = "roles"; + + public String encode(User user, LocalDateTime expiration, String secret) { + var roles = user.getRoles().stream().map(role -> role.getAuthority()).toList(); + return encode(user.getId(), roles, expiration, secret); + } + + public String encode(UserDetails 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() + .collect(Collectors.joining(","))) + .setExpiration(Date.from(expiration + .atZone(ZoneId.systemDefault()) + .toInstant())) + .signWith(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 joinedRolesString = decoded.getBody().get(ROLES_KEY_ON_JWT).toString(); + var roles = joinedRolesString.split(","); + var authorities = stream(roles).map(Role::new).toList(); + + return new Authorized(id, authorities); + } +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/session/service/RefreshTokenService.java b/src/main/java/com/github/throyer/common/springboot/domain/session/service/RefreshTokenService.java new file mode 100644 index 00000000..bb3b1c3c --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/service/RefreshTokenService.java @@ -0,0 +1,50 @@ +package com.github.throyer.common.springboot.domain.session.service; + +import com.github.throyer.common.springboot.domain.session.entity.RefreshToken; +import com.github.throyer.common.springboot.domain.session.model.RefreshTokenRequest; +import com.github.throyer.common.springboot.domain.session.model.RefreshTokenResponse; +import com.github.throyer.common.springboot.domain.session.repository.RefreshTokenRepository; +import static com.github.throyer.common.springboot.utils.Constants.SECURITY.JWT; +import static com.github.throyer.common.springboot.utils.Responses.forbidden; +import java.time.LocalDateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class RefreshTokenService { + + public static final String REFRESH_SESSION_ERROR_MESSAGE = "Refresh token expirado ou inválido."; + + @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 RefreshTokenRepository refreshTokenRepository; + + public RefreshTokenResponse 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)); + + return new RefreshTokenResponse( + token, + refresh, + expiresIn + ); + } +} \ No newline at end of file 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/session/service/SessionService.java similarity index 67% rename from src/main/java/com/github/throyer/common/springboot/domain/services/security/SecurityService.java rename to src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java index 2d9a7ba8..7627149a 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/services/security/SecurityService.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java @@ -1,18 +1,14 @@ -package com.github.throyer.common.springboot.domain.services.security; +package com.github.throyer.common.springboot.domain.session.service; -import static com.github.throyer.common.springboot.utils.Constants.SECURITY.INVALID_USERNAME; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.session.model.Authorized; import static com.github.throyer.common.springboot.utils.Constants.SECURITY.JWT; import static java.util.Objects.nonNull; +import java.util.Optional; 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 java.util.logging.Level; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.context.SecurityContextHolder; @@ -22,40 +18,43 @@ import org.springframework.stereotype.Service; @Service -public class SecurityService implements UserDetailsService { - - private static final Logger LOGGER = LoggerFactory.getLogger(SecurityService.class); +public class SessionService implements UserDetailsService { @Autowired UserRepository repository; + private static final Logger LOGGER = Logger.getLogger(SessionService.class.getName()); + + public static final String INVALID_USERNAME = "Invalid user name."; private static String SECRET; - public SecurityService(@Value("${token.secret}") String secret) { - SecurityService.SECRET = secret; + public SessionService(@Value("${token.secret}") String secret) { + SessionService.SECRET = secret; } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return new Authorized(repository.findOptionalByEmailFetchRoles(email) - .orElseThrow(() -> new UsernameNotFoundException(INVALID_USERNAME))); + var user = repository.findOptionalByEmailFetchRoles(email) + .orElseThrow(() -> new UsernameNotFoundException(INVALID_USERNAME)); + + return new Authorized(user); } public static void authorize(String token) { try { var authorized = JWT.decode(token, SECRET); SecurityContextHolder - .getContext() + .getContext() .setAuthentication(authorized.getAuthentication()); } catch (Exception exception) { - LOGGER.error("Token expired or invalid"); + LOGGER.log(Level.WARNING, "Token expired or invalid"); } } public static Optional authorized() { try { var principal = getPrincipal(); - + if (nonNull(principal) && principal instanceof Authorized authorized) { return of(authorized); } @@ -63,15 +62,15 @@ public static Optional authorized() { } catch (Exception exception) { return empty(); } - + } private static Object getPrincipal() { var authentication = SecurityContextHolder.getContext() - .getAuthentication(); + .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/validation/SimpleError.java b/src/main/java/com/github/throyer/common/springboot/domain/shared/SimpleError.java similarity index 95% rename from src/main/java/com/github/throyer/common/springboot/domain/validation/SimpleError.java rename to src/main/java/com/github/throyer/common/springboot/domain/shared/SimpleError.java index baf705d9..9ef4f98c 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/validation/SimpleError.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/shared/SimpleError.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.validation; +package com.github.throyer.common.springboot.domain.shared; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java b/src/main/java/com/github/throyer/common/springboot/domain/shared/Toast.java similarity index 58% rename from src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java rename to src/main/java/com/github/throyer/common/springboot/domain/shared/Toast.java index e8e42bc3..2794c4ff 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Toast.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/shared/Toast.java @@ -1,6 +1,7 @@ -package com.github.throyer.common.springboot.domain.models.shared; +package com.github.throyer.common.springboot.domain.shared; 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/domain/models/shared/Type.java b/src/main/java/com/github/throyer/common/springboot/domain/shared/Type.java similarity index 84% rename from src/main/java/com/github/throyer/common/springboot/domain/models/shared/Type.java rename to src/main/java/com/github/throyer/common/springboot/domain/shared/Type.java index 9e11b23a..3108adb3 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/shared/Type.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/shared/Type.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.models.shared; +package com.github.throyer.common.springboot.domain.shared; public enum Type { 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/user/entity/User.java similarity index 74% rename from src/main/java/com/github/throyer/common/springboot/domain/models/entity/User.java rename to src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java index 207d4c7e..936403bd 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/models/entity/User.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot.domain.models.entity; +package com.github.throyer.common.springboot.domain.user.entity; import java.io.Serializable; import java.security.Principal; @@ -21,9 +21,16 @@ 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 com.github.throyer.common.springboot.domain.user.model.CreateUserProps; + +import com.github.throyer.common.springboot.domain.session.model.Authorized; +import com.github.throyer.common.springboot.domain.user.model.UpdateUserProps; +import com.github.throyer.common.springboot.domain.management.entity.Auditable; +import com.github.throyer.common.springboot.domain.role.entity.Role; +import com.github.throyer.common.springboot.domain.management.model.Addressable; +import static com.github.throyer.common.springboot.domain.management.repository.Queries.NON_DELETED_CLAUSE; +import static com.github.throyer.common.springboot.utils.Constants.SECURITY.PASSWORD_ENCODER; + import lombok.Data; import org.hibernate.annotations.Where; @@ -32,8 +39,8 @@ @Data @Entity @Table(name = "user") -@Where(clause = Auditable.NON_DELETED_CLAUSE) -public class User extends Auditable implements Serializable, HasEmail { +@Where(clause = NON_DELETED_CLAUSE) +public class User extends Auditable implements Serializable, Addressable { public static final Integer PASSWORD_STRENGTH = 10; @@ -78,6 +85,13 @@ public User(String name, String email, String password, List roles) { this.roles = roles; } + public User(CreateUserProps props, List roles) { + this.name = props.getName(); + this.email = props.getEmail(); + this.password = props.getPassword(); + this.roles = roles; + } + public List getRoles() { return roles; } @@ -96,7 +110,7 @@ public String getEmail() { return email; } - public void merge(UpdateUser dto) { + public void merge(UpdateUserProps dto) { setName(dto.getName()); setEmail(dto.getEmail()); } @@ -143,13 +157,12 @@ public boolean isPrincipal(Authorized authorized) { } public Boolean validatePassword(String password) { - var encoder = new BCryptPasswordEncoder(PASSWORD_STRENGTH); - return encoder.matches(password, this.password); + return PASSWORD_ENCODER.matches(password, this.password); } @PrePersist private void created() { - this.password = new BCryptPasswordEncoder(PASSWORD_STRENGTH).encode(password); + this.password = PASSWORD_ENCODER.encode(password); } @Override 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/user/model/CreateUserProps.java similarity index 51% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/CreateUserApi.java rename to src/main/java/com/github/throyer/common/springboot/domain/user/model/CreateUserProps.java index 593c5fc4..d4421931 100644 --- 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/user/model/CreateUserProps.java @@ -1,19 +1,21 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.user.model; -import static com.github.throyer.common.springboot.domain.validation.EmailValidations.validateEmailUniqueness; +import static com.github.throyer.common.springboot.domain.mail.exceptions.EmailValidations.validateEmailUniqueness; + +import com.github.throyer.common.springboot.domain.management.model.Addressable; +import com.github.throyer.common.springboot.domain.user.entity.User; +import java.util.List; 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 javax.validation.constraints.Size; + import lombok.Data; +import lombok.NoArgsConstructor; @Data -public class CreateUserApi implements HasEmail { +@NoArgsConstructor +public class CreateUserProps implements Addressable { @NotEmpty(message = "email is a required field.") private String name; @@ -26,7 +28,7 @@ public class CreateUserApi implements HasEmail { @Size(min = 8, max = 155, message = "The password must contain at least {min} characters.") private String password; - public CreateUserApi(String name, String email, String password) { + public CreateUserProps(String name, String email, String password) { setName(name); setEmail(email); setPassword(password); @@ -37,9 +39,6 @@ public void validate() { } public User user() { - return new UserBuilder(name) - .setEmail(email) - .setPassword(password) - .build(); + return new User(name, email, password, List.of()); } } 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/user/model/UpdateUserProps.java similarity index 78% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UpdateUser.java rename to src/main/java/com/github/throyer/common/springboot/domain/user/model/UpdateUserProps.java index bf59107e..84285f43 100644 --- 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/user/model/UpdateUserProps.java @@ -1,12 +1,12 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.user.model; 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; +import com.github.throyer.common.springboot.domain.management.model.Addressable; -public class UpdateUser implements HasEmail { +public class UpdateUserProps implements Addressable { @NotNull(message = "O nome não pode ser NULL.") @NotEmpty(message = "Por favor, forneça um nome.") 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/user/model/UserDetails.java similarity index 86% rename from src/main/java/com/github/throyer/common/springboot/domain/services/user/dto/UserDetails.java rename to src/main/java/com/github/throyer/common/springboot/domain/user/model/UserDetails.java index d03e76f5..cae53563 100644 --- 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/user/model/UserDetails.java @@ -1,10 +1,10 @@ -package com.github.throyer.common.springboot.domain.services.user.dto; +package com.github.throyer.common.springboot.domain.user.model; 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 com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.management.model.Entity; import static java.util.Optional.ofNullable; public class UserDetails implements Entity { diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/repository/Queries.java b/src/main/java/com/github/throyer/common/springboot/domain/user/repository/Queries.java new file mode 100644 index 00000000..3d6cf27f --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/repository/Queries.java @@ -0,0 +1,76 @@ +package com.github.throyer.common.springboot.domain.user.repository; + +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 FIND_USERNAME_BY_ID = """ + SELECT user.name FROM #{#entityName} user + WHERE user.id = ?1 + """; + + public static final String FIND_USER_BY_ID_FETCH_ROLES = """ + SELECT user FROM #{#entityName} user + LEFT JOIN FETCH user.roles + WHERE user.id = ?1 + """; + + public static final String FIND_USER_BY_EMAIL_FETCH_ROLES = """ + SELECT user FROM #{#entityName} user + LEFT JOIN FETCH user.roles + WHERE user.email = ?1 + """; + + public static final String FIND_ALL_USER_DETAILS_WITHOUT_ROLES = """ + SELECT + new com.github.throyer.common.springboot.domain.user.model.UserDetails( + user.id, + user.name, + user.email + ) + FROM #{#entityName} user + """; + + public static final String COUNT_ENABLED_USERS = """ + select + count(id) + from + "user" + where deleted_at is null + """; + + public static final String FIND_ALL_USER_DETAILS_FETCH_ROLES = """ + with user_roles as ( + select + ur.user_id, string_agg(r.initials, ',') 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, + 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 + """; +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/repository/UserRepository.java b/src/main/java/com/github/throyer/common/springboot/domain/user/repository/UserRepository.java new file mode 100644 index 00000000..e675b5f5 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/repository/UserRepository.java @@ -0,0 +1,61 @@ +package com.github.throyer.common.springboot.domain.user.repository; + +import java.util.Optional; + +import com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.DELETE_USER_BY_ID; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.FIND_ALL_USER_DETAILS_WITHOUT_ROLES; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.FIND_USERNAME_BY_ID; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.FIND_USER_BY_ID_FETCH_ROLES; +import com.github.throyer.common.springboot.domain.management.repository.SoftDeleteRepository; + +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; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.FIND_USER_BY_EMAIL_FETCH_ROLES; + +@Repository +public interface UserRepository 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 entities) { + entities.forEach(entity -> deleteById(entity.getId())); + } + + public Page findDistinctBy(Pageable pageable); + + public Boolean existsByEmail(String email); + + public Optional findOptionalByIdAndDeletedAtIsNull(Long id); + + @Query(FIND_USERNAME_BY_ID) + public Optional findNameById(Long id); + + @Query(FIND_USER_BY_ID_FETCH_ROLES) + public Optional findOptionalByIdFetchRoles(Long id); + + @Query(FIND_USER_BY_EMAIL_FETCH_ROLES) + public Optional findOptionalByEmailFetchRoles(String email); + + public Optional findOptionalByEmail(String email); + + @Query(FIND_ALL_USER_DETAILS_WITHOUT_ROLES) + public Page finAllUserDetailsWithoutRoles(Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/service/CreateUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/user/service/CreateUserService.java new file mode 100644 index 00000000..88f01673 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/service/CreateUserService.java @@ -0,0 +1,35 @@ +package com.github.throyer.common.springboot.domain.user.service; + +import com.github.throyer.common.springboot.domain.role.repository.RoleRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.user.model.CreateUserProps; +import com.github.throyer.common.springboot.domain.user.entity.User; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CreateUserService { + + @Autowired + UserRepository userRepository; + + @Autowired + RoleRepository roleRepository; + + public UserDetails create(CreateUserProps data) { + + data.validate(); + + var roles = roleRepository.findOptionalByInitials("USER") + .map(role -> List.of(role)) + .orElseGet(() -> List.of()); + + var user = userRepository.save(new User(data, roles)); + + return new UserDetails(user); + } +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserByIdService.java b/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserByIdService.java new file mode 100644 index 00000000..e256c612 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserByIdService.java @@ -0,0 +1,30 @@ +package com.github.throyer.common.springboot.domain.user.service; + +import static com.github.throyer.common.springboot.domain.session.service.SessionService.authorized; +import static com.github.throyer.common.springboot.utils.Responses.notFound; +import static com.github.throyer.common.springboot.utils.Responses.unauthorized; + +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class FindUserByIdService { + + @Autowired + UserRepository repository; + + public UserDetails find(Long id) { + authorized() + .filter(authorized -> authorized.itsMeOrSessionIsADM(id)) + .orElseThrow(() -> unauthorized("Invalid permission to read resource")); + + var user = repository + .findOptionalByIdFetchRoles(id) + .orElseThrow(() -> notFound("User not found")); + + return new UserDetails(user); + } +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserService.java new file mode 100644 index 00000000..9f216c09 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/service/FindUserService.java @@ -0,0 +1,60 @@ +package com.github.throyer.common.springboot.domain.user.service; + +import com.github.throyer.common.springboot.domain.pagination.model.Page; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.COUNT_ENABLED_USERS; +import static com.github.throyer.common.springboot.domain.user.repository.Queries.FIND_ALL_USER_DETAILS_FETCH_ROLES; +import com.github.throyer.common.springboot.domain.pagination.service.Pagination; + +import org.springframework.beans.factory.annotation.Autowired; +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 query = manager + .createNativeQuery(FIND_ALL_USER_DETAILS_FETCH_ROLES, Tuple.class); + + var count = ((BigInteger) manager + .createNativeQuery(COUNT_ENABLED_USERS) + .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); + } +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/service/RemoveUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/user/service/RemoveUserService.java new file mode 100644 index 00000000..dde36110 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/service/RemoveUserService.java @@ -0,0 +1,28 @@ +package com.github.throyer.common.springboot.domain.user.service; + +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import static com.github.throyer.common.springboot.domain.session.service.SessionService.authorized; +import static com.github.throyer.common.springboot.utils.Responses.notFound; +import static com.github.throyer.common.springboot.utils.Responses.unauthorized; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class RemoveUserService { + + @Autowired + UserRepository repository; + + public void remove(Long id) { + authorized() + .filter(authorized -> authorized.itsMeOrSessionIsADM(id)) + .orElseThrow(() -> unauthorized("Invalid permission to remove resource")); + + var user = repository + .findOptionalByIdFetchRoles(id) + .orElseThrow(() -> notFound("User not found")); + + repository.delete(user); + } +} diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/service/UpdateUserService.java b/src/main/java/com/github/throyer/common/springboot/domain/user/service/UpdateUserService.java new file mode 100644 index 00000000..da7d5e74 --- /dev/null +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/service/UpdateUserService.java @@ -0,0 +1,35 @@ +package com.github.throyer.common.springboot.domain.user.service; + +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; +import static com.github.throyer.common.springboot.domain.session.service.SessionService.authorized; +import com.github.throyer.common.springboot.domain.user.model.UpdateUserProps; +import com.github.throyer.common.springboot.domain.user.model.UserDetails; +import static com.github.throyer.common.springboot.domain.mail.exceptions.EmailValidations.validateEmailUniquenessOnModify; +import static com.github.throyer.common.springboot.utils.Responses.notFound; +import static com.github.throyer.common.springboot.utils.Responses.unauthorized; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UpdateUserService { + + @Autowired + UserRepository repository; + + public UserDetails update(Long id, UpdateUserProps body) { + + authorized() + .filter(authorized -> authorized.itsMeOrSessionIsADM(id)) + .orElseThrow(() -> unauthorized("Permission invalidates resource update")); + + var actual = repository + .findOptionalByIdFetchRoles(id) + .orElseThrow(() -> notFound("User not found")); + + validateEmailUniquenessOnModify(body, actual); + + actual.merge(body); + + return new UserDetails(repository.save(actual)); + } +} 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/errors/ValidationHandlers.java b/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java index 42f2cea6..a7ebfc5c 100644 --- a/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java +++ b/src/main/java/com/github/throyer/common/springboot/errors/ValidationHandlers.java @@ -1,17 +1,18 @@ package com.github.throyer.common.springboot.errors; +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 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 com.github.throyer.common.springboot.domain.mail.exceptions.EmailNotUniqueException; +import com.github.throyer.common.springboot.domain.shared.SimpleError; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; @@ -53,12 +54,6 @@ public ResponseEntity status(ResponseStatusException exception) { .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) { 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 index 3cd1b1dd..48e62c1c 100644 --- a/src/main/java/com/github/throyer/common/springboot/middlewares/AuthorizationMiddleware.java +++ b/src/main/java/com/github/throyer/common/springboot/middlewares/AuthorizationMiddleware.java @@ -1,6 +1,6 @@ package com.github.throyer.common.springboot.middlewares; -import static com.github.throyer.common.springboot.utils.TokenUtils.authorization; +import com.github.throyer.common.springboot.domain.session.service.SessionService; import static java.util.Optional.ofNullable; import java.io.IOException; @@ -10,14 +10,19 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import com.github.throyer.common.springboot.domain.services.security.SecurityService; +import static java.util.Objects.isNull; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @Component +@Order(1) public class AuthorizationMiddleware extends OncePerRequestFilter { + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String ACCEPTABLE_TOKEN_TYPE = "Bearer "; + @Override protected void doFilterInternal( HttpServletRequest request, @@ -27,8 +32,26 @@ protected void doFilterInternal( throws ServletException, IOException { ofNullable(authorization(request)) - .ifPresent(SecurityService::authorize); + .ifPresent(SessionService::authorize); filter.doFilter(request, response); } + + private static String authorization(HttpServletRequest request) { + var token = request.getHeader(AUTHORIZATION_HEADER); + + if (tokenIsNull(token)) { + return null; + } + + if (token.substring(7).equals("Bearer")) { + return null; + } + + return token.substring(7, token.length()); + } + + private static boolean tokenIsNull(String token) { + return isNull(token) || token.isEmpty() || !token.startsWith(ACCEPTABLE_TOKEN_TYPE); + } } \ 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 index 201ff98f..fc7d8e8d 100644 --- a/src/main/java/com/github/throyer/common/springboot/utils/Constants.java +++ b/src/main/java/com/github/throyer/common/springboot/utils/Constants.java @@ -1,12 +1,16 @@ package com.github.throyer.common.springboot.utils; -import com.github.throyer.common.springboot.domain.services.security.JsonWebToken; +import com.github.throyer.common.springboot.domain.session.service.JsonWebToken; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 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 Integer PASSWORD_STRENGTH = 10; + public static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(PASSWORD_STRENGTH); public static final String ROLES_KEY_ON_JWT = "roles"; public static final String INVALID_USERNAME = "Invalid username."; 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 index d0d4c470..f4188c8d 100644 --- a/src/main/java/com/github/throyer/common/springboot/utils/Random.java +++ b/src/main/java/com/github/throyer/common/springboot/utils/Random.java @@ -1,13 +1,11 @@ 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; +import com.github.throyer.common.springboot.domain.role.entity.Role; +import com.github.throyer.common.springboot.domain.user.entity.User; public class Random { @@ -35,12 +33,11 @@ public static User randomUser() { } 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(); + return new User( + FAKER.name().fullName(), + FAKER.internet().safeEmailAddress(), + password(), + roles + ); } } 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 index a07aae71..bda86060 100644 --- a/src/main/java/com/github/throyer/common/springboot/utils/Responses.java +++ b/src/main/java/com/github/throyer/common/springboot/utils/Responses.java @@ -2,11 +2,13 @@ import java.net.URI; -import com.github.throyer.common.springboot.domain.models.shared.Entity; +import com.github.throyer.common.springboot.domain.management.model.Entity; 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; /** @@ -101,4 +103,13 @@ public static final ResponseStatusException notFound(String reason) { public static final ResponseStatusException InternalServerError(String reason) { return new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, reason); } + + public static final

Boolean validate(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/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java b/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java index e6d2a6f7..12f48097 100644 --- a/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java +++ b/src/main/java/com/github/throyer/common/springboot/utils/Toasts.java @@ -1,11 +1,10 @@ package com.github.throyer.common.springboot.utils; -import com.github.throyer.common.springboot.domain.models.shared.Toast; +import com.github.throyer.common.springboot.domain.shared.Toast; import java.util.List; -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 static com.github.throyer.common.springboot.domain.shared.Type.*; +import com.github.throyer.common.springboot.domain.shared.Type; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -54,7 +53,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/utils/TokenUtils.java b/src/main/java/com/github/throyer/common/springboot/utils/TokenUtils.java index 7326ae62..e7923ad1 100644 --- a/src/main/java/com/github/throyer/common/springboot/utils/TokenUtils.java +++ b/src/main/java/com/github/throyer/common/springboot/utils/TokenUtils.java @@ -4,47 +4,14 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Objects; - -import javax.servlet.http.HttpServletRequest; - -import com.github.throyer.common.springboot.domain.models.entity.Role; public class TokenUtils { - private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String ACCEPTABLE_TOKEN_TYPE = "Bearer "; - - /** - * Recupera o token de dentro do cabeçalho da request. - * - * @param request {request que chegou na API} - * @return null se o token estiver vazio ou for do tipo errado. - */ - public static String authorization(HttpServletRequest request) { - var token = request.getHeader(AUTHORIZATION_HEADER); - - if (tokenIsNull(token)) - return null; - - return token.substring(7, token.length()); - } - - /** - * Verifica se o token esta vazio ou não é do tipo 'Bearer'. - * - * @param token - * @return true se o token for vazio ou não for bearer. - */ - private static boolean tokenIsNull(String token) { - return Objects.isNull(token) || token.isEmpty() || !token.startsWith(ACCEPTABLE_TOKEN_TYPE); - } - public static String token(Integer expiration, String secret) { - return token(expiration, secret, List.of(new Role("ADM"))); + return token(expiration, secret, List.of("ADM")); } - public static String token(Integer expiration, String secret, List roles) { + public static String token(Integer expiration, String secret, List roles) { var token = JWT.encode(1L, roles, LocalDateTime.now().plusHours(expiration), secret); return String.format("Bearer %s", token); } diff --git a/src/test/java/com/github/throyer/common/springboot/controllers/SessionsTests.java b/src/test/java/com/github/throyer/common/springboot/controllers/SessionsTests.java index d783dce9..b2ea85ec 100644 --- a/src/test/java/com/github/throyer/common/springboot/controllers/SessionsTests.java +++ b/src/test/java/com/github/throyer/common/springboot/controllers/SessionsTests.java @@ -9,7 +9,7 @@ import java.util.Map; -import com.github.throyer.common.springboot.domain.repositories.UserRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/github/throyer/common/springboot/controllers/UsersTests.java b/src/test/java/com/github/throyer/common/springboot/controllers/UsersTests.java index 8efb19be..6d5de143 100644 --- a/src/test/java/com/github/throyer/common/springboot/controllers/UsersTests.java +++ b/src/test/java/com/github/throyer/common/springboot/controllers/UsersTests.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -import com.github.throyer.common.springboot.domain.repositories.UserRepository; +import com.github.throyer.common.springboot.domain.user.repository.UserRepository; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName;