User.java
package com.albertsilva.dev.asjcatalog.domain.user;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.albertsilva.dev.asjcatalog.domain.recovery.Token;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
/**
* Representa um usuário do sistema.
*
* <p>
* Esta entidade armazena informações de conta e credenciais do usuário,
* além das roles atribuídas para controle de acesso.
* </p>
*
* <p>
* A classe também implementa {@link UserDetails}: esta entidade de domínio é
* usada diretamente pelo Spring Security como representação do usuário
* autenticável.
* </p>
* <ul>
* <li>{@link #getUsername()} devolve o e-mail</li>
* <li>{@link #getAuthorities()} devolve o próprio conjunto de roles</li>
* <li>{@link #isEnabled()} devolve o indicador {@code active}</li>
* <li>{@link #isAccountNonExpired()}, {@link #isAccountNonLocked()} e
* {@link #isCredentialsNonExpired()} retornam sempre {@code true}: o modelo
* atual não possui expiração ou bloqueio de conta nem expiração de
* credenciais</li>
* </ul>
*
* <p>
* <b>Regras e invariantes:</b>
* </p>
* <ul>
* <li>O e-mail é obrigatório e único (mapeamento) e funciona como nome de
* usuário; o formato e a unicidade "de negócio" são verificados fora da
* entidade (DTOs e validators)</li>
* <li>Nome, sobrenome e senha não possuem restrições no mapeamento</li>
* <li>A entidade não codifica a senha: o valor informado em
* {@link #setPassword(String)} é guardado como está, e espera-se que seja o
* hash já calculado por quem chama</li>
* <li>Uma instância nova nasce inativa ({@code active = false}). Contas
* inativas continuam com suas roles, mas {@link #isEnabled()} devolve
* {@code false}, e o fluxo de login customizado do sistema rejeita contas
* nessa situação</li>
* <li>As roles são um conjunto: adicionar a mesma role duas vezes não duplica o
* vínculo</li>
* <li>A identidade é definida somente pelo {@code id} (ver
* {@link #equals(Object)})</li>
* </ul>
*
* <p>
* <b>Mapeamento:</b>
* </p>
* <ul>
* <li>Tabela: {@code tb_user}</li>
* <li>Muitos-para-muitos com {@link Role} (lado dono, tabela de junção
* {@code tb_user_role})</li>
* <li>Um-para-muitos com {@link Token} (lado inverso; {@code cascade = ALL} e
* {@code orphanRemoval = true})</li>
* </ul>
*/
@Entity
@Table(name = "tb_user")
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
/** Identificador único do usuário. Gerado pelo banco de dados. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** Primeiro nome do usuário, para exibição e saudação. */
private String firstName;
/** Sobrenome do usuário. */
private String lastName;
/**
* Email do usuário.
*
* <p>
* Campo obrigatório e único, usado como identificador de conta.
* </p>
*/
@Column(nullable = false, unique = true)
private String email;
/** Senha da conta, esperada já codificada (hash); a entidade não a codifica. */
private String password;
/**
* Conjunto de roles/autoridades atribuídas ao usuário.
*
* <p>
* Lado dono do relacionamento (tabela {@code tb_user_role}), sem
* {@code cascade}: as roles precisam já existir.
* </p>
*/
@ManyToMany
@JoinTable(name = "tb_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
/**
* Tokens de ativação de conta e de recuperação de senha emitidos para este
* usuário.
*
* <p>
* Lado inverso do relacionamento (o lado dono é {@link Token#getUser()}). Com
* {@code cascade = ALL} e {@code orphanRemoval = true}, as operações de
* persistência aplicadas ao usuário se propagam aos tokens (inclusive a
* remoção do usuário, que remove seus tokens), e um token retirado deste
* conjunto é excluído. A classe não expõe getter para este campo.
* </p>
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Token> tokens = new HashSet<>();
/**
* Indica se a conta do usuário está ativa.
*
* <p>
* É o valor devolvido por {@link #isEnabled()}. Vale {@code false} em uma
* instância recém-criada. Pode ser alterado por {@link #activate()},
* {@link #deactivate()} ou {@link #setActive(boolean)}.
* </p>
*/
private boolean active;
/**
* Cria um usuário vazio: campos nulos, {@code active} igual a {@code false},
* sem roles e sem tokens. Construtor sem argumentos exigido pela JPA.
*/
public User() {
}
/**
* Cria um usuário com os dados informados.
*
* <p>
* Nenhum argumento é validado, a senha não é codificada e os conjuntos de
* roles e de tokens começam vazios.
* </p>
*
* @param id identificador do usuário
* @param firstName primeiro nome
* @param lastName sobrenome
* @param email e-mail (também usado como nome de usuário)
* @param password senha, esperada já codificada
* @param active indica se a conta está ativa
*/
public User(Long id, String firstName, String lastName, String email, String password, boolean active) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
this.active = active;
}
/**
* @return identificador único do usuário
*/
public Long getId() {
return id;
}
/**
* @param id identificador único do usuário
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return primeiro nome do usuário
*/
public String getFirstName() {
return firstName;
}
/**
* @param firstName primeiro nome do usuário
*/
public void setFirstName(String firstName) {
this.firstName = firstName;
}
/**
* @return sobrenome do usuário
*/
public String getLastName() {
return lastName;
}
/**
* @param lastName sobrenome do usuário
*/
public void setLastName(String lastName) {
this.lastName = lastName;
}
/**
* @return email do usuário
*/
public String getEmail() {
return email;
}
/**
* @param email email do usuário
*/
public void setEmail(String email) {
this.email = email;
}
/**
* @return senha criptografada do usuário
*/
public String getPassword() {
return password;
}
/**
* @param password senha criptografada do usuário
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return conjunto de roles/autoridades associadas ao usuário
*/
public Set<Role> getRoles() {
return roles;
}
/**
* Adiciona uma role ao usuário.
*
* <p>
* Como as roles formam um conjunto, adicionar uma role já presente não a
* duplica. Não há validação: a role não é verificada nem persistida por este
* método.
* </p>
*
* @param role role a ser adicionada
*/
public void addRole(Role role) {
this.roles.add(role);
}
/**
* @return {@code true} se a conta do usuário estiver ativa
*/
public boolean isActive() {
return active;
}
/**
* Define diretamente o indicador de conta ativa. Tem o mesmo efeito de
* {@link #activate()} (com {@code true}) e de {@link #deactivate()} (com
* {@code false}).
*
* @param active {@code true} para ativar a conta, {@code false} para
* desativá-la
*/
public void setActive(boolean active) {
this.active = active;
}
/**
* Ativa a conta ({@code active = true}). Operação idempotente, sem outros
* efeitos: não altera roles nem tokens.
*/
public void activate() {
this.active = true;
}
/**
* Desativa a conta ({@code active = false}). Operação idempotente, sem outros
* efeitos: não altera roles nem tokens.
*/
public void deactivate() {
this.active = false;
}
/**
* Verifica se o usuário possui uma role com o nome informado.
*
* <p>
* A comparação é exata (diferencia maiúsculas de minúsculas) e feita com o
* valor de {@link Role#getAuthority()}; portanto o nome deve incluir o
* prefixo, por exemplo {@code ROLE_ADMIN}.
* </p>
*
* @param roleName nome da role a ser verificada
* @return {@code true} se o usuário possui a role; {@code false} caso
* contrário
*/
public boolean hasRole(String roleName) {
if (roleName == null) {
return false;
}
return roles.stream().anyMatch(role -> roleName.equals(role.getAuthority()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
/**
* Compara dois usuários pelo identificador ({@code id}).
*
* <p>
* Duas instâncias sem {@code id} (ainda não persistidas) são consideradas
* iguais entre si, e {@link #hashCode()} depende apenas do {@code id}. A
* comparação exige a mesma classe exata ({@code getClass()}), e não apenas
* compatibilidade de tipo.
* </p>
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
/**
* Retorna as autoridades (roles) associadas ao usuário.
*
* <p>
* Este método é usado pelo Spring Security para determinar as permissões do
* usuário durante a autenticação e autorização.
* </p>
*
* @return coleção de autoridades (roles) do usuário; é o próprio conjunto
* interno {@code roles}, e não uma cópia
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
/**
* Retorna o email do usuário, que é usado como nome de usuário para
* autenticação.
*
* @return email do usuário como nome de usuário para autenticação
*/
@Override
public String getUsername() {
return email;
}
/**
* Retorna sempre {@code true}: o modelo atual não possui expiração de conta.
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* Retorna sempre {@code true}: o modelo atual não possui bloqueio de conta.
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* Retorna sempre {@code true}: o modelo atual não possui expiração de
* credenciais.
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* Indica se a conta está habilitada para autenticação. Devolve o valor do
* indicador {@code active}.
*
* @return {@code true} se a conta estiver ativa
*/
@Override
public boolean isEnabled() {
return active;
}
}