// Helper functions for authentication use keyring::Entry; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use std::io::{self, Write}; /// Request payload for login endpoint #[derive(Serialize, Deserialize, Debug)] pub struct LoginRequest { pub username: String, pub password: String, } /// Response from login endpoint containing JWT tokens #[derive(Serialize, Deserialize, Debug)] pub struct TokenResponse { pub access_token: String, pub refresh_token: String, pub expires_in: i64, pub user: UserInfo, } /// User information from login response #[derive(Serialize, Deserialize, Debug)] pub struct UserInfo { pub id: String, pub username: String, pub email: String, pub roles: Vec, } /// Request payload for logout endpoint #[derive(Serialize)] pub struct LogoutRequest { pub access_token: String, } /// Session information (used by auth sessions command - Agente 5) #[derive(Serialize, Deserialize, Debug)] #[allow(dead_code)] // Planned for auth sessions command implementation pub struct SessionInfo { pub user_id: String, pub username: String, pub roles: Vec, pub created_at: String, pub expires_at: String, pub is_active: bool, } /// Token verification response (used by auth verify command - Agente 4) #[derive(Serialize, Deserialize, Debug)] #[allow(dead_code)] // Planned for auth verify command implementation pub struct VerifyResponse { pub valid: bool, pub user_id: Option, pub username: Option, pub roles: Option>, pub expires_at: Option, } // Secure token storage using OS keyring /// Store tokens in secure keyring pub fn store_tokens_in_keyring( username: &str, access_token: &str, refresh_token: &str, ) -> Result<(), String> { let entry_access = Entry::new("provisioning-access", username) .map_err(|e| format!("Keyring access error: {}", e))?; let entry_refresh = Entry::new("provisioning-refresh", username) .map_err(|e| format!("Keyring refresh error: {}", e))?; entry_access .set_password(access_token) .map_err(|e| format!("Failed to store access token: {}", e))?; entry_refresh .set_password(refresh_token) .map_err(|e| format!("Failed to store refresh token: {}", e))?; Ok(()) } /// Retrieve access token from keyring pub fn get_access_token(username: &str) -> Result { let entry = Entry::new("provisioning-access", username).map_err(|e| format!("Keyring error: {}", e))?; entry .get_password() .map_err(|e| format!("No token found: {}", e)) } /// Remove tokens from keyring pub fn remove_tokens_from_keyring(username: &str) -> Result<(), String> { let entry_access = Entry::new("provisioning-access", username) .map_err(|e| format!("Keyring access error: {}", e))?; let entry_refresh = Entry::new("provisioning-refresh", username) .map_err(|e| format!("Keyring refresh error: {}", e))?; // Keyring 3.x uses delete_credential instead of delete_password let _ = entry_access.delete_credential(); let _ = entry_refresh.delete_credential(); Ok(()) } // Secure password input (no echo) /// Prompt for password without echoing to terminal pub fn prompt_password(prompt: &str) -> Result { print!("{}", prompt); io::stdout() .flush() .map_err(|e| format!("Flush error: {}", e))?; rpassword::read_password().map_err(|e| format!("Password read error: {}", e)) } // HTTP API calls /// Send login request to control center pub fn send_login_request( url: &str, username: &str, password: &str, ) -> Result { let client = Client::new(); let response = client .post(format!("{}/auth/login", url)) .json(&LoginRequest { username: username.to_string(), password: password.to_string(), }) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .unwrap_or_else(|_| "Unknown error".to_string()); return Err(format!("Login failed: HTTP {} - {}", status, error_text)); } response .json::() .map_err(|e| format!("Failed to parse response: {}", e)) } /// Send logout request to control center pub fn send_logout_request(url: &str, access_token: &str) -> Result<(), String> { let client = Client::new(); let response = client .post(format!("{}/auth/logout", url)) .bearer_auth(access_token) .json(&LogoutRequest { access_token: access_token.to_string(), }) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .unwrap_or_else(|_| "Unknown error".to_string()); return Err(format!("Logout failed: HTTP {} - {}", status, error_text)); } Ok(()) } /// Verify token with control center (planned for auth verify command - Agente 4) #[allow(dead_code)] pub fn verify_token(url: &str, token: &str) -> Result { let client = Client::new(); let response = client .get(format!("{}/auth/verify", url)) .bearer_auth(token) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; if !response.status().is_success() { return Ok(VerifyResponse { valid: false, user_id: None, username: None, roles: None, expires_at: None, }); } response .json::() .map_err(|e| format!("Failed to parse response: {}", e)) } /// List active sessions (planned for auth sessions command - Agente 5) #[allow(dead_code)] pub fn list_sessions(url: &str, token: &str) -> Result, String> { let client = Client::new(); let response = client .get(format!("{}/auth/sessions", url)) .bearer_auth(token) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); return Err(format!("Failed to list sessions: HTTP {}", status)); } response .json::>() .map_err(|e| format!("Failed to parse response: {}", e)) } // MFA support /// MFA enrollment request payload #[derive(Serialize, Debug)] pub struct MfaEnrollRequest { pub mfa_type: String, // "totp" or "webauthn" } /// MFA enrollment response with secret and QR code #[derive(Deserialize, Debug)] pub struct MfaEnrollResponse { pub secret: String, pub qr_code_uri: String, pub backup_codes: Vec, } /// MFA verification request payload #[derive(Serialize, Debug)] pub struct MfaVerifyRequest { pub code: String, } /// Send MFA enrollment request to control center pub fn send_mfa_enroll_request( url: &str, access_token: &str, mfa_type: &str, ) -> Result { let client = Client::new(); let response = client .post(format!("{}/mfa/enroll/{}", url, mfa_type)) .bearer_auth(access_token) .json(&MfaEnrollRequest { mfa_type: mfa_type.to_string(), }) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .unwrap_or_else(|_| "Unknown error".to_string()); return Err(format!( "MFA enroll failed: HTTP {} - {}", status, error_text )); } response .json::() .map_err(|e| format!("Failed to parse response: {}", e)) } /// Send MFA verification request to control center pub fn send_mfa_verify_request(url: &str, access_token: &str, code: &str) -> Result { let client = Client::new(); let response = client .post(format!("{}/mfa/verify", url)) .bearer_auth(access_token) .json(&MfaVerifyRequest { code: code.to_string(), }) .send() .map_err(|e| format!("HTTP request failed: {}", e))?; Ok(response.status().is_success()) } /// Generate QR code for TOTP enrollment pub fn generate_qr_code(uri: &str) -> Result { use qrcode::render::unicode; use qrcode::QrCode; let code = QrCode::new(uri).map_err(|e| format!("QR code generation failed: {}", e))?; let qr_string = code .render::() .dark_color(unicode::Dense1x2::Light) .light_color(unicode::Dense1x2::Dark) .build(); Ok(qr_string) } /// Display QR code in terminal with instructions pub fn display_qr_code(uri: &str) -> Result<(), String> { let qr = generate_qr_code(uri)?; println!("\n{}\n", qr); println!("Scan this QR code with your authenticator app"); println!("Or enter this secret manually: {}", extract_secret(uri)?); Ok(()) } /// Extract secret from TOTP URI fn extract_secret(uri: &str) -> Result { uri.split("secret=") .nth(1) .and_then(|s| s.split('&').next()) .ok_or("Failed to extract secret from URI".to_string()) .map(|s| s.to_string()) }