diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs index 7777ef74..154db613 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -295,6 +295,7 @@ fn create_ldk_settings( fake_wallet: None, grpc_processor: None, database: cdk_mintd::config::Database::default(), + auth_database: None, mint_management_rpc: None, prometheus: None, auth: None, diff --git a/crates/cdk-integration-tests/src/shared.rs b/crates/cdk-integration-tests/src/shared.rs index 4ad50bf1..fe9d0afe 100644 --- a/crates/cdk-integration-tests/src/shared.rs +++ b/crates/cdk-integration-tests/src/shared.rs @@ -219,6 +219,7 @@ pub fn create_fake_wallet_settings( engine: DatabaseEngine::from_str(database).expect("valid database"), postgres: None, }, + auth_database: None, mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), @@ -268,6 +269,7 @@ pub fn create_cln_settings( fake_wallet: None, grpc_processor: None, database: cdk_mintd::config::Database::default(), + auth_database: None, mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), @@ -315,6 +317,7 @@ pub fn create_lnd_settings( fake_wallet: None, grpc_processor: None, database: cdk_mintd::config::Database::default(), + auth_database: None, mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 52898fb4..c6510539 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -74,6 +74,19 @@ max_connections = 20 # Connection timeout in seconds (optional, defaults to 10) connection_timeout_seconds = 10 +# Auth database configuration (optional, only used when auth is enabled) +[auth_database.postgres] +# PostgreSQL connection URL for authentication database +# Can also be set via CDK_MINTD_AUTH_POSTGRES_URL environment variable +# Environment variables take precedence over config file settings +url = "postgresql://user:password@localhost:5432/cdk_mint_auth" +# TLS mode: "disable", "prefer", "require" (optional, defaults to "disable") +tls_mode = "disable" +# Maximum number of connections in the pool (optional, defaults to 20) +max_connections = 20 +# Connection timeout in seconds (optional, defaults to 10) +connection_timeout_seconds = 10 + [ln] # Required ln backend `cln`, `lnd`, `fakewallet`, 'lnbits', 'ldknode' ln_backend = "fakewallet" diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index bb35edad..fa5bc6ff 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -367,6 +367,30 @@ pub struct Database { pub postgres: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AuthDatabase { + pub postgres: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostgresAuthConfig { + pub url: String, + pub tls_mode: Option, + pub max_connections: Option, + pub connection_timeout_seconds: Option, +} + +impl Default for PostgresAuthConfig { + fn default() -> Self { + Self { + url: String::new(), + tls_mode: Some("disable".to_string()), + max_connections: Some(20), + connection_timeout_seconds: Some(10), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PostgresConfig { pub url: String, @@ -457,6 +481,8 @@ pub struct Settings { pub fake_wallet: Option, pub grpc_processor: Option, pub database: Database, + #[cfg(feature = "auth")] + pub auth_database: Option, #[cfg(feature = "management-rpc")] pub mint_management_rpc: Option, pub auth: Option, diff --git a/crates/cdk-mintd/src/env_vars/database.rs b/crates/cdk-mintd/src/env_vars/database.rs index 5b9621c8..6b71ed7d 100644 --- a/crates/cdk-mintd/src/env_vars/database.rs +++ b/crates/cdk-mintd/src/env_vars/database.rs @@ -2,13 +2,19 @@ use std::env; -use crate::config::PostgresConfig; +use crate::config::{PostgresAuthConfig, PostgresConfig}; pub const ENV_POSTGRES_URL: &str = "CDK_MINTD_POSTGRES_URL"; pub const ENV_POSTGRES_TLS_MODE: &str = "CDK_MINTD_POSTGRES_TLS_MODE"; pub const ENV_POSTGRES_MAX_CONNECTIONS: &str = "CDK_MINTD_POSTGRES_MAX_CONNECTIONS"; pub const ENV_POSTGRES_CONNECTION_TIMEOUT: &str = "CDK_MINTD_POSTGRES_CONNECTION_TIMEOUT_SECONDS"; +pub const ENV_AUTH_POSTGRES_URL: &str = "CDK_MINTD_AUTH_POSTGRES_URL"; +pub const ENV_AUTH_POSTGRES_TLS_MODE: &str = "CDK_MINTD_AUTH_POSTGRES_TLS_MODE"; +pub const ENV_AUTH_POSTGRES_MAX_CONNECTIONS: &str = "CDK_MINTD_AUTH_POSTGRES_MAX_CONNECTIONS"; +pub const ENV_AUTH_POSTGRES_CONNECTION_TIMEOUT: &str = + "CDK_MINTD_AUTH_POSTGRES_CONNECTION_TIMEOUT_SECONDS"; + impl PostgresConfig { pub fn from_env(mut self) -> Self { // Check for new PostgreSQL URL env var first, then fallback to legacy DATABASE_URL @@ -38,3 +44,29 @@ impl PostgresConfig { self } } + +impl PostgresAuthConfig { + pub fn from_env(mut self) -> Self { + if let Ok(url) = env::var(ENV_AUTH_POSTGRES_URL) { + self.url = url; + } + + if let Ok(tls_mode) = env::var(ENV_AUTH_POSTGRES_TLS_MODE) { + self.tls_mode = Some(tls_mode); + } + + if let Ok(max_connections) = env::var(ENV_AUTH_POSTGRES_MAX_CONNECTIONS) { + if let Ok(parsed) = max_connections.parse::() { + self.max_connections = Some(parsed); + } + } + + if let Ok(timeout) = env::var(ENV_AUTH_POSTGRES_CONNECTION_TIMEOUT) { + if let Ok(parsed) = timeout.parse::() { + self.connection_timeout_seconds = Some(parsed); + } + } + + self + } +} diff --git a/crates/cdk-mintd/src/env_vars/mod.rs b/crates/cdk-mintd/src/env_vars/mod.rs index 61501aab..a3f4c597 100644 --- a/crates/cdk-mintd/src/env_vars/mod.rs +++ b/crates/cdk-mintd/src/env_vars/mod.rs @@ -75,6 +75,21 @@ impl Settings { ); } + // Parse auth database configuration from environment variables (when auth is enabled) + #[cfg(feature = "auth")] + { + self.auth_database = Some(crate::config::AuthDatabase { + postgres: Some( + self.auth_database + .clone() + .unwrap_or_default() + .postgres + .unwrap_or_default() + .from_env(), + ), + }); + } + self.info = self.info.clone().from_env(); self.mint_info = self.mint_info.clone().from_env(); self.ln = self.ln.clone().from_env(); diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 3bde4706..2a058e95 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -43,7 +43,7 @@ use cdk_common::database::DynMintDatabase; #[cfg(feature = "prometheus")] use cdk_common::payment::MetricsMintPayment; use cdk_common::payment::MintPayment; -#[cfg(feature = "auth")] +#[cfg(all(feature = "auth", feature = "postgres"))] use cdk_postgres::MintPgAuthDatabase; #[cfg(feature = "postgres")] use cdk_postgres::MintPgDatabase; @@ -663,16 +663,20 @@ async fn setup_authentication( DatabaseEngine::Postgres => { #[cfg(feature = "postgres")] { - // Get the PostgreSQL configuration, ensuring it exists - let pg_config = settings.database.postgres.as_ref().ok_or_else(|| { - anyhow!("PostgreSQL configuration is required when using PostgreSQL engine") + // Require dedicated auth database configuration - no fallback to main database + let auth_db_config = settings.auth_database.as_ref().ok_or_else(|| { + anyhow!("Auth database configuration is required when using PostgreSQL with authentication. Set [auth_database] section in config file or CDK_MINTD_AUTH_POSTGRES_URL environment variable") })?; - if pg_config.url.is_empty() { - bail!("PostgreSQL URL is required for auth database. Set it in config file [database.postgres] section or via CDK_MINTD_POSTGRES_URL/CDK_MINTD_DATABASE_URL environment variable"); + let auth_pg_config = auth_db_config.postgres.as_ref().ok_or_else(|| { + anyhow!("PostgreSQL auth database configuration is required when using PostgreSQL with authentication. Set [auth_database.postgres] section in config file or CDK_MINTD_AUTH_POSTGRES_URL environment variable") + })?; + + if auth_pg_config.url.is_empty() { + bail!("Auth database PostgreSQL URL is required and cannot be empty. Set it in config file [auth_database.postgres] section or via CDK_MINTD_AUTH_POSTGRES_URL environment variable"); } - Arc::new(MintPgAuthDatabase::new(pg_config.url.as_str()).await?) + Arc::new(MintPgAuthDatabase::new(auth_pg_config.url.as_str()).await?) } #[cfg(not(feature = "postgres"))] { @@ -1188,3 +1192,27 @@ pub async fn run_mintd_with_shutdown( ) .await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_postgres_auth_url_validation() { + // Test that the auth database config requires explicit configuration + + // Test empty URL + let auth_config = config::PostgresAuthConfig { + url: "".to_string(), + ..Default::default() + }; + assert!(auth_config.url.is_empty()); + + // Test non-empty URL + let auth_config = config::PostgresAuthConfig { + url: "postgresql://user:password@localhost:5432/auth_db".to_string(), + ..Default::default() + }; + assert!(!auth_config.url.is_empty()); + } +} diff --git a/crates/cdk-postgres/start_db_for_test.sh b/crates/cdk-postgres/start_db_for_test.sh index d2a95b82..7a53af47 100755 --- a/crates/cdk-postgres/start_db_for_test.sh +++ b/crates/cdk-postgres/start_db_for_test.sh @@ -27,4 +27,11 @@ docker exec -e PGPASSWORD="${DB_PASS}" "${CONTAINER_NAME}" \ docker exec -e PGPASSWORD="${DB_PASS}" "${CONTAINER_NAME}" \ psql -U "${DB_USER}" -d "${DB_NAME}" -c "CREATE DATABASE mintdb_auth;" +# Export environment variables for both main and auth databases export DATABASE_URL="host=localhost user=${DB_USER} password=${DB_PASS} dbname=${DB_NAME} port=${DB_PORT}" +export CDK_MINTD_POSTGRES_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/mintdb" +export CDK_MINTD_AUTH_POSTGRES_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/mintdb_auth" + +echo "Database URLs configured:" +echo "Main database: ${CDK_MINTD_POSTGRES_URL}" +echo "Auth database: ${CDK_MINTD_AUTH_POSTGRES_URL}" diff --git a/crates/cdk-sql-common/src/mint/auth/mod.rs b/crates/cdk-sql-common/src/mint/auth/mod.rs index f5ca4356..cd573956 100644 --- a/crates/cdk-sql-common/src/mint/auth/mod.rs +++ b/crates/cdk-sql-common/src/mint/auth/mod.rs @@ -198,9 +198,11 @@ where for (endpoint, auth) in protected_endpoints.iter() { if let Err(err) = query( r#" - INSERT OR REPLACE INTO protected_endpoints + INSERT INTO protected_endpoints (endpoint, auth) - VALUES (:endpoint, :auth); + VALUES (:endpoint, :auth) + ON CONFLICT (endpoint) DO UPDATE SET + auth = EXCLUDED.auth; "#, )? .bind("endpoint", serde_json::to_string(endpoint)?)