//! Portal Server CLI //! //! Command-line interface for running the portal server with configurable options. use clap::Parser; use portal_server::{PortalServerBuilder, ServerConfig}; use tracing::{info, error}; use anyhow::Result; #[derive(Parser)] #[command(name = "portal-server")] #[command(about = "Portal Server for KYC verification and payment processing")] #[command(version = "0.1.0")] struct Cli { /// Server host address #[arg(long, default_value = "127.0.0.1")] host: String, /// Server port #[arg(short, long, default_value = "3001")] port: u16, /// Stripe secret key #[arg(long, env)] stripe_secret_key: Option, /// Stripe publishable key #[arg(long, env)] stripe_publishable_key: Option, /// Stripe webhook secret #[arg(long, env)] stripe_webhook_secret: Option, /// Identify API key for KYC verification #[arg(long, env)] identify_api_key: Option, /// Identify webhook secret for signature verification #[arg(long, env)] identify_webhook_secret: Option, /// API keys for authentication (comma-separated) #[arg(long, env)] api_keys: Option, /// Identify API URL #[arg(long, env, default_value = "https://api.identify.com")] identify_api_url: String, /// CORS allowed origins (comma-separated) #[arg(long, env, default_value = "*")] cors_origins: String, /// Directory to serve static files from #[arg(long)] static_dir: Option, /// Load configuration from environment variables #[arg(long)] from_env: bool, /// Path to .env file (defaults to .env in current directory) #[arg(long)] env_file: Option, /// Enable verbose logging #[arg(short, long)] verbose: bool, } fn load_env_file(cli: &Cli) -> Result<()> { use std::path::Path; if let Some(env_file_path) = &cli.env_file { // Use the specified .env file path info!("Loading .env file from: {}", env_file_path); if Path::new(env_file_path).exists() { dotenv::from_path(env_file_path) .map_err(|e| anyhow::anyhow!("Failed to load .env file from {}: {}", env_file_path, e))?; info!("Successfully loaded .env file from: {}", env_file_path); } else { return Err(anyhow::anyhow!("Specified .env file not found: {}", env_file_path)); } } else { // Try default locations in order of preference let default_paths = [ ".env", // Current directory "portal-server/.env", // portal-server subdirectory ]; let mut loaded = false; for path in &default_paths { if Path::new(path).exists() { info!("Loading .env file from: {}", path); dotenv::from_path(path) .map_err(|e| anyhow::anyhow!("Failed to load .env file from {}: {}", path, e))?; info!("Successfully loaded .env file from: {}", path); loaded = true; break; } } if !loaded { info!("No .env file found in default locations. Using environment variables and CLI arguments only."); } } Ok(()) } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); // Initialize tracing if cli.verbose { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); } else { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); } info!("Starting Portal Server..."); // Load .env file if specified or use default locations load_env_file(&cli)?; // Build configuration let config = if cli.from_env { info!("Loading configuration from environment variables"); ServerConfig::from_env()? } else { info!("Using configuration from command line arguments"); build_config_from_cli(&cli)? }; // Log configuration (without sensitive data) info!("Server configuration:"); info!(" Host: {}", config.host); info!(" Port: {}", config.port); info!(" Identify API URL: {}", config.identify_api_url); info!(" CORS Origins: {:?}", config.cors_origins); info!(" Stripe configured: {}", !config.stripe_secret_key.is_empty()); info!(" Identify configured: {}", !config.identify_api_key.is_empty()); // Build server let mut builder = PortalServerBuilder::new(config); // Add static file serving if specified if let Some(static_dir) = cli.static_dir { builder = builder.with_static_dir(static_dir); } let server = builder.build().await?; // Run server if let Err(e) = server.run().await { error!("Server error: {}", e); std::process::exit(1); } Ok(()) } fn build_config_from_cli(cli: &Cli) -> Result { let stripe_secret_key = cli.stripe_secret_key .clone() .or_else(|| std::env::var("STRIPE_SECRET_KEY").ok()) .ok_or_else(|| anyhow::anyhow!("Stripe secret key is required. Use --stripe-secret-key or set STRIPE_SECRET_KEY environment variable"))?; let stripe_publishable_key = cli.stripe_publishable_key .clone() .or_else(|| std::env::var("STRIPE_PUBLISHABLE_KEY").ok()) .ok_or_else(|| anyhow::anyhow!("Stripe publishable key is required. Use --stripe-publishable-key or set STRIPE_PUBLISHABLE_KEY environment variable"))?; let identify_api_key = cli.identify_api_key .clone() .or_else(|| std::env::var("IDENTIFY_API_KEY").ok()) .ok_or_else(|| anyhow::anyhow!("Identify API key is required. Use --identify-api-key or set IDENTIFY_API_KEY environment variable"))?; let cors_origins = cli.cors_origins .split(',') .map(|s| s.trim().to_string()) .collect(); let api_keys = cli.api_keys .clone() .or_else(|| std::env::var("API_KEYS").ok()) .map(|keys| keys.split(',').map(|s| s.trim().to_string()).collect()) .unwrap_or_default(); Ok(ServerConfig { host: cli.host.clone(), port: cli.port, stripe_secret_key, stripe_publishable_key, stripe_webhook_secret: cli.stripe_webhook_secret.clone(), identify_api_key, identify_webhook_secret: cli.identify_webhook_secret.clone(), identify_api_url: cli.identify_api_url.clone(), cors_origins, api_keys, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_cli_parsing() { let cli = Cli::parse_from(&[ "portal-server", "--host", "0.0.0.0", "--port", "8080", "--stripe-secret-key", "sk_test_123", "--stripe-publishable-key", "pk_test_123", "--identify-api-key", "identify_123", "--verbose", ]); assert_eq!(cli.host, "0.0.0.0"); assert_eq!(cli.port, 8080); assert_eq!(cli.stripe_secret_key, Some("sk_test_123".to_string())); assert_eq!(cli.stripe_publishable_key, Some("pk_test_123".to_string())); assert_eq!(cli.identify_api_key, Some("identify_123".to_string())); assert!(cli.verbose); } #[test] fn test_config_from_cli() { let cli = Cli { host: "localhost".to_string(), port: 3000, stripe_secret_key: Some("sk_test_123".to_string()), stripe_publishable_key: Some("pk_test_123".to_string()), stripe_webhook_secret: None, identify_api_key: Some("identify_123".to_string()), identify_webhook_secret: None, api_keys: None, identify_api_url: "https://api.identify.com".to_string(), cors_origins: "*".to_string(), static_dir: None, from_env: false, env_file: None, verbose: false, }; let config = build_config_from_cli(&cli).unwrap(); assert_eq!(config.host, "localhost"); assert_eq!(config.port, 3000); assert_eq!(config.stripe_secret_key, "sk_test_123"); assert_eq!(config.identify_api_key, "identify_123"); } }