Files
attune/crates/cli/src/commands/auth.rs
2026-02-04 17:46:30 -06:00

214 lines
5.7 KiB
Rust

use anyhow::Result;
use clap::Subcommand;
use serde::{Deserialize, Serialize};
use crate::client::ApiClient;
use crate::config::CliConfig;
use crate::output::{self, OutputFormat};
#[derive(Subcommand)]
pub enum AuthCommands {
/// Log in to Attune API
Login {
/// Username or email
#[arg(short, long)]
username: String,
/// Password (will prompt if not provided)
#[arg(long)]
password: Option<String>,
},
/// Log out and clear authentication tokens
Logout,
/// Show current authentication status
Whoami,
/// Refresh authentication token
Refresh,
}
#[derive(Debug, Serialize, Deserialize)]
struct LoginRequest {
login: String,
password: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct LoginResponse {
access_token: String,
refresh_token: String,
expires_in: i64,
}
#[derive(Debug, Serialize, Deserialize)]
struct Identity {
id: i64,
login: String,
display_name: Option<String>,
}
pub async fn handle_auth_command(
profile: &Option<String>,
command: AuthCommands,
api_url: &Option<String>,
output_format: OutputFormat,
) -> Result<()> {
match command {
AuthCommands::Login { username, password } => {
handle_login(username, password, profile, api_url, output_format).await
}
AuthCommands::Logout => handle_logout(profile, output_format).await,
AuthCommands::Whoami => handle_whoami(profile, api_url, output_format).await,
AuthCommands::Refresh => handle_refresh(profile, api_url, output_format).await,
}
}
async fn handle_login(
username: String,
password: Option<String>,
profile: &Option<String>,
api_url: &Option<String>,
output_format: OutputFormat,
) -> Result<()> {
let config = CliConfig::load_with_profile(profile.as_deref())?;
// Prompt for password if not provided
let password = match password {
Some(p) => p,
None => {
let pw = dialoguer::Password::new()
.with_prompt("Password")
.interact()?;
pw
}
};
let mut client = ApiClient::from_config(&config, api_url);
let login_req = LoginRequest {
login: username,
password,
};
let response: LoginResponse = client.post("/auth/login", &login_req).await?;
// Save tokens to config
let mut config = CliConfig::load()?;
config.set_auth(
response.access_token.clone(),
response.refresh_token.clone(),
)?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
output::print_output(&response, output_format)?;
}
OutputFormat::Table => {
output::print_success("Successfully logged in");
output::print_info(&format!("Token expires in {} seconds", response.expires_in));
}
}
Ok(())
}
async fn handle_logout(profile: &Option<String>, output_format: OutputFormat) -> Result<()> {
let mut config = CliConfig::load_with_profile(profile.as_deref())?;
config.clear_auth()?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
let msg = serde_json::json!({"message": "Successfully logged out"});
output::print_output(&msg, output_format)?;
}
OutputFormat::Table => {
output::print_success("Successfully logged out");
}
}
Ok(())
}
async fn handle_whoami(
profile: &Option<String>,
api_url: &Option<String>,
output_format: OutputFormat,
) -> Result<()> {
let config = CliConfig::load_with_profile(profile.as_deref())?;
if config.auth_token().ok().flatten().is_none() {
anyhow::bail!("Not logged in. Use 'attune auth login' to authenticate.");
}
let mut client = ApiClient::from_config(&config, api_url);
let identity: Identity = client.get("/auth/me").await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
output::print_output(&identity, output_format)?;
}
OutputFormat::Table => {
output::print_section("Current Identity");
output::print_key_value_table(vec![
("ID", identity.id.to_string()),
("Login", identity.login),
(
"Display Name",
identity.display_name.unwrap_or_else(|| "-".to_string()),
),
]);
}
}
Ok(())
}
async fn handle_refresh(
profile: &Option<String>,
api_url: &Option<String>,
output_format: OutputFormat,
) -> Result<()> {
let config = CliConfig::load_with_profile(profile.as_deref())?;
// Check if we have a refresh token
let refresh_token = config
.refresh_token()
.ok()
.flatten()
.ok_or_else(|| anyhow::anyhow!("No refresh token found. Please log in again."))?;
let mut client = ApiClient::from_config(&config, api_url);
#[derive(Serialize)]
struct RefreshRequest {
refresh_token: String,
}
// Call the refresh endpoint
let response: LoginResponse = client
.post("/auth/refresh", &RefreshRequest { refresh_token })
.await?;
// Save new tokens to config
let mut config = CliConfig::load()?;
config.set_auth(
response.access_token.clone(),
response.refresh_token.clone(),
)?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
output::print_output(&response, output_format)?;
}
OutputFormat::Table => {
output::print_success("Token refreshed successfully");
output::print_info(&format!(
"New token expires in {} seconds",
response.expires_in
));
}
}
Ok(())
}