package models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import controllers.OJController;
import controllers.routes;
import play.Logger;
import play.Play;
import play.cache.Cache;
import play.data.validation.Constraints;
import play.db.ebean.Model;
import utils.FileUtil;
import utils.ImageUtil;
import utils.Mailer;
import utils.StringHashing;
import javax.persistence.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* User to the system, including system admin and end user.
*
* @author Xinzi Zhou
*/
@Entity
public class User extends Model {
@Id
public long id;
/**
* An unique string identifier of the user.
*/
@Constraints.Pattern("^(\\w+)$")
@Column(unique = true, nullable = false)
public String name;
@Column(unique = true, nullable = false)
public String email;
public boolean isEmailVerified = false;
public Date lastEmailModifiedTime = new Date();
public Date lastVerificationEmailSentTime = new Date();
/**
* When the user requested for creating a new password.
* This time is also used to generate a reset token.
*/
public Date resetPasswordRequestedTime;
/**
* The hashed password of the user.
*/
private String password;
/**
* A secret string for the user. Generated programmatically. Invisible to the user and any other clients.
*/
public String secret = UUID.randomUUID().toString();
public int adminLevel = 0;
public int status = 0; // 1 for pending delete. 2 for deleted.
public boolean gender; // female is always true
public String displayName;
public String school;
public String country;
public String link;
@Lob
public String description;
public Date createTime = new Date(); // Create time is never changed after user account is created.
public Date lastLoginTime;
@OneToMany(mappedBy = "follower")
public List<UserRelation> following;
@OneToMany(mappedBy = "following")
public List<UserRelation> followers;
@OneToMany(mappedBy = "user")
public List<ProblemFollower> followingProblems;
@OneToMany(mappedBy = "user")
public List<ProblemFollower> starringProblems;
@JsonIgnore
@OneToMany(mappedBy = "user")
public List<Solution> solutions;
@JsonIgnore
@OneToMany(mappedBy = "user")
public List<ContestParticipant> participatingContests;
@JsonIgnore
@OneToMany(mappedBy = "user")
public List<Mail> mails;
public static final long ONE_DAY = 24 * 60 * 60 * 1000;
public static boolean isValidEmail(String email) {
final String EMAIL_PATTERN =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
return email.matches(EMAIL_PATTERN);
}
public String setEmail(String newEmail) {
if (newEmail == null) {
return "Email is empty.";
}
if (!isValidEmail(newEmail)) {
return "Email is invalid";
}
if (User.find.where().eq("email", newEmail).findRowCount() > 0) {
return "Email is registered.";
}
email = newEmail;
isEmailVerified = false;
lastEmailModifiedTime = new Date();
return null;
}
public static void updateStatus() {
List<User> pendingVerifyUsers = find.where().eq("isEmailVerified", false).findList();
for (User user : pendingVerifyUsers) {
if ((new Date()).getTime() - user.lastEmailModifiedTime.getTime() > ONE_DAY) {
user.pendingDelete();
}
}
}
private static String substring(String in) {
if (in.length() <= 255) {
return in;
}
return in.substring(0, 255);
}
public void pendingDelete() {
if (status > 0) return;
status = 1;
name = substring("deleted_" + UUID.randomUUID().toString() + "_" + name);
email = substring("deleted_" + UUID.randomUUID().toString() + "_" + email);
save();
}
public void setPassword(String password) {
this.password = hashPassword(password);
}
public boolean verifyPassword(String password) {
return this.password.equals(hashPassword(password));
}
private static String hashPassword(String password) {
return StringHashing.sha1(password);
}
public static Finder<Long, User> find = new Finder<> (Long.class, User.class);
@JsonIgnore
public List<Problem> getSolvedProblems() {
List<Problem> problems;
problems = (List<Problem>) Cache.get("userSolvedProblems." + id);
if (problems == null) {
List<Solution> solutions = Solution.find.where().eq("user_id", id).eq("result", 200)
.select("problem").setDistinct(true).orderBy("id DESC").findList();
// Set<Problem> problems = solutions.stream().map(solution -> solution.problem).collect(Collectors.toSet());
// Will get ebean error when using collector.
problems = new ArrayList<>();
for (Solution solution : solutions) {
if (!problems.contains(solution.problem)) {
problems.add(solution.problem);
}
}
Cache.set("userSolvedProblems." + id, problems, 60*15);
}
return problems;
}
public void removeSolvedProblemsCache() {
Cache.remove("userSolvedProblems." + id);
}
@JsonIgnore
public String getVerificationPass() {
String secret = Play.application().configuration().getString("application.secret");
String createTimeString = "" + createTime.getTime();
String original = secret + email + createTimeString;
String pass = StringHashing.sha1(original);
Logger.debug("Verification pass for " + name + " is " + pass);
return pass;
}
@JsonIgnore
public String getVerificationLink() {
String siteUrl = Play.application().configuration().getString("application.siteUrl");
String verifyPath = routes.UserController.verifyEmail(name, getVerificationPass()).toString();
return siteUrl + verifyPath;
}
@JsonIgnore
public String getResetPasswordLink() {
String siteUrl = Play.application().configuration().getString("application.siteUrl");
String resetUrl = routes.UserController.resetPassWordPage(name, getResetPasswordToken()).toString();
return siteUrl + resetUrl;
}
public static final long TEN_MIN = 10 * 60 * 1000;
public void sendVerifyEmail() throws Exception {
if (lastVerificationEmailSentTime != null
&& ((new Date()).getTime() - lastVerificationEmailSentTime.getTime()) < TEN_MIN
&& lastEmailModifiedTime.getTime() < lastVerificationEmailSentTime.getTime()) {
Logger.info("User verification email is not sent, because of the minimum time duration limit.");
throw new Exception("Last verification email is just sent, please check your inbox.");
}
lastVerificationEmailSentTime = new Date();
save();
refresh(); // The date in Java and MySQL has different precisions.
String content = views.html.email.verifyEmail.render(this).toString();
Mailer.sendMail(this, "Verify Your Email Address on " + OJController.getSiteName(), content);
}
public String getResetPasswordToken() {
String secret = Play.application().configuration().getString("application.secret");
if (resetPasswordRequestedTime == null) {
return null;
}
String resetTimeString = "" + resetPasswordRequestedTime.getTime();
String preHashToken = secret + email + resetTimeString;
String token = StringHashing.sha1(preHashToken);
Logger.debug("Reset password token generated for " + name + " based on " + resetTimeString + " is " + token);
return token;
}
/**
* Send an email containing link to reset password to user.
* @throws Exception When user is not eligible to reset its password.
*/
public void sendResetPasswordEmail() throws Exception {
if (resetPasswordRequestedTime != null
&& (new Date()).getTime() - resetPasswordRequestedTime.getTime() < TEN_MIN) {
throw new Exception("We have just send you a password reset email. Please check your inbox.");
}
resetPasswordRequestedTime = new Date();
save();
refresh(); // The date in Java and MySQL has different precisions.
String content = views.html.email.resetPassword.render(this).toString();
Mailer.sendMail(this, "Reset Your Password on " + OJController.getSiteName(), content);
}
public void setProfileImage(File file) throws Exception {
Logger.info("Try to set profile image from file " + file.getAbsolutePath() + ".");
Path uploadPath = Paths.get("upload/avatar");
if (!Files.exists(uploadPath)) {
FileUtil.createPath(uploadPath);
}
Path target = getProfileImagePath();
if (Files.exists(target)) {
Files.delete(target);
}
boolean renameSuccess = file.renameTo(target.toFile());
if (!renameSuccess) {
throw new Exception("Unable to rename profile image.");
}
}
public void setProfileImage(byte[] byteArray) throws Exception {
Path temp = Files.createTempFile("avatar", ".jpg");
Files.write(temp, byteArray);
setProfileImage(temp.toFile());
}
/**
* Get the path of profile image.
* @return Path to the user's profile image
*/
@JsonIgnore
public Path getProfileImagePath() {
return Paths.get("upload/avatar/" + id + ".jpg");
}
/**
* Get the byte array of user's resized profile image.
* @param width Output width.
* @param height Output height.
* @return Byte array of an image.
* @throws IOException If the user does not upload a profile image, throws IO Exception.
*/
@JsonIgnore
public byte[] getProfileImage(int width, int height) throws IOException {
return ImageUtil.resize(getProfileImagePath().toFile(), width, height);
}
}