use crate::error::AppError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub use vti_common::config::{
AuthConfig, LogConfig, LogFormat, MessagingConfig, StoreConfig,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AppConfig {
pub vtc_did: Option<String>,
pub vta_did: Option<String>,
#[serde(alias = "community_name")]
pub vtc_name: Option<String>,
#[serde(alias = "community_description")]
pub vtc_description: Option<String>,
pub public_url: Option<String>,
#[serde(default = "default_server_config")]
pub server: ServerConfig,
#[serde(default)]
pub log: LogConfig,
#[serde(default = "default_store_config")]
pub store: StoreConfig,
pub messaging: Option<MessagingConfig>,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub secrets: SecretsConfig,
#[serde(skip)]
pub config_path: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SecretsConfig {
pub secret: Option<String>,
pub aws_secret_name: Option<String>,
pub aws_region: Option<String>,
pub gcp_project: Option<String>,
pub gcp_secret_name: Option<String>,
pub azure_vault_url: Option<String>,
pub azure_secret_name: Option<String>,
#[serde(default = "default_keyring_service")]
pub keyring_service: String,
}
fn default_keyring_service() -> String {
"vtc".to_string()
}
impl Default for SecretsConfig {
fn default() -> Self {
Self {
secret: None,
aws_secret_name: None,
aws_region: None,
gcp_project: None,
gcp_secret_name: None,
azure_vault_url: None,
azure_secret_name: None,
keyring_service: default_keyring_service(),
}
}
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
8200
}
fn default_server_config() -> ServerConfig {
ServerConfig::default()
}
fn default_store_config() -> StoreConfig {
StoreConfig {
data_dir: PathBuf::from("data/vtc"),
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
}
}
}
impl AppConfig {
pub fn load(config_path: Option<PathBuf>) -> Result<Self, AppError> {
let path = config_path
.or_else(|| std::env::var("VTC_CONFIG_PATH").ok().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("config.toml"));
if !path.exists() {
return Err(AppError::Config(format!(
"configuration file not found: {}",
path.display()
)));
}
let contents = std::fs::read_to_string(&path).map_err(AppError::Io)?;
let mut config = toml::from_str::<AppConfig>(&contents)
.map_err(|e| AppError::Config(format!("failed to parse {}: {e}", path.display())))?;
config.config_path = path.clone();
if let Ok(vtc_did) = std::env::var("VTC_DID") {
config.vtc_did = Some(vtc_did);
}
if let Ok(vta_did) = std::env::var("VTC_VTA_DID") {
config.vta_did = Some(vta_did);
}
if let Ok(host) = std::env::var("VTC_SERVER_HOST") {
config.server.host = host;
}
if let Ok(port) = std::env::var("VTC_SERVER_PORT") {
config.server.port = port
.parse()
.map_err(|e| AppError::Config(format!("invalid VTC_SERVER_PORT: {e}")))?;
}
if let Ok(level) = std::env::var("VTC_LOG_LEVEL") {
config.log.level = level;
}
if let Ok(format) = std::env::var("VTC_LOG_FORMAT") {
config.log.format = match format.to_lowercase().as_str() {
"json" => LogFormat::Json,
"text" => LogFormat::Text,
other => {
return Err(AppError::Config(format!(
"invalid VTC_LOG_FORMAT '{other}', expected 'text' or 'json'"
)));
}
};
}
if let Ok(public_url) = std::env::var("VTC_PUBLIC_URL") {
config.public_url = Some(public_url);
}
if let Ok(data_dir) = std::env::var("VTC_STORE_DATA_DIR") {
config.store.data_dir = PathBuf::from(data_dir);
}
match (
std::env::var("VTC_MESSAGING_MEDIATOR_URL"),
std::env::var("VTC_MESSAGING_MEDIATOR_DID"),
) {
(Ok(url), Ok(did)) => {
config.messaging = Some(MessagingConfig {
mediator_url: url,
mediator_did: did,
mediator_host: None,
});
}
(Ok(url), Err(_)) => {
let messaging = config.messaging.get_or_insert(MessagingConfig {
mediator_url: String::new(),
mediator_did: String::new(),
mediator_host: None,
});
messaging.mediator_url = url;
}
(Err(_), Ok(did)) => {
let messaging = config.messaging.get_or_insert(MessagingConfig {
mediator_url: String::new(),
mediator_did: String::new(),
mediator_host: None,
});
messaging.mediator_did = did;
}
(Err(_), Err(_)) => {}
}
if let Ok(secret) = std::env::var("VTC_SECRETS_SECRET") {
config.secrets.secret = Some(secret);
}
if let Ok(name) = std::env::var("VTC_SECRETS_AWS_SECRET_NAME") {
config.secrets.aws_secret_name = Some(name);
}
if let Ok(region) = std::env::var("VTC_SECRETS_AWS_REGION") {
config.secrets.aws_region = Some(region);
}
if let Ok(project) = std::env::var("VTC_SECRETS_GCP_PROJECT") {
config.secrets.gcp_project = Some(project);
}
if let Ok(name) = std::env::var("VTC_SECRETS_GCP_SECRET_NAME") {
config.secrets.gcp_secret_name = Some(name);
}
if let Ok(url) = std::env::var("VTC_SECRETS_AZURE_VAULT_URL") {
config.secrets.azure_vault_url = Some(url);
}
if let Ok(name) = std::env::var("VTC_SECRETS_AZURE_SECRET_NAME") {
config.secrets.azure_secret_name = Some(name);
}
if let Ok(service) = std::env::var("VTC_SECRETS_KEYRING_SERVICE") {
config.secrets.keyring_service = service;
}
if let Ok(expiry) = std::env::var("VTC_AUTH_ACCESS_EXPIRY") {
config.auth.access_token_expiry = expiry
.parse()
.map_err(|e| AppError::Config(format!("invalid VTC_AUTH_ACCESS_EXPIRY: {e}")))?;
}
if let Ok(expiry) = std::env::var("VTC_AUTH_REFRESH_EXPIRY") {
config.auth.refresh_token_expiry = expiry
.parse()
.map_err(|e| AppError::Config(format!("invalid VTC_AUTH_REFRESH_EXPIRY: {e}")))?;
}
if let Ok(ttl) = std::env::var("VTC_AUTH_CHALLENGE_TTL") {
config.auth.challenge_ttl = ttl
.parse()
.map_err(|e| AppError::Config(format!("invalid VTC_AUTH_CHALLENGE_TTL: {e}")))?;
}
if let Ok(interval) = std::env::var("VTC_AUTH_SESSION_CLEANUP_INTERVAL") {
config.auth.session_cleanup_interval = interval.parse().map_err(|e| {
AppError::Config(format!("invalid VTC_AUTH_SESSION_CLEANUP_INTERVAL: {e}"))
})?;
}
if let Ok(key) = std::env::var("VTC_AUTH_JWT_SIGNING_KEY") {
config.auth.jwt_signing_key = Some(key);
}
Ok(config)
}
pub fn save(&self) -> Result<(), AppError> {
let contents = toml::to_string_pretty(self)
.map_err(|e| AppError::Config(format!("failed to serialize config: {e}")))?;
std::fs::write(&self.config_path, contents).map_err(AppError::Io)?;
Ok(())
}
}