User.java

/*
 * BSD 2-Clause License
 * 
 * Copyright (c) 2022, [Aleksandra Serba, Marcin Czerniak, Bartosz Wawrzyniak, Adrian Antkowiak]
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package dev.vernite.vernite.user;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import dev.vernite.vernite.common.utils.counter.CounterSequence;
import dev.vernite.vernite.workspace.Workspace;

import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

@Entity
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private Date deleted;

    private boolean deletedPermanently;

    @Column(nullable = false, unique = true)
    private String email;

    @JsonIgnore
    @Column(nullable = false, length = 32)
    @NotNull
    private byte[] hash;

    @JsonIgnore
    @Column(nullable = false, length = 20)
    private byte[] salt;

    private String avatar;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String surname;

    @Column(nullable = false, unique = true)
    private String username;

    private String language;
    private String dateFormat;

    @Getter
    @Setter
    private String timeFormat;

    @Getter
    @Setter
    private Integer firstDayOfWeek;

    @JsonIgnore
    @OnDelete(action = OnDeleteAction.CASCADE)
    @OneToOne(cascade = { CascadeType.PERSIST }, optional = false)
    private CounterSequence counterSequence;

    @JsonIgnore
    @OnDelete(action = OnDeleteAction.CASCADE)
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    @OrderBy("name, id")
    private List<Workspace> workspaces = new ArrayList<>();

    public User() {
    }

    public User(String name, String surname, String username, String email, String password) {
        this(name, surname, username, email, password, null, null);
    }

    public User(String name, String surname, String username, String email, String password, String language,
            String dateFormat) {
        this.setName(name);
        this.setSurname(surname);
        this.setUsername(username);
        this.setEmail(email);
        this.setPassword(password);
        this.setLanguage(language);
        this.setDateFormat(dateFormat);
        this.setCounterSequence(new CounterSequence());
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public boolean isDeleted() {
        return getDeleted() != null;
    }

    public Date getDeleted() {
        return deleted;
    }

    public void setDeleted(Date deleted) {
        this.deleted = deleted;
    }

    public boolean isDeletedPermanently() {
        return deletedPermanently;
    }

    public void setDeletedPermanently(boolean deletedPermanently) {
        this.deletedPermanently = deletedPermanently;
    }

    public String getEmail() {
        return isDeleted() ? "(deleted)" : email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public byte[] getHash() {
        return hash;
    }

    public void setHash(byte[] hash) {
        this.hash = hash;
    }

    public byte[] getSalt() {
        return salt;
    }

    public void setSalt(byte[] salt) {
        this.salt = salt;
    }

    public String getAvatar() {
        return isDeleted() ? null : avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getName() {
        return isDeleted() ? "(deleted)" : name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return isDeleted() ? "(deleted)" : surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public String getUsername() {
        return isDeleted() ? "(deleted)" : username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public List<Workspace> getWorkspaces() {
        return workspaces;
    }

    public void setWorkspaces(List<Workspace> workspaces) {
        this.workspaces = workspaces;
    }

    public boolean checkPassword(String password) {
        byte[] hash;
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(password.getBytes(StandardCharsets.UTF_8));
            digest.update(this.getSalt());
            hash = digest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        return MessageDigest.isEqual(hash, this.getHash());
    }

    public void setPassword(String password) {
        byte[] salt = new byte[20];
        new SecureRandom().nextBytes(salt);
        byte[] hash;
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(password.getBytes(StandardCharsets.UTF_8));
            digest.update(salt);
            hash = digest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        this.setHash(hash);
        this.setSalt(salt);
    }

    public String getLanguage() {
        return language;
    }

    public void setLanguage(String language) {
        this.language = language;
    }

    public String getDateFormat() {
        return dateFormat;
    }

    public void setDateFormat(String dateFormat) {
        this.dateFormat = dateFormat;
    }

    public CounterSequence getCounterSequence() {
        return counterSequence;
    }

    public void setCounterSequence(CounterSequence counterSequence) {
        this.counterSequence = counterSequence;
    }

    @Override
    public String toString() {
        return "U[" + this.getUsername() + "#" + this.getId() + "]";
    }

    // TODO: lombok auto generate
    @Override
    public int hashCode() {
        // final int prime = 31;
        // int result = 1;
        // result = prime * result + Arrays.hashCode(getHash());
        // result = prime * result + Arrays.hashCode(getSalt());
        // result = prime * result + Objects.hash(getAvatar(), getCounterSequence(),
        // getEmail(), getId(), getName(), getSurname(), getUsername(), getWorkspaces(),
        // getDeleted(), getLanguage(), getDateFormat());
        return (int) getId();
    }

    // TODO: lombok auto generate
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        User other = (User) obj;
        return getId() == other.getId();
        // return Objects.equals(getAvatar(), other.getAvatar()) &&
        // Objects.equals(getCounterSequence(), other.getCounterSequence())
        // && Objects.equals(getEmail(), other.getEmail()) && Arrays.equals(getHash(),
        // other.getHash()) && getId() == other.getId()
        // && Objects.equals(getName(), other.getName()) && Arrays.equals(getSalt(),
        // other.getSalt())
        // && Objects.equals(getSurname(), other.getSurname()) &&
        // Objects.equals(getUsername(), other.getUsername())
        // && Objects.equals(getWorkspaces(), other.getWorkspaces()) &&
        // Objects.equals(getDeleted(), other.getDeleted())
        // && Objects.equals(getLanguage(), other.getLanguage()) &&
        // Objects.equals(getDateFormat(), other.getDateFormat());
    }
}