Added login flow

This commit is contained in:
CodingPhoenixx
2026-02-15 13:47:25 +01:00
parent 2a2bd5d7b0
commit fa9a51b1a6
13 changed files with 317 additions and 13 deletions
@@ -83,6 +83,9 @@ public class Backend {
providerManager.createAllDatabaseTables(); providerManager.createAllDatabaseTables();
logger.success("Database tables created!"); logger.success("Database tables created!");
logger.info("Enabling providers...");
providerManager.enableAllProviders();
logger.success("Providers enabled!");
logger.info("Starting web server..."); logger.info("Starting web server...");
webServer.start(); webServer.start();
@@ -0,0 +1,16 @@
package dev.coph.flightscore.backend.action.result;
import lombok.Getter;
import lombok.experimental.Accessors;
@Getter
@Accessors(fluent = true)
public class ActionResult {
private final boolean success;
private final String message;
public ActionResult(boolean success, String message) {
this.success = success;
this.message = message;
}
}
@@ -1,11 +1,28 @@
package dev.coph.flightscore.backend.action.result; package dev.coph.flightscore.backend.action.result;
import dev.coph.flightscore.backend.user.User; import dev.coph.flightscore.backend.user.User;
import lombok.Getter;
import lombok.experimental.Accessors;
public class LoginActionResult { @Getter
@Accessors(fluent = true)
public class LoginActionResult extends ActionResult{
private User user;
private String accessToken;
private String refreshToken;
private final User user;
private final String accessToken;
private final String refreshToken;
public LoginActionResult(boolean success, String message) {
super(success, message);
this.user = null;
this.accessToken = null;
this.refreshToken = null;
}
public LoginActionResult(User user, String accessToken, String refreshToken) {
super(true, "Login successful");
this.user = user;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
} }
@@ -33,4 +33,7 @@ public class Config extends AbstractConfiguration {
@DefaultValue("7a0c878e20c039349ef3fc5e0bebcedeb1441c123c118b243ee196dc6246cfd7") @DefaultValue("7a0c878e20c039349ef3fc5e0bebcedeb1441c123c118b243ee196dc6246cfd7")
private String jwt_secret; private String jwt_secret;
@DefaultValue("15")
private Long jwt_expirationInMinutes;
} }
@@ -4,7 +4,11 @@ import dev.coph.simplesql.query.Query;
public interface Provider { public interface Provider {
int priority(); int priority();
String key(); String key();
public void createDatabaseTables(Query query);
void createDatabaseTables(Query query);
void onEnable(Query query);
} }
@@ -13,17 +13,22 @@ public class ProviderManager {
this.backend = backend; this.backend = backend;
} }
public void createAllDatabaseTables(){ public void createAllDatabaseTables() {
Query query = new Query(backend.databaseAdapter()); Query query = new Query(backend.databaseAdapter());
providers.values().stream().sorted((a, b) -> Integer.compare(b.priority(), a.priority())).forEach(provider -> provider.createDatabaseTables(query)); providers.values().stream().sorted((a, b) -> Integer.compare(b.priority(), a.priority())).forEach(provider -> provider.createDatabaseTables(query));
}
public void enableAllProviders() {
Query query = new Query(backend.databaseAdapter());
providers.values().stream().sorted((a, b) -> Integer.compare(b.priority(), a.priority())).forEach(provider -> provider.onEnable(query));
query.execute(); query.execute();
} }
public void registerProvider(Provider provider){ public void registerProvider(Provider provider) {
providers.put(provider.key(), provider); providers.put(provider.key(), provider);
} }
public <T extends Provider> T provider(String key){ public <T extends Provider> T provider(String key) {
return (T) providers.get(key); return (T) providers.get(key);
} }
@@ -16,4 +16,20 @@ public class User {
private String phoneNumber; private String phoneNumber;
private Role role; private Role role;
public User(ULID id, String firstname, String lastname, String email, String phoneNumber) {
this.id = id;
this.firstName = firstname;
this.lastName = lastname;
this.email = email;
this.phoneNumber = phoneNumber;
}
public User(ULID id, String firstname, String lastname, String email, String phoneNumber, Role role) {
this.id = id;
this.firstName = firstname;
this.lastName = lastname;
this.email = email;
this.phoneNumber = phoneNumber;
this.role = role;
}
} }
@@ -2,21 +2,36 @@ package dev.coph.flightscore.backend.user;
import dev.coph.flightscore.backend.Backend; import dev.coph.flightscore.backend.Backend;
import dev.coph.flightscore.backend.action.result.LoginActionResult; import dev.coph.flightscore.backend.action.result.LoginActionResult;
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.utils.TokenGenerator;
import dev.coph.simpleauthentication.cryptography.CCrypt;
import dev.coph.simpleauthentication.jwt.JWT;
import dev.coph.simpleauthentication.jwt.JwtException;
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;
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 org.checkerframework.checker.units.qual.C; import dev.coph.simpleutilities.ulid.ULID;
import lombok.extern.slf4j.Slf4j;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class UserProvider implements Provider { public class UserProvider implements Provider {
private Logger logger = Logger.of("UserProvider"); private Logger logger = Logger.of("UserProvider");
private final Backend backend; private final Backend backend;
private byte[] JWT_SECRET;
private long JWT_EXPIRATION_TIME;
public UserProvider(Backend backend) { public UserProvider(Backend backend) {
this.backend = backend; this.backend = backend;
} }
@@ -39,14 +54,15 @@ public class UserProvider implements Provider {
.column("id", DataType.BINARY, 26, true) .column("id", DataType.BINARY, 26, true)
.column("fistname", DataType.VARCHAR, 255) .column("fistname", DataType.VARCHAR, 255)
.column("lastname", DataType.VARCHAR, 255) .column("lastname", DataType.VARCHAR, 255)
.column("email", DataType.VARCHAR, 255, 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("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)
@@ -57,12 +73,124 @@ public class UserProvider implements Provider {
.column(new Column("revoked", DataType.BOOLEAN, true).defaultValue(false)) .column(new Column("revoked", DataType.BOOLEAN, true).defaultValue(false))
.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);
} }
public LoginActionResult login(String email, String password) { @Override
public void onEnable(Query query) {
String jwtSecretString = backend.configurationManager().configuration(Config.class).jwt_secret();
if (jwtSecretString == null) {
throw new RuntimeException("JWT_SECRET is not set in config.yml");
}
JWT_SECRET = jwtSecretString.getBytes();
Long jwtExpirationMinutes = backend.configurationManager().configuration(Config.class).jwt_expirationInMinutes();
if (jwtExpirationMinutes == null) {
throw new RuntimeException("JWT_EXPIRATION_MINUTES is not set in config.yml");
}
JWT_EXPIRATION_TIME = jwtExpirationMinutes * 60 * 1000;
}
public LoginActionResult login(String email, String password) {
Query query = new Query(backend.databaseAdapter());
logger.debug("Logging in user with email: " + email);
AtomicBoolean success = new AtomicBoolean(false);
var select = Query.select()
.table("users")
.condition("email", email)
.columnKey("password")
.resultActionAfterQuery(srs -> {
srs.next(resultSet -> {
var hashedPassword = resultSet.getString("password");
if (CCrypt.checkPassword(password, hashedPassword)) {
success.set(true);
}
}, () -> logger.warn("User not found with email: " + email), e -> logger.error("Error logging in user", e));
});
query.executeQuery(select);
if (!success.get()) {
logger.warn("Invalid login credentials for user with email: " + email);
return new LoginActionResult(false, "Invalid login credentials");
}
AtomicReference<User> userReference = new AtomicReference<>();
select = Query.select()
.table("users")
.condition("email", email)
.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(email));
return;
}
User user = new User(
new ULID(resultSet.getBytes("id")),
resultSet.getString("firstname"),
resultSet.getString("lastname"),
resultSet.getString("email"),
resultSet.getString("phoneNumber"),
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")
);
userReference.set(user);
}
}, () -> logger.warn("User not found with email: " + email), e -> logger.error("Error logging in user", e));
});
query.executeQuery(select);
User user = userReference.get();
if (user == null) {
logger.error("User-Details not found with email: " + email);
return new LoginActionResult(false, "Error logging in 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();
var insert = Query.insert()
.table("refresh_tokens")
.entry("user_id", user.id().toString())
.entry("token", CCrypt.hashPassword(refreshToken, CCrypt.gensalt()))
.entry("expires_at", Timestamp.from(Instant.ofEpochMilli(expiresAt)));
query.async(true).executeQuery(insert);
return new LoginActionResult(user, accessToken, refreshToken);
}
private String createAccessToken(User user, long expiresAt) {
try {
return new JWT.Builder()
.audience("flightscore-api")
.issuer("flightscore-api")
.subject(user.email())
.claim("id", user.id().toString())
.claim("role", user.role().id().toString())
.expiresAt(expiresAt / 1000)
.issuedAt(System.currentTimeMillis() / 1000)
.sign(JWT_SECRET);
} catch (JwtException e) {
logger.error("Error creating JWT", e);
}
return null; return null;
} }
} }
@@ -7,6 +7,10 @@ import lombok.experimental.Accessors;
@Getter @Getter
@Accessors(fluent = true) @Accessors(fluent = true)
public class Permission { public class Permission {
public Permission(ULID id, String name) {
this.id = id;
this.name = name;
}
private ULID id; private ULID id;
private String name; private String name;
@@ -2,14 +2,19 @@ package dev.coph.flightscore.backend.user.permission;
import dev.coph.flightscore.backend.Backend; import dev.coph.flightscore.backend.Backend;
import dev.coph.flightscore.backend.provider.Provider; import dev.coph.flightscore.backend.provider.Provider;
import dev.coph.simplelogger.Logger;
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 java.util.HashMap;
import java.util.List; import java.util.List;
public class PermissionProvider implements Provider { public class PermissionProvider implements Provider {
private final Backend backend; private final Backend backend;
private final Logger logger = Logger.of("PermissionProvider");
private final HashMap<ULID, Permission> permissions = new HashMap<>();
public PermissionProvider(Backend backend) { public PermissionProvider(Backend backend) {
this.backend = backend; this.backend = backend;
@@ -36,5 +41,32 @@ public class PermissionProvider implements Provider {
; ;
query.query(tableCreate); query.query(tableCreate);
} }
@Override
public void onEnable(Query query) {
var select = Query.select()
.table("permissions")
.resultActionAfterQuery(srs -> {
srs.forEach(resultSet -> {
var permissionIdBytes = resultSet.getBytes("id");
var permissionName = resultSet.getString("name");
var roleId = new ULID(permissionIdBytes);
permissions.put(roleId, new Permission(roleId, permissionName));
}, () -> logger.warn("Error loading permissions: No permissions found"), e -> logger.error("Error loading permissions", e));
logger.success("Loaded " + permissions.size() + " permissions!");
});
}
public Permission permission(ULID id) {
return permissions.get(id);
}
public HashMap<ULID, Permission> permissions() {
return permissions;
}
public Permission permission(String name) {
return permissions.values().stream().filter(permission -> permission.name().equalsIgnoreCase(name)).findFirst().orElse(null);
}
} }
@@ -11,6 +11,11 @@ import java.util.HashSet;
@Accessors(fluent = true) @Accessors(fluent = true)
public class Role { public class Role {
public Role(ULID id, String name) {
this.id = id;
this.name = name;
}
private ULID id; private ULID id;
private String name; private String name;
private HashSet<Permission> permissions = new HashSet<>(); private HashSet<Permission> permissions = new HashSet<>();
@@ -2,13 +2,20 @@ package dev.coph.flightscore.backend.user.role;
import dev.coph.flightscore.backend.Backend; 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.simplelogger.Logger;
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 java.util.HashMap;
import java.util.List; import java.util.List;
public class RoleProvider implements Provider { public class RoleProvider implements Provider {
private final Logger logger = Logger.of("RoleProvider");
private HashMap<ULID, Role> roles = new HashMap<>();
private final Backend backend; private final Backend backend;
public RoleProvider(Backend backend) { public RoleProvider(Backend backend) {
@@ -45,5 +52,53 @@ public class RoleProvider implements Provider {
.foreignKey(List.of("permissionId"), "permissions", List.of("id")); .foreignKey(List.of("permissionId"), "permissions", List.of("id"));
query.query(tableCreate); query.query(tableCreate);
} }
@Override
public void onEnable(Query query) {
var select = Query.select()
.table("roles")
.resultActionAfterQuery(srs -> {
srs.forEach(resultSet -> {
var roleIdBytes = resultSet.getBytes("id");
var roleName = resultSet.getString("name");
var roleId = new ULID(roleIdBytes);
roles.put(roleId, new Role(roleId, roleName));
}, () -> logger.warn("Error loading roles: No roles found"), e -> logger.error("Error loading roles", e));
logger.success("Loaded " + roles.size() + " roles!");
});
query.query(select);
select = Query.select()
.table("role_permissions")
.resultActionAfterQuery(srs -> {
srs.forEach(resultSet -> {
var roleIdBytes = resultSet.getBytes("roleId");
var permissionIdBytes = resultSet.getBytes("permissionId");
var roleId = new ULID(roleIdBytes);
var permissionId = new ULID(permissionIdBytes);
Role role = roles.get(roleId);
if (role == null) {
logger.warn("Role not found for permission: " + permissionId);
return;
}
Permission permission = backend.permissionProvider().permission(permissionId);
if (permission == null) {
logger.warn("Permission not found for role: " + roleId);
return;
}
role.permissions().add(permission);
}, () -> logger.warn("Error loading role permissions: No role permissions found"), e -> logger.error("Error loading role permissions", e));
});
query.query(select);
}
public Role role(ULID id) {
return roles.get(id);
}
public HashMap<ULID, Role> roles() {
return roles;
}
public Role role(String name) {
return roles.values().stream().filter(role -> role.name().equalsIgnoreCase(name)).findFirst().orElse(null);
}
} }
@@ -0,0 +1,16 @@
package dev.coph.flightscore.backend.utils;
import java.security.SecureRandom;
import java.util.Base64;
public class TokenGenerator {
private static final SecureRandom secureRandom = new SecureRandom();
private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder().withoutPadding();
public static String generateRefreshToken() {
byte[] randomBytes = new byte[64];
secureRandom.nextBytes(randomBytes);
return base64Encoder.encodeToString(randomBytes);
}
}