Added login flow
This commit is contained in:
@@ -83,6 +83,9 @@ public class Backend {
|
||||
providerManager.createAllDatabaseTables();
|
||||
logger.success("Database tables created!");
|
||||
|
||||
logger.info("Enabling providers...");
|
||||
providerManager.enableAllProviders();
|
||||
logger.success("Providers enabled!");
|
||||
|
||||
logger.info("Starting web server...");
|
||||
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;
|
||||
|
||||
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")
|
||||
private String jwt_secret;
|
||||
|
||||
@DefaultValue("15")
|
||||
private Long jwt_expirationInMinutes;
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import dev.coph.simplesql.query.Query;
|
||||
|
||||
public interface Provider {
|
||||
int priority();
|
||||
|
||||
String key();
|
||||
public void createDatabaseTables(Query query);
|
||||
|
||||
void createDatabaseTables(Query query);
|
||||
|
||||
void onEnable(Query query);
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ public class ProviderManager {
|
||||
public void createAllDatabaseTables() {
|
||||
Query query = new Query(backend.databaseAdapter());
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,4 +16,20 @@ public class User {
|
||||
private String phoneNumber;
|
||||
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.action.result.LoginActionResult;
|
||||
import dev.coph.flightscore.backend.config.Config;
|
||||
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.simplesql.database.Column;
|
||||
import dev.coph.simplesql.database.attributes.ColumnType;
|
||||
import dev.coph.simplesql.database.attributes.CreateMethod;
|
||||
import dev.coph.simplesql.database.attributes.DataType;
|
||||
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.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Slf4j
|
||||
public class UserProvider implements Provider {
|
||||
private Logger logger = Logger.of("UserProvider");
|
||||
private final Backend backend;
|
||||
|
||||
private byte[] JWT_SECRET;
|
||||
private long JWT_EXPIRATION_TIME;
|
||||
|
||||
public UserProvider(Backend backend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
@@ -39,14 +54,15 @@ public class UserProvider implements Provider {
|
||||
.column("id", DataType.BINARY, 26, true)
|
||||
.column("fistname", 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("password", DataType.VARCHAR, 255, true)
|
||||
.column("role", DataType.BINARY, 26)
|
||||
.primaryKey(List.of("id"))
|
||||
.index(List.of("email"))
|
||||
.index(List.of("email", "password"))
|
||||
.foreignKey(List.of("role"), "roles", List.of("id"));;
|
||||
.foreignKey(List.of("role"), "roles", List.of("id"));
|
||||
;
|
||||
query.query(tableCreate);
|
||||
tableCreate = Query.tableCreate()
|
||||
.createMethod(CreateMethod.IF_NOT_EXISTS)
|
||||
@@ -57,12 +73,124 @@ public class UserProvider implements Provider {
|
||||
.column(new Column("revoked", DataType.BOOLEAN, true).defaultValue(false))
|
||||
.primaryKey(List.of("token_hash"))
|
||||
.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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import lombok.experimental.Accessors;
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
public class Permission {
|
||||
public Permission(ULID id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private ULID id;
|
||||
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.provider.Provider;
|
||||
import dev.coph.simplelogger.Logger;
|
||||
import dev.coph.simplesql.database.attributes.CreateMethod;
|
||||
import dev.coph.simplesql.database.attributes.DataType;
|
||||
import dev.coph.simplesql.query.Query;
|
||||
import dev.coph.simpleutilities.ulid.ULID;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class PermissionProvider implements Provider {
|
||||
private final Backend backend;
|
||||
private final Logger logger = Logger.of("PermissionProvider");
|
||||
private final HashMap<ULID, Permission> permissions = new HashMap<>();
|
||||
|
||||
public PermissionProvider(Backend backend) {
|
||||
this.backend = backend;
|
||||
@@ -36,5 +41,32 @@ public class PermissionProvider implements Provider {
|
||||
;
|
||||
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)
|
||||
public class Role {
|
||||
|
||||
public Role(ULID id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private ULID id;
|
||||
private String name;
|
||||
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.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.DataType;
|
||||
import dev.coph.simplesql.query.Query;
|
||||
import dev.coph.simpleutilities.ulid.ULID;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class RoleProvider implements Provider {
|
||||
private final Logger logger = Logger.of("RoleProvider");
|
||||
private HashMap<ULID, Role> roles = new HashMap<>();
|
||||
|
||||
private final Backend backend;
|
||||
|
||||
public RoleProvider(Backend backend) {
|
||||
@@ -45,5 +52,53 @@ public class RoleProvider implements Provider {
|
||||
.foreignKey(List.of("permissionId"), "permissions", List.of("id"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user