refactor: structure of an hexagonal architecture

Created a kernel crate to store models and future action implementations.
Will be useful to create admin cli.
This commit is contained in:
Matthieu Bessat 2024-11-28 12:47:00 +01:00
parent 69af48bb62
commit 3713cc2443
87 changed files with 834 additions and 474 deletions

22
lib/kernel/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "kernel"
edition = "2021"
[dependencies]
utils = { path = "../utils" }
log = { workspace = true }
env_logger = { workspace = true }
anyhow = { workspace = true }
fully_pub = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
toml = { workspace = true }
sqlx = { workspace = true }
dotenvy = { workspace = true }
uuid = { workspace = true }
url = { workspace = true }

View file

View file

4
lib/kernel/src/consts.rs Normal file
View file

@ -0,0 +1,4 @@
pub const DEFAULT_DB_PATH: &str = "/var/lib/minauthator/minauthator.db";
pub const DEFAULT_ASSETS_PATH: &str = "/usr/local/lib/minauthator/assets";
pub const DEFAULT_CONFIG_PATH: &str = "/etc/minauthator/config.yaml";

51
lib/kernel/src/context.rs Normal file
View file

@ -0,0 +1,51 @@
use std::{env, fs};
use anyhow::{Result, Context, anyhow};
use fully_pub::fully_pub;
use log::info;
use sqlx::{Pool, Sqlite};
use crate::{
consts::{DEFAULT_CONFIG_PATH, DEFAULT_DB_PATH}, database::prepare_database, models::config::Config, repositories::storage::Storage
};
/// get server config
fn get_config(path: String) -> Result<Config> {
let inp_def_yaml = fs::read_to_string(path)
.expect("Should have been able to read the the config file");
toml::from_str(&inp_def_yaml)
.map_err(|e| anyhow!("Failed to parse config, {:?}", e))
}
#[fully_pub]
struct StartKernelConfig {
config_path: Option<String>,
database_path: Option<String>,
}
#[derive(Debug, Clone)]
#[fully_pub]
struct AppSecrets {
jwt_secret: String
}
pub async fn get_kernel_context(start_config: StartKernelConfig) -> Result<(Config, AppSecrets, Storage)> {
env_logger::init();
let _ = dotenvy::dotenv();
let database_path = &start_config.database_path.unwrap_or(DEFAULT_DB_PATH.to_string());
info!("Using database file at {}", database_path);
let storage = prepare_database(database_path).await.context("Could not prepare db.")?;
let config_path = start_config.config_path.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
info!("Using config file at {}", &config_path);
let config: Config = get_config(config_path)
.expect("Cannot get config.");
dotenvy::dotenv().context("loading .env")?;
let secrets = AppSecrets {
jwt_secret: env::var("APP_JWT_SECRET").context("Expecting APP_JWT_SECRET env var.")?
};
Ok((config, secrets, storage))
}

View file

@ -0,0 +1,21 @@
use anyhow::{Context, Result};
use sqlx::{sqlite::{SqliteConnectOptions, SqlitePoolOptions}, ConnectOptions};
use std::str::FromStr;
use crate::repositories::storage::Storage;
pub async fn prepare_database(sqlite_db_path: &str) -> Result<Storage> {
let conn_str = format!("sqlite:{}", sqlite_db_path);
let pool = SqlitePoolOptions::new()
.max_connections(50)
.connect_with(
SqliteConnectOptions::from_str(&conn_str)?
.log_statements(log::LevelFilter::Trace)
)
.await
.context("could not connect to database_url")?;
Ok(Storage(pool))
}

7
lib/kernel/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod models;
pub mod database;
pub mod consts;
pub mod context;
pub mod actions;
pub mod repositories;

View file

@ -0,0 +1,33 @@
use fully_pub::fully_pub;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::types::Json;
use strum_macros::{Display, EnumIter, EnumString};
#[derive(
Clone, Debug, Serialize, Deserialize, PartialEq,
sqlx::Type,
Display, EnumString, EnumIter
)]
#[strum(serialize_all = "snake_case")]
pub enum AuthorizationScope {
UserReadBasic,
UserReadRoles
}
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
#[fully_pub]
struct Authorization {
/// uuid
id: String,
user_id: String,
/// app_id
client_id: String,
scopes: Json<Vec<AuthorizationScope>>,
/// defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
code: String,
last_used_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc>
}

View file

@ -0,0 +1,76 @@
use fully_pub::fully_pub;
use serde::{Deserialize, Serialize};
const fn _default_true() -> bool { true }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[fully_pub]
/// Instance branding/customization config
struct InstanceConfig {
base_uri: String,
name: String,
logo_uri: Option<String>
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[fully_pub]
enum AppAuthorizeFlow {
/// user must grant the app
Explicit,
/// authorized by default for all scopes
Implicit
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[fully_pub]
enum AppVisibility {
/// app is public (visible to non-signed in user), useful for app discovery
Public,
/// app is visible to all signed-in users
Internal,
/// app will be only visible when the user reach the login endpoint
Private
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[fully_pub]
struct Application {
slug: String,
name: String,
description: String,
logo_uri: Option<String>,
client_id: String,
client_secret: String,
allowed_redirect_uris: Vec<String>,
authorize_flow: AppAuthorizeFlow,
visibility: AppVisibility,
login_uri: String
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[fully_pub]
struct Role {
slug: String,
name: String,
description: Option<String>,
#[serde(default = "_default_true")]
default: bool
}
// todo: Role hierarchy https://en.wikipedia.org/wiki/Role_hierarchy
#[derive(Debug, Clone, Serialize, Deserialize)]
#[fully_pub]
/// Configuration of this Minauthator instance
struct Config {
instance: InstanceConfig,
applications: Vec<Application>,
roles: Vec<Role>
}
#[derive(Debug, Clone)]
#[fully_pub]
struct AppSecrets {
jwt_secret: String
}

View file

@ -0,0 +1,3 @@
pub mod config;
pub mod user;
pub mod authorization;

View file

@ -0,0 +1,31 @@
use fully_pub::fully_pub;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::types::Json;
#[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(strum_macros::Display)]
#[fully_pub]
enum UserStatus {
Active,
Disabled
}
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
#[fully_pub]
struct User {
/// uuid
id: String,
handle: String,
full_name: Option<String>,
email: Option<String>,
website: Option<String>,
picture: Option<Vec<u8>>, // embeded blob to store profile pic
password_hash: Option<String>, // argon2 password hash
status: UserStatus,
roles: Json<Vec<String>>,
activation_token: Option<String>,
last_login_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc>
}

View file

@ -0,0 +1,2 @@
pub mod storage;
pub mod users;

View file

@ -0,0 +1,7 @@
use fully_pub::fully_pub;
use sqlx::{Pool, Sqlite};
/// storage interface
#[fully_pub]
#[derive(Clone, Debug)]
struct Storage(Pool<Sqlite>);

View file

@ -0,0 +1,14 @@
// user repositories
use crate::models::user::User;
use super::storage::Storage;
use anyhow::{Result, Context};
async fn get_user_by_id(storage: &Storage, user_id: &str) -> Result<User> {
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(&storage.0)
.await
.context("To get user from claim")
}