User.java
/*
* Copyright © 2023-2025 The CTAN Team and individual authors
*
* This file is distributed under the 3-clause BSD license.
* See file LICENSE for details.
*/
package org.ctan.site.domain.account;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import org.ctan.site.domain.Gender;
import org.ctan.site.services.util.SecurityUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
/**
* The domain class <code>User</code> contains the description of a user account
* on the site.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Entity
@Table(name = "usr")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class User {
/**
* The class <code>CtanPrincipal</code> contains the transport object for
* authentication.
*/
@AllArgsConstructor
public static final class CtanPrincipal implements Principal {
/**
* The field <code>name</code> contains the account name.
*/
@Getter
private String name;
}
/**
* The class <code>PrintableUser</code> contains the printable attributes of
* a user.
*/
@Getter
@Builder
public static class PrintableUser {
long id;
String account;
String name;
}
/**
* The field <code>id</code> contains the numerical id.
*/
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "usr_generator")
@SequenceGenerator(name = "usr_generator", sequenceName = "usr_seq",
allocationSize = 1)
@EqualsAndHashCode.Exclude
private Long id;
/**
* The field <code>version</code> contains the version number for optimistic
* locking.
*/
@Version
@EqualsAndHashCode.Exclude
@Default
private Long version = 1L;
/**
* The field <code>accountExpired</code> contains the indication whether the
* account is expired.
*/
@Column(name = "account_expired")
@Default
private Boolean accountExpired = false;
/**
* The field <code>accountLocked</code> contains the indication whether the
* account is locked.
*/
@Column(name = "account_locked")
@Default
private Boolean accountLocked = false;
/**
* The field <code>authorKey</code> contains the reference to the author
* table or {@code null}.
*/
@Column(length = 96, name = "author_key")
private String authorKey;
/**
* The field <code>avatar</code> contains the avatar image data.
*/
@Column(length = 65536)
@JsonIgnore
private byte[] avatar;
/**
* The field <code>avatarType</code> contains the file type for the avatar.
*/
@Column(length = 32, name = "avatar_type")
@JsonIgnore
private String avatarType;
/**
* The field <code>dateCreated</code> contains the timestamp when this
* account has been created.
*/
@Column(name = "date_created")
@JsonFormat(pattern = "dd-MM-yyyy hh:mm:ss")
private LocalDateTime dateCreated;
/**
* The field <code>email</code> contains the email of the user or
* {@code null}.
*/
@Column(length = 255)
private String email;
/**
* The field <code>enabled</code> contains the indicator that the user has
* confirmed the registered account.
*/
@Column
@Default
private Boolean enabled = true;
/**
* The field <code>gender</code> contains the gender of the user.
*/
@Column(name = "gender", length = 6)
@Enumerated(EnumType.STRING)
@Default
private Gender gender = Gender.M;
/**
* The field <code>htmlEmail</code> contains the indicator whether the user
* wants to receive HTML email.
*/
@Column(name = "html_email")
@Default
private Boolean htmlEmail = true;
/**
* The field <code>city</code> contains the description where the user is
* located.
*/
@Column(length = 64)
private String city;
/**
* The field <code>country</code> contains the description where the user is
* located.
*/
@Column(length = 4)
private String country;
/**
* The field <code>name</code> contains the real name of the user.
*/
@Column(length = 96, nullable = false)
private String name;
/**
* The field <code>password</code> contains the encoded password.
*/
@Column(length = 64)
@JsonIgnore
private String password;
/**
* The field <code>passwordExpired</code> contains the indicator whether the
* password is expired. Currently the password does not expire at all.
*/
@Column(name = "password_expired")
@Default
private Boolean passwordExpired = false;
/**
* The field <code>roles</code> contains the roles of the user.
*/
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role", nullable = false)
@Enumerated(EnumType.STRING)
// @JsonBackReference
// @JsonIgnore
@Default
@EqualsAndHashCode.Exclude
private Set<Role> roles = new HashSet<>();
/**
* The field <code>selfDescription</code> contains the public description of
* the user.
*/
@Column(length = 16384, name = "self_description")
private String selfDescription;
/**
* The field <code>showEmail</code> contains the indicator whether to
* present the email publicly.
*/
@Column(name = "show_email")
@Default
private Boolean showEmail = true;
/**
* The field <code>showName</code> contains the indicator whether to present
* the real name publicly.
*/
@Column(name = "show_name")
@Default
private Boolean showName = true;
/**
* The field <code>account</code> contains the unique name of the user
* account.
*/
@Column(length = 64, unique = true, nullable = false)
private String account;
/**
* The field <code>principal</code> contains the generated user principal.
*/
@Transient
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@ToString.Exclude
@EqualsAndHashCode.Exclude
@Default
private transient CtanPrincipal principal = null;
/**
* The method <code>accept</code> provides means to check that the user is
* authenticated.
*
* @param passwd the password
*
* @return {@code true} iff the password is valid
*/
public boolean accept(String passwd) {
return enabled
&& !accountExpired
&& !accountLocked
&& !passwordExpired
&& SecurityUtils.verify(passwd, password);
}
/**
* The method <code>getPrincipal</code> provides means to get the principal.
*
* @return the principal
*/
public Principal getPrincipal() {
if (principal == null) {
principal = new CtanPrincipal(account);
}
return principal;
}
/**
* The method <code>getPrintable</code> provides means to get the printable
* attributes of the user.
*
* @return the user attributes
*/
public PrintableUser getPrintable() {
return PrintableUser.builder()
.id(getId())
.account(account)
.name(showName ? name : null)
.build();
}
/**
* The method <code>setHashedPassword</code> provides means to set the
* password. Internally the password is hashed and the hash is stored.
*
* @param passwd the clear-text password
*/
public void setHashedPassword(String passwd) {
this.password =
(passwd == null ? null : SecurityUtils.generateHash(passwd));
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return account;
}
}