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;
    }
}