Added base user functions

This commit is contained in:
CodingPhoenixx
2026-02-15 18:41:32 +01:00
parent fa9a51b1a6
commit e1f4522c7d
5 changed files with 220 additions and 9 deletions
@@ -15,21 +15,24 @@ public class User {
private String email; private String email;
private String phoneNumber; private String phoneNumber;
private Role role; private Role role;
private boolean blocked;
public User(ULID id, String firstname, String lastname, String email, String phoneNumber) { public User(ULID id, String firstname, String lastname, String email, String phoneNumber, boolean blocked) {
this.id = id; this.id = id;
this.firstName = firstname; this.firstName = firstname;
this.lastName = lastname; this.lastName = lastname;
this.email = email; this.email = email;
this.phoneNumber = phoneNumber; this.phoneNumber = phoneNumber;
this.blocked = blocked;
} }
public User(ULID id, String firstname, String lastname, String email, String phoneNumber, Role role) { public User(ULID id, String firstname, String lastname, String email, String phoneNumber, boolean blocked, Role role) {
this.id = id; this.id = id;
this.firstName = firstname; this.firstName = firstname;
this.lastName = lastname; this.lastName = lastname;
this.email = email; this.email = email;
this.phoneNumber = phoneNumber; this.phoneNumber = phoneNumber;
this.role = role; this.role = role;
this.blocked = blocked;
} }
} }
@@ -5,10 +5,13 @@ import dev.coph.flightscore.backend.action.result.LoginActionResult;
import dev.coph.flightscore.backend.config.Config; import dev.coph.flightscore.backend.config.Config;
import dev.coph.flightscore.backend.provider.Provider; import dev.coph.flightscore.backend.provider.Provider;
import dev.coph.flightscore.backend.user.role.Role; import dev.coph.flightscore.backend.user.role.Role;
import dev.coph.flightscore.backend.utils.Hash;
import dev.coph.flightscore.backend.utils.TokenGenerator; import dev.coph.flightscore.backend.utils.TokenGenerator;
import dev.coph.simpleauthentication.cryptography.CCrypt; import dev.coph.simpleauthentication.cryptography.CCrypt;
import dev.coph.simpleauthentication.jwt.JWT; import dev.coph.simpleauthentication.jwt.JWT;
import dev.coph.simpleauthentication.jwt.JwtException; import dev.coph.simpleauthentication.jwt.JwtException;
import dev.coph.simplecache.Cache;
import dev.coph.simplecache.CacheBuilder;
import dev.coph.simplelogger.Logger; import dev.coph.simplelogger.Logger;
import dev.coph.simplesql.database.Column; import dev.coph.simplesql.database.Column;
import dev.coph.simplesql.database.attributes.ColumnType; import dev.coph.simplesql.database.attributes.ColumnType;
@@ -19,6 +22,7 @@ import dev.coph.simpleutilities.ulid.ULID;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -32,6 +36,8 @@ public class UserProvider implements Provider {
private byte[] JWT_SECRET; private byte[] JWT_SECRET;
private long JWT_EXPIRATION_TIME; private long JWT_EXPIRATION_TIME;
private Cache<ULID, User> userCache;
public UserProvider(Backend backend) { public UserProvider(Backend backend) {
this.backend = backend; this.backend = backend;
} }
@@ -52,17 +58,18 @@ public class UserProvider implements Provider {
.createMethod(CreateMethod.IF_NOT_EXISTS) .createMethod(CreateMethod.IF_NOT_EXISTS)
.table("users") .table("users")
.column("id", DataType.BINARY, 26, true) .column("id", DataType.BINARY, 26, true)
.column("fistname", DataType.VARCHAR, 255) .column("firstname", DataType.VARCHAR, 255)
.column("lastname", DataType.VARCHAR, 255) .column("lastname", DataType.VARCHAR, 255)
.column("email", DataType.VARCHAR, 255, ColumnType.UNIQUE, true) .column("email", DataType.VARCHAR, 255, ColumnType.UNIQUE, true)
.column("phoneNumber", DataType.VARCHAR, 255) .column("phoneNumber", DataType.VARCHAR, 255)
.column("password", DataType.VARCHAR, 255, true) .column("password", DataType.VARCHAR, 255, true)
.column(new Column("blocked", DataType.BOOLEAN, true).defaultValue(false))
.column("role", DataType.BINARY, 26) .column("role", DataType.BINARY, 26)
.primaryKey(List.of("id")) .primaryKey(List.of("id"))
.index(List.of("email")) .index(List.of("email"))
.index(List.of("email", "password")) .index(List.of("email", "password"))
.foreignKey(List.of("role"), "roles", List.of("id")); .foreignKey(List.of("role"), "roles", List.of("id"));
;
query.query(tableCreate); query.query(tableCreate);
tableCreate = Query.tableCreate() tableCreate = Query.tableCreate()
.createMethod(CreateMethod.IF_NOT_EXISTS) .createMethod(CreateMethod.IF_NOT_EXISTS)
@@ -74,12 +81,18 @@ public class UserProvider implements Provider {
.primaryKey(List.of("token_hash")) .primaryKey(List.of("token_hash"))
.index(List.of("user_id")) .index(List.of("user_id"))
.foreignKey(List.of("user_id"), "users", List.of("id")); .foreignKey(List.of("user_id"), "users", List.of("id"));
;
query.query(tableCreate); query.query(tableCreate);
} }
@Override @Override
public void onEnable(Query query) { public void onEnable(Query query) {
this.userCache = CacheBuilder.<ULID, User>newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.loader(this::loadUser)
.build();
String jwtSecretString = backend.configurationManager().configuration(Config.class).jwt_secret(); String jwtSecretString = backend.configurationManager().configuration(Config.class).jwt_secret();
if (jwtSecretString == null) { if (jwtSecretString == null) {
throw new RuntimeException("JWT_SECRET is not set in config.yml"); throw new RuntimeException("JWT_SECRET is not set in config.yml");
@@ -93,6 +106,49 @@ public class UserProvider implements Provider {
JWT_EXPIRATION_TIME = jwtExpirationMinutes * 60 * 1000; JWT_EXPIRATION_TIME = jwtExpirationMinutes * 60 * 1000;
} }
private User loadUser(ULID userId) {
AtomicReference<User> userReference = new AtomicReference<>();
var select = Query.select()
.table("users")
.condition("id", userId.toString())
.resultActionAfterQuery(srs -> {
srs.next(resultSet -> {
byte[] roleBytes = resultSet.getBytes("role");
if (roleBytes != null) {
ULID roleId = new ULID(roleBytes);
Role role = backend.roleProvider().role(roleId);
if (role == null) {
logger.warn("User '%s' has role but role not found.".formatted(userId));
return;
}
User user = new User(
new ULID(resultSet.getBytes("id")),
resultSet.getString("firstname"),
resultSet.getString("lastname"),
resultSet.getString("email"),
resultSet.getString("phoneNumber"),
resultSet.getBoolean("blocked"),
role
);
userReference.set(user);
} else {
User user = new User(
new ULID(resultSet.getBytes("id")),
resultSet.getString("firstname"),
resultSet.getString("lastname"),
resultSet.getString("email"),
resultSet.getString("phoneNumber"),
resultSet.getBoolean("blocked")
);
userReference.set(user);
}
}, () -> logger.warn("User not found with id: " + userId), e -> logger.error("Error logging in user", e));
});
Query query = new Query(backend.databaseAdapter());
query.executeQuery(select);
return userReference.get();
}
public LoginActionResult login(String email, String password) { public LoginActionResult login(String email, String password) {
Query query = new Query(backend.databaseAdapter()); Query query = new Query(backend.databaseAdapter());
@@ -102,8 +158,13 @@ public class UserProvider implements Provider {
.table("users") .table("users")
.condition("email", email) .condition("email", email)
.columnKey("password") .columnKey("password")
.columnKey("blocked")
.resultActionAfterQuery(srs -> { .resultActionAfterQuery(srs -> {
srs.next(resultSet -> { srs.next(resultSet -> {
if (resultSet.getBoolean("blocked")) {
logger.warn("Blocked User with email: " + email + " tried to login.");
return;
}
var hashedPassword = resultSet.getString("password"); var hashedPassword = resultSet.getString("password");
if (CCrypt.checkPassword(password, hashedPassword)) { if (CCrypt.checkPassword(password, hashedPassword)) {
success.set(true); success.set(true);
@@ -136,6 +197,7 @@ public class UserProvider implements Provider {
resultSet.getString("lastname"), resultSet.getString("lastname"),
resultSet.getString("email"), resultSet.getString("email"),
resultSet.getString("phoneNumber"), resultSet.getString("phoneNumber"),
resultSet.getBoolean("blocked"),
role role
); );
userReference.set(user); userReference.set(user);
@@ -145,7 +207,8 @@ public class UserProvider implements Provider {
resultSet.getString("firstname"), resultSet.getString("firstname"),
resultSet.getString("lastname"), resultSet.getString("lastname"),
resultSet.getString("email"), resultSet.getString("email"),
resultSet.getString("phoneNumber") resultSet.getString("phoneNumber"),
resultSet.getBoolean("blocked")
); );
userReference.set(user); userReference.set(user);
} }
@@ -158,6 +221,7 @@ public class UserProvider implements Provider {
logger.error("User-Details not found with email: " + email); logger.error("User-Details not found with email: " + email);
return new LoginActionResult(false, "Error logging in user"); return new LoginActionResult(false, "Error logging in user");
} }
userCache.put(user.id(), user);
long expiresAt = System.currentTimeMillis() + JWT_EXPIRATION_TIME; long expiresAt = System.currentTimeMillis() + JWT_EXPIRATION_TIME;
String accessToken = createAccessToken(user, expiresAt); String accessToken = createAccessToken(user, expiresAt);
@@ -169,7 +233,103 @@ public class UserProvider implements Provider {
var insert = Query.insert() var insert = Query.insert()
.table("refresh_tokens") .table("refresh_tokens")
.entry("user_id", user.id().toString()) .entry("user_id", user.id().toString())
.entry("token", CCrypt.hashPassword(refreshToken, CCrypt.gensalt())) .entry("token", Hash.hashString(refreshToken))
.entry("expires_at", Timestamp.from(Instant.ofEpochMilli(expiresAt)));
query.async(true).executeQuery(insert);
return new LoginActionResult(user, accessToken, refreshToken);
}
public LoginActionResult refreshToken(String refreshToken) {
if (refreshToken == null || refreshToken.isEmpty()) {
return new LoginActionResult(false, "Invalid refresh token");
}
Query query = new Query(backend.databaseAdapter());
AtomicReference<User> userReference = new AtomicReference<>();
var select = Query.select()
.table("refresh_tokens")
.condition("token_hash", Hash.hashString(refreshToken))
.resultActionAfterQuery(srs -> {
srs.next(resultSet -> {
if (resultSet.getBoolean("revoked")) {
return;
}
if (resultSet.getTimestamp("expires_at").before(Timestamp.from(Instant.now()))) {
return;
}
byte[] userIdBytes = resultSet.getBytes("user_id");
if (userIdBytes == null) {
return;
}
ULID userId = new ULID(userIdBytes);
userReference.set(userCache.get(userId));
}, () -> logger.debug("Refresh token not found"), e -> logger.error("Error refreshing token", e));
});
query.executeQuery(select);
User user = userReference.get();
if (user == null) {
return new LoginActionResult(false, "Invalid refresh token");
}
var update = Query.update()
.table("refresh_tokens")
.condition("token_hash", Hash.hashString(refreshToken))
.entry("revoked", true);
query.query(update);
long expiresAt = System.currentTimeMillis() + JWT_EXPIRATION_TIME;
String accessToken = createAccessToken(user, expiresAt);
if (accessToken == null) {
return new LoginActionResult(false, "Error creating JWT");
}
refreshToken = TokenGenerator.generateRefreshToken();
var insert = Query.insert()
.table("refresh_tokens")
.entry("user_id", user.id().toString())
.entry("token", Hash.hashString(refreshToken))
.entry("expires_at", Timestamp.from(Instant.ofEpochMilli(expiresAt)));
query.async(true).executeQuery(insert);
return new LoginActionResult(user, accessToken, refreshToken);
}
public LoginActionResult register(String firstName, String lastName, String email, String phoneNumber, String password) {
Role role = backend.roleProvider().defaultRole();
var userId = ULID.randomUlid();
var hashedPassword = CCrypt.hashPassword(password, CCrypt.gensalt());
Query query = new Query(backend.databaseAdapter());
var insert = Query.insert()
.table("users")
.entry("id", userId.toString())
.entry("firstname", firstName)
.entry("lastname", lastName)
.entry("email", email)
.entry("phoneNumber", phoneNumber)
.entry("password", hashedPassword)
.entry("role", role.id().toString());
query.executeQuery(insert);
User user = new User(userId, firstName, lastName, email, phoneNumber, false, role);
userCache.put(user.id(), user);
long expiresAt = System.currentTimeMillis() + JWT_EXPIRATION_TIME;
String accessToken = createAccessToken(user, expiresAt);
if (accessToken == null) {
return new LoginActionResult(false, "Error creating JWT");
}
String refreshToken = TokenGenerator.generateRefreshToken();
insert = Query.insert()
.table("refresh_tokens")
.entry("user_id", user.id().toString())
.entry("token", Hash.hashString(refreshToken))
.entry("expires_at", Timestamp.from(Instant.ofEpochMilli(expiresAt))); .entry("expires_at", Timestamp.from(Instant.ofEpochMilli(expiresAt)));
query.async(true).executeQuery(insert); query.async(true).executeQuery(insert);
@@ -11,13 +11,15 @@ import java.util.HashSet;
@Accessors(fluent = true) @Accessors(fluent = true)
public class Role { public class Role {
public Role(ULID id, String name) { public Role(ULID id, String name, boolean defaultRole) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.defaultRole = defaultRole;
} }
private ULID id; private ULID id;
private String name; private String name;
private boolean defaultRole;
private HashSet<Permission> permissions = new HashSet<>(); private HashSet<Permission> permissions = new HashSet<>();
} }
@@ -4,17 +4,24 @@ import dev.coph.flightscore.backend.Backend;
import dev.coph.flightscore.backend.provider.Provider; import dev.coph.flightscore.backend.provider.Provider;
import dev.coph.flightscore.backend.user.permission.Permission; import dev.coph.flightscore.backend.user.permission.Permission;
import dev.coph.simplelogger.Logger; import dev.coph.simplelogger.Logger;
import dev.coph.simplesql.database.Column;
import dev.coph.simplesql.database.attributes.CreateMethod; import dev.coph.simplesql.database.attributes.CreateMethod;
import dev.coph.simplesql.database.attributes.DataType; import dev.coph.simplesql.database.attributes.DataType;
import dev.coph.simplesql.query.Query; import dev.coph.simplesql.query.Query;
import dev.coph.simpleutilities.ulid.ULID; import dev.coph.simpleutilities.ulid.ULID;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.units.qual.C;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@Accessors(fluent = true)
public class RoleProvider implements Provider { public class RoleProvider implements Provider {
private final Logger logger = Logger.of("RoleProvider"); private final Logger logger = Logger.of("RoleProvider");
private HashMap<ULID, Role> roles = new HashMap<>(); private HashMap<ULID, Role> roles = new HashMap<>();
@Getter
private Role defaultRole;
private final Backend backend; private final Backend backend;
@@ -40,8 +47,10 @@ public class RoleProvider implements Provider {
.table("roles") .table("roles")
.column("id", DataType.BINARY, 26, true) .column("id", DataType.BINARY, 26, true)
.column("name", DataType.VARCHAR, 255) .column("name", DataType.VARCHAR, 255)
.column(new Column("default", DataType.BOOLEAN, true).defaultValue(false))
.primaryKey(List.of("id")); .primaryKey(List.of("id"));
query.query(tableCreate); query.query(tableCreate);
tableCreate = Query.tableCreate() tableCreate = Query.tableCreate()
.createMethod(CreateMethod.IF_NOT_EXISTS) .createMethod(CreateMethod.IF_NOT_EXISTS)
.table("role_permissions") .table("role_permissions")
@@ -61,8 +70,13 @@ public class RoleProvider implements Provider {
srs.forEach(resultSet -> { srs.forEach(resultSet -> {
var roleIdBytes = resultSet.getBytes("id"); var roleIdBytes = resultSet.getBytes("id");
var roleName = resultSet.getString("name"); var roleName = resultSet.getString("name");
var isDefault = resultSet.getBoolean("default");
var roleId = new ULID(roleIdBytes); var roleId = new ULID(roleIdBytes);
roles.put(roleId, new Role(roleId, roleName)); Role role = new Role(roleId, roleName, isDefault);
roles.put(roleId, role);
if (isDefault) {
defaultRole = role;
}
}, () -> logger.warn("Error loading roles: No roles found"), e -> logger.error("Error loading roles", e)); }, () -> logger.warn("Error loading roles: No roles found"), e -> logger.error("Error loading roles", e));
logger.success("Loaded " + roles.size() + " roles!"); logger.success("Loaded " + roles.size() + " roles!");
}); });
@@ -94,11 +108,14 @@ public class RoleProvider implements Provider {
public Role role(ULID id) { public Role role(ULID id) {
return roles.get(id); return roles.get(id);
} }
public HashMap<ULID, Role> roles() { public HashMap<ULID, Role> roles() {
return roles; return roles;
} }
public Role role(String name) { public Role role(String name) {
return roles.values().stream().filter(role -> role.name().equalsIgnoreCase(name)).findFirst().orElse(null); return roles.values().stream().filter(role -> role.name().equalsIgnoreCase(name)).findFirst().orElse(null);
} }
} }
@@ -0,0 +1,29 @@
package dev.coph.flightscore.backend.utils;
import dev.coph.simplelogger.GenericLogger;
import dev.coph.simplelogger.Logger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Hash {
public static String hashString(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedHash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : encodedHash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
GenericLogger.error("Error hashing string", e);
return null;
}
}
}