Jesús Pérez be62c8701a feat: Add ARGUMENTS documentation and interactive update mode
- Add `show-arguments` recipe documenting all version update commands
- Add `complete-update-interactive` recipe for manual confirmations
- Maintain `complete-update` as automatic mode (no prompts)
- Update `update-help` to reference new recipes and modes
- Document 7-step workflow and step-by-step differences

Changes:
- complete-update: Automatic mode (recommended for CI/CD)
- complete-update-interactive: Interactive mode (with confirmations)
- show-arguments: Complete documentation of all commands and modes
- Both modes share same 7-step workflow with different behavior in Step 4
2025-10-19 00:05:16 +01:00

328 lines
9.3 KiB
Rust

// 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<String>,
}
/// 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<String>,
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<String>,
pub username: Option<String>,
pub roles: Option<Vec<String>>,
pub expires_at: Option<String>,
}
// 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<String, String> {
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<String, String> {
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<TokenResponse, String> {
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::<TokenResponse>()
.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<VerifyResponse, String> {
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::<VerifyResponse>()
.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<Vec<SessionInfo>, 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::<Vec<SessionInfo>>()
.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<String>,
}
/// 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<MfaEnrollResponse, String> {
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::<MfaEnrollResponse>()
.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<bool, String> {
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<String, String> {
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::<unicode::Dense1x2>()
.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<String, String> {
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())
}