mod builder;
pub use builder::ConfigBuilder;
#[cfg(feature = "admin-override")]
pub use crate::auth::admin_override::AdminOverrideConfig;
use std::env;
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
pub host: String,
pub user: String,
pub password: String,
pub name: String,
pub port: u16,
}
impl DatabaseConfig {
pub fn url(&self) -> String {
format!(
"mysql://{}:{}@{}:{}/{}",
self.user, self.password, self.host, self.port, self.name
)
}
pub fn postgres_url(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.user, self.password, self.host, self.port, self.name
)
}
pub fn url_for_tenant(&self, tenant: &str) -> String {
format!(
"mysql://{}:{}@{}:{}/{}",
self.user, self.password, self.host, self.port, tenant
)
}
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
host: "localhost".to_string(),
user: "root".to_string(),
password: String::new(),
name: "brylix".to_string(),
port: 3306,
}
}
}
#[derive(Debug, Clone)]
pub struct MultiTenantConfig {
pub enabled: bool,
pub required_db_version: i32,
pub db_password: Option<String>,
}
impl Default for MultiTenantConfig {
fn default() -> Self {
Self {
enabled: false,
required_db_version: 1,
db_password: None,
}
}
}
#[derive(Debug, Clone)]
pub struct JwtConfig {
pub secret: String,
pub exp_days: i64,
}
impl Default for JwtConfig {
fn default() -> Self {
Self {
secret: String::new(),
exp_days: 7,
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub database: DatabaseConfig,
pub jwt: JwtConfig,
pub multi_tenant: MultiTenantConfig,
pub log_level: String,
#[cfg(feature = "admin-override")]
pub admin_override: Option<AdminOverrideConfig>,
}
impl Config {
pub fn from_env() -> Result<Self, String> {
let database = DatabaseConfig {
host: env::var("DB_HOST").map_err(|_| "DB_HOST must be set")?,
user: env::var("DB_USER").map_err(|_| "DB_USER must be set")?,
password: env::var("DB_PASSWORD").map_err(|_| "DB_PASSWORD must be set")?,
name: env::var("DB_NAME").map_err(|_| "DB_NAME must be set")?,
port: env::var("DB_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3306),
};
let jwt = JwtConfig {
secret: env::var("JWT_SECRET").map_err(|_| "JWT_SECRET must be set")?,
exp_days: env::var("JWT_EXP_DAYS")
.map_err(|_| "JWT_EXP_DAYS must be set")?
.parse()
.map_err(|_| "JWT_EXP_DAYS must be a valid integer")?,
};
let multi_tenant = MultiTenantConfig {
enabled: env::var("MULTI_TENANT_MODE")
.map(|v| v == "true")
.unwrap_or(false),
required_db_version: env::var("REQUIRED_DB_VERSION")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1),
db_password: env::var("TENANT_DB_PASSWORD").ok(),
};
let log_level = env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
#[cfg(feature = "admin-override")]
let admin_override = env::var("ADMIN_JWT_SECRET").ok().map(|secret| {
let expiry_secs = env::var("ADMIN_OVERRIDE_EXPIRY_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(60);
AdminOverrideConfig::new(secret).with_expiry_secs(expiry_secs)
});
Ok(Config {
database,
jwt,
multi_tenant,
log_level,
#[cfg(feature = "admin-override")]
admin_override,
})
}
pub fn init() -> Result<&'static Config, String> {
if let Some(config) = CONFIG.get() {
return Ok(config);
}
tracing::info!("Loading application configuration");
let config = Config::from_env()?;
if CONFIG.set(config).is_err() {
tracing::debug!("Config already initialized by another thread (race condition)");
}
CONFIG
.get()
.ok_or_else(|| "Failed to initialize config".to_string())
}
pub fn init_with(config: Config) -> Result<&'static Config, String> {
if CONFIG.set(config).is_err() {
tracing::debug!("Config already initialized");
}
CONFIG
.get()
.ok_or_else(|| "Failed to initialize config".to_string())
}
pub fn get() -> &'static Config {
CONFIG
.get()
.expect("Config not initialized. Call Config::init() first.")
}
pub fn try_get() -> Option<&'static Config> {
CONFIG.get()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_database_url() {
let db = DatabaseConfig {
host: "localhost".to_string(),
user: "root".to_string(),
password: "secret".to_string(),
name: "testdb".to_string(),
port: 3306,
};
assert_eq!(db.url(), "mysql://root:secret@localhost:3306/testdb");
}
#[test]
fn test_postgres_url() {
let db = DatabaseConfig {
host: "localhost".to_string(),
user: "root".to_string(),
password: "secret".to_string(),
name: "testdb".to_string(),
port: 5432,
};
assert_eq!(
db.postgres_url(),
"postgres://root:secret@localhost:5432/testdb"
);
}
}