package de.srsoftware.widerhall.data; import de.srsoftware.widerhall.Util; import java.security.InvalidKeyException; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; import java.util.*; import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Util.t; /** * @author Stephan Richter * This class represents User objects of the widerhall db. */ public class User { public static final String TABLE_NAME = "Users"; public static final int PERMISSION_ADMIN = 1; public static final int PERMISSION_CREATE_LISTS = 2; public static final String HASHED_PASS = "hashedPassword"; public static final String SALT = "salt"; private static final HashMap users = new HashMap<>(); private String email, salt, hashedPass, name; private int permissions; /** * create a new user object * @param email * @param name * @param salt * @param hashedPass * @param permissions */ public User(String email, String name, String salt, String hashedPass, int permissions) { this.email = email.toLowerCase(); this.name = name; this.salt = salt; this.hashedPass = hashedPass; this.permissions = permissions; } /*********** field accessors ***************/ public String email() { return email; } public String hashedPassword() { return hashedPass; } public String name() { return name; } public int permissions(){ return permissions; } public String salt(){ return salt; } /************** end of field accessors ****************/ /** * Add a new permission to the current user object. * Also updates the corresponding db entry * @param newPermission * @throws SQLException */ public void addPermission(int newPermission) throws SQLException { permissions |= newPermission; Database.open() .update(TABLE_NAME) .set(PERMISSIONS,permissions) .where(EMAIL,email()) .compile() .run(); } /** * Create a new user object by hashing it's password and storing user data, salt and hashed password to the db. * Initially, the user is created without any permissions. * @param email * @param name * @param password * @return * @throws SQLException */ public static User create(String email, String name, String password) throws SQLException { email = email.toLowerCase(); String salt = null; String hashedPass = null; if (password != null) { salt = Util.sha256(email + name + LocalDate.now()); hashedPass = Util.sha256(password + salt); } return new User(email,name,salt,hashedPass,0).save(); } /** * create user table * @throws SQLException */ public static void createTable() throws SQLException { var sql = new StringBuilder() .append("CREATE TABLE ").append(TABLE_NAME) .append(" (") .append(EMAIL).append(" ").append(VARCHAR).append(" NOT NULL PRIMARY KEY, ") .append(NAME).append(" ").append(VARCHAR).append(", ") .append(PERMISSIONS).append(" ").append(INT).append(", ") .append(SALT).append(" ").append(VARCHAR).append(", ") .append(HASHED_PASS).append(" ").append(VARCHAR) .append(");"); Database.open().query(sql).compile().run(); } /** * Withdraw a specific permission from the user object. * Updated permission flag will be written to db. * @param newPermission * @throws SQLException */ public void dropPermission(int newPermission) throws SQLException { permissions ^= (permissions & newPermission); Database.open().update(TABLE_NAME).set(PERMISSIONS,permissions).compile().run(); } /** * check, if User object has requested permission(s). * @param permission * @return */ public boolean hashPermission(int permission){ return (permissions & permission) > 0; } public static User load(String email) throws SQLException { var rs = Database.open().select(TABLE_NAME).where(EMAIL,email.toLowerCase()).compile().exec(); try { if (rs.next()) { return User.from(rs); } return null; } finally { rs.close(); } } /** * Load the list of all users. Internally calls loadAll(null) * @return * @throws SQLException */ public static List loadAll() throws SQLException { return loadAll(null); } /** * Load the list of all users identified by the provided email list. * If emails is null, all users are loaded. * If emails is empty, an empty list well be returned. * @param emails * @return * @throws SQLException */ public static List loadAll(Collection emails) throws SQLException { if (emails != null && emails.isEmpty()) return List.of(); var userList = new ArrayList(); var query = Database.open().select(TABLE_NAME); if (emails != null) query.where(EMAIL,emails); var rs = query.compile().exec(); while (rs.next()) userList.add(User.from(rs)); Collections.sort(userList,(u1,u2)->u1.name.compareTo(u2.name)); return userList; } /** * Create a new User object from a ResultSet. * This method is cached: If a User object with an identifying email has been loaded before, the already-loaded object will be returned. * @param rs * @return * @throws SQLException */ private static User from(ResultSet rs) throws SQLException { var email = rs.getString(EMAIL); var user = users.get(email); if (user == null) users.put(email,user = new User( rs.getString(EMAIL), rs.getString(NAME), rs.getString(SALT), rs.getString(HASHED_PASS), rs.getInt(PERMISSIONS))); return user; } /** * Loads the user identified by it's email, but only if the provided password matches. * @param email * @param password * @return * @throws InvalidKeyException * @throws SQLException */ public static User loadUser(String email, String password) throws InvalidKeyException, SQLException { ResultSet rs = Database.open() .select(TABLE_NAME) .where(EMAIL,email.toLowerCase()) .compile() .exec(); try { if (rs.next()) { var loadedUser = User.from(rs); if (loadedUser.matching(password)) return loadedUser; } } finally { rs.close(); } throw new InvalidKeyException(); } /** * checks, if the provided password matches the User obejcts's original password by comparing hashes. * @param password * @return */ private boolean matching(String password) { if (hashedPass == null) return password == null; return hashedPass.equals(Util.sha256(password+salt)); } /** * Checks, whether the user table is empty * @return * @throws SQLException */ public static boolean noUsers() throws SQLException { var rs = Database.open().select(TABLE_NAME,"count(*)").compile().exec(); try { if (rs.next()) return rs.getInt(1) < 1; } finally { rs.close(); } return false; } /** * creates a readable permission list from the permission flag. * @return */ public String permissionList(){ var list = new ArrayList(); if (hashPermission(PERMISSION_ADMIN)) list.add("admin"); if (hashPermission(PERMISSION_CREATE_LISTS)) list.add("create lists"); return String.join(", ",list); } /** * creates a map containing all of the Users data but the password. * @return */ public Map safeMap(){ return Map.of(NAME,name,EMAIL,email,PERMISSIONS,permissionList(),PASSWORD,t(hashedPassword() == null ? "no" : "yes")); } /** * sae the current User object to the database * @return * @throws SQLException */ private User save() throws SQLException { var req = Database.open().insertInto(TABLE_NAME).set(EMAIL,email).set(NAME,name); if (salt != null) req.set(SALT,salt); if (hashedPass != null) req.set(HASHED_PASS,hashedPass); req.compile().run(); return this; } }