use crate::config::setup::initialize_config_dir;
use std::io::{self, Write};
pub struct SetupWizard;
impl SetupWizard {
pub fn run_for_profile(profile_name: &str) -> Result<(), Box<dyn std::error::Error>> {
if let Err(validation_error) = crate::config::validate_profile_name(profile_name) {
return Err(format!("Invalid profile name: {validation_error}").into());
}
println!("🎯 Welcome to Quetty Setup Wizard!");
if profile_name == "default" {
println!("This will help you create your initial configuration.\n");
} else {
println!("Setting up profile: {profile_name}\n");
}
let config_dir = initialize_config_dir()?;
let profile_dir = config_dir.join("profiles").join(profile_name);
let env_path = profile_dir.join(".env");
if env_path.exists() {
print!(
"Profile '{}' configuration already exists at: {}\nDo you want to update it? (y/N): ",
profile_name,
env_path.display()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("Setup cancelled. Existing profile '{profile_name}' preserved.");
return Ok(());
}
}
println!(
"✓ Configuration directory created: {}",
config_dir.display()
);
let auth_method = Self::prompt_auth_method()?;
let connection_string = if auth_method == "connection_string" {
Some(Self::prompt_connection_string()?)
} else {
None
};
std::fs::create_dir_all(&profile_dir)?;
let env_path = profile_dir.join(".env");
let env_content = Self::generate_env_content(&auth_method, connection_string.as_deref())?;
std::fs::write(&env_path, env_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&env_path, permissions)?;
}
println!("\n✅ Setup complete!");
println!(
"📁 Profile '{}' configuration saved to: {}",
profile_name,
env_path.display()
);
println!("🔧 Configuration uses embedded defaults with your custom authentication.");
if auth_method == "client_secret" {
println!("\n⚠️ Important: Add your Azure AD credentials to the .env file:");
println!(" AZURE_AD__TENANT_ID=your-tenant-id");
println!(" AZURE_AD__CLIENT_ID=your-client-id");
println!(" AZURE_AD__CLIENT_SECRET=your-client-secret");
} else if auth_method == "device_code" {
println!(
"\n📝 Note: Device code authentication will prompt you to sign in when you start the app."
);
} else if auth_method == "connection_string" {
println!("\n✨ Connection string configured successfully!");
println!("🔗 Your Service Bus connection is ready to use.");
println!("💡 No additional authentication setup required.");
}
println!("\n🚀 Run 'quetty' to start the application!");
crate::config::invalidate_profile_cache();
Ok(())
}
fn prompt_auth_method() -> Result<String, Box<dyn std::error::Error>> {
println!("Choose your authentication method:");
println!("1. Device Code Flow (recommended for development)");
println!(" - Interactive browser-based authentication");
println!(" - No client secret required");
println!(" - Great for personal use");
println!();
println!("2. Client Secret Flow (recommended for automation)");
println!(" - Service principal authentication");
println!(" - Requires client secret");
println!(" - Best for CI/CD and automated scripts");
println!();
println!("3. Connection String (fastest setup)");
println!(" - Direct Service Bus connection");
println!(" - No Azure AD setup required");
println!(" - Get from Azure Portal → Service Bus → Shared access policies");
println!();
loop {
print!("Select option (1, 2, or 3): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim() {
"1" => return Ok("device_code".to_string()),
"2" => return Ok("client_secret".to_string()),
"3" => return Ok("connection_string".to_string()),
_ => println!("Please enter 1, 2, or 3."),
}
}
}
fn prompt_connection_string() -> Result<String, Box<dyn std::error::Error>> {
println!("\n📋 Enter your Service Bus connection string:");
println!("💡 You can find this in the Azure Portal:");
println!(" 1. Go to your Service Bus namespace");
println!(" 2. Select 'Shared access policies'");
println!(" 3. Click on a policy (e.g., 'RootManageSharedAccessKey')");
println!(" 4. Copy the 'Primary Connection String'");
println!();
loop {
print!("Connection string: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let connection_string = input.trim().to_string();
if connection_string.is_empty() {
println!("Connection string cannot be empty. Please try again.");
continue;
}
if !connection_string.contains("Endpoint=sb://")
|| !connection_string.contains("SharedAccessKeyName=")
|| !connection_string.contains("SharedAccessKey=")
{
println!("⚠️ Invalid connection string format. It should contain:");
println!(" - Endpoint=sb://...");
println!(" - SharedAccessKeyName=...");
println!(" - SharedAccessKey=...");
println!("Please try again.");
continue;
}
return Ok(connection_string);
}
}
fn generate_env_content(
auth_method: &str,
connection_string: Option<&str>,
) -> Result<String, Box<dyn std::error::Error>> {
let mut env_content = String::new();
env_content.push_str("# Environment variables for default profile\n");
env_content.push_str("# SECRETS AND AUTHENTICATION ONLY\n");
env_content.push_str(
"# For other settings, create config.toml or keys.toml in this directory\n\n",
);
if auth_method == "connection_string" {
env_content.push_str("# Connection string authentication\n");
if let Some(conn_str) = connection_string {
env_content.push_str(&format!("SERVICEBUS__CONNECTION_STRING={conn_str}\n\n"));
} else {
env_content.push_str("# SERVICEBUS__CONNECTION_STRING=your-connection-string\n\n");
}
} else {
env_content.push_str(&format!("# Authentication method: {auth_method}\n"));
env_content.push_str(&format!("AZURE_AD__AUTH_METHOD={auth_method}\n\n"));
if auth_method == "device_code" {
env_content.push_str("# Device code flow - no additional credentials needed\n");
env_content.push_str("# The app will prompt you to sign in interactively\n\n");
} else if auth_method == "client_secret" {
env_content.push_str("# Client secret flow - add your Azure AD credentials:\n");
env_content.push_str("# AZURE_AD__TENANT_ID=your-tenant-id\n");
env_content.push_str("# AZURE_AD__CLIENT_ID=your-client-id\n");
env_content.push_str("# AZURE_AD__CLIENT_SECRET=your-client-secret\n\n");
}
env_content.push_str("# Alternative: Service Bus connection string (if switching to connection string auth):\n");
env_content.push_str("# SERVICEBUS__CONNECTION_STRING=your-connection-string\n");
}
env_content.push_str(
"# SERVICEBUS__ENCRYPTED_CONNECTION_STRING=your-encrypted-connection-string\n",
);
env_content.push_str("# SERVICEBUS__ENCRYPTION_SALT=your-encryption-salt\n\n");
env_content.push_str("# Optional: Azure resource information (if not auto-discovered)\n");
env_content.push_str("# AZURE_AD__SUBSCRIPTION_ID=your-subscription-id\n");
env_content.push_str("# AZURE_AD__RESOURCE_GROUP=your-resource-group\n");
env_content.push_str("# AZURE_AD__NAMESPACE=your-servicebus-namespace\n");
Ok(env_content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_env_content_device_code() {
let env_content = SetupWizard::generate_env_content("device_code", None).unwrap();
assert!(env_content.contains("AZURE_AD__AUTH_METHOD=device_code"));
assert!(env_content.contains("Device code flow"));
}
#[test]
fn test_generate_env_content_client_secret() {
let env_content = SetupWizard::generate_env_content("client_secret", None).unwrap();
assert!(env_content.contains("AZURE_AD__AUTH_METHOD=client_secret"));
assert!(env_content.contains("AZURE_AD__CLIENT_SECRET"));
}
#[test]
fn test_generate_env_content_connection_string() {
let test_conn_str = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=testkey";
let env_content =
SetupWizard::generate_env_content("connection_string", Some(test_conn_str)).unwrap();
assert!(env_content.contains("SERVICEBUS__CONNECTION_STRING="));
assert!(env_content.contains(test_conn_str));
assert!(env_content.contains("Connection string authentication"));
}
#[test]
fn test_generate_env_content_connection_string_none() {
let env_content = SetupWizard::generate_env_content("connection_string", None).unwrap();
assert!(env_content.contains("# SERVICEBUS__CONNECTION_STRING=your-connection-string"));
assert!(env_content.contains("Connection string authentication"));
}
}