use anyhow::Result;
use clap::Subcommand;
use colored::Colorize;
use serde::Serialize;
use std::path::PathBuf;
use crate::daemon::{check_daemon_running, DaemonClient, DaemonStatus};
use crate::output::{render_serialized, OutputFormat};
#[derive(Subcommand)]
pub enum CredentialCommand {
Store {
#[arg(long, value_name = "NAME")]
provider: String,
#[arg(long, value_name = "LABEL")]
label: String,
#[arg(long, value_name = "SCOPE", default_value = "personal")]
scope: String,
#[arg(long, value_name = "KEY")]
value: String,
},
Connect {
#[arg(long, value_name = "NAME")]
provider: String,
#[arg(long, value_name = "URI")]
redirect_uri: String,
},
List,
Get {
#[arg(value_name = "ID")]
id: String,
},
Disconnect {
#[arg(value_name = "ID")]
id: String,
},
Delete {
#[arg(value_name = "ID")]
id: String,
},
Grant {
#[command(subcommand)]
command: GrantCommand,
},
}
#[derive(Subcommand)]
pub enum GrantCommand {
Add {
#[arg(value_name = "BINDING_ID")]
binding_id: String,
#[arg(long, value_name = "TYPE")]
target_type: String,
#[arg(long, value_name = "UUID")]
target_id: Option<String>,
#[arg(long, value_name = "STRING")]
granted_by: Option<String>,
},
List {
#[arg(value_name = "BINDING_ID")]
binding_id: String,
},
Revoke {
#[arg(value_name = "BINDING_ID")]
binding_id: String,
#[arg(value_name = "GRANT_ID")]
grant_id: String,
},
}
pub async fn handle_command(
command: CredentialCommand,
_config_path: Option<PathBuf>,
host: &str,
port: u16,
output_format: OutputFormat,
) -> Result<()> {
let daemon_status = check_daemon_running(host, port).await;
match daemon_status {
Ok(DaemonStatus::Running { .. }) => {}
Ok(DaemonStatus::Unhealthy { pid, error }) => {
println!(
"{}",
format!("⚠ Daemon is running (PID: {pid}) but unhealthy: {error}").yellow()
);
println!("Run 'aegis daemon status' for more info.");
return Ok(());
}
_ => {
println!(
"{}",
"Credential management requires the daemon to be running.".red()
);
println!("Run 'aegis daemon start' to start the daemon.");
return Ok(());
}
}
let auth_key = crate::auth::require_key().await?;
let client = DaemonClient::new(host, port)?.with_auth(auth_key);
match command {
CredentialCommand::Store {
provider,
label,
scope,
value,
} => store_credential(&provider, &label, &scope, &value, &client, output_format).await,
CredentialCommand::Connect {
provider,
redirect_uri,
} => connect_oauth(&provider, &redirect_uri, &client, output_format).await,
CredentialCommand::List => list_credentials(&client, output_format).await,
CredentialCommand::Get { id } => get_credential(&id, &client, output_format).await,
CredentialCommand::Disconnect { id } | CredentialCommand::Delete { id } => {
delete_credential(&id, &client, output_format).await
}
CredentialCommand::Grant { command } => match command {
GrantCommand::Add {
binding_id,
target_type,
target_id,
granted_by,
} => {
add_grant(
&binding_id,
&target_type,
target_id.as_deref(),
granted_by.as_deref(),
&client,
output_format,
)
.await
}
GrantCommand::List { binding_id } => {
list_grants(&binding_id, &client, output_format).await
}
GrantCommand::Revoke {
binding_id,
grant_id,
} => revoke_grant(&binding_id, &grant_id, &client, output_format).await,
},
}
}
#[derive(Serialize)]
struct CredentialMutationOutput {
id: String,
status: &'static str,
}
async fn store_credential(
provider: &str,
label: &str,
scope: &str,
value: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
let body = client
.store_api_key(provider, label, Some(scope), value)
.await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
let id = body
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
println!(
"{}",
format!("✓ Credential stored for provider '{provider}' (id: {id})").green()
);
Ok(())
}
async fn connect_oauth(
provider: &str,
redirect_uri: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
let body = client.initiate_oauth(provider, redirect_uri).await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
let auth_url = body
.get("authorization_url")
.and_then(|v| v.as_str())
.unwrap_or("<url not returned by server>");
let state = body
.get("state")
.and_then(|v| v.as_str())
.unwrap_or("<state not returned by server>");
println!("{}", "OAuth flow initiated.".cyan());
println!("{:16} {}", "Authorization URL:".dimmed(), auth_url.bold());
println!("{:16} {}", "State:".dimmed(), state);
println!();
println!(
"{}",
"Visit the URL above to authorize access. The callback is handled by the web UI.".dimmed()
);
Ok(())
}
async fn list_credentials(client: &DaemonClient, output_format: OutputFormat) -> Result<()> {
let body = client.list_credentials().await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
Ok(())
}
async fn get_credential(
id: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
let body = client.get_credential(id).await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
Ok(())
}
async fn delete_credential(
id: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
client.delete_credential(id).await?;
if output_format.is_structured() {
return render_serialized(
output_format,
&CredentialMutationOutput {
id: id.to_string(),
status: "deleted",
},
);
}
println!("{}", format!("✓ Credential '{id}' deleted").green());
Ok(())
}
async fn add_grant(
binding_id: &str,
target_type: &str,
target_id: Option<&str>,
granted_by: Option<&str>,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
let body = client
.add_credential_grant(binding_id, target_type, target_id, granted_by)
.await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
let grant_id = body
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
println!(
"{}",
format!("✓ Grant '{grant_id}' added to credential '{binding_id}'").green()
);
Ok(())
}
async fn list_grants(
binding_id: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
let body = client.list_credential_grants(binding_id).await?;
if output_format.is_structured() {
return render_serialized(output_format, &body);
}
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
Ok(())
}
async fn revoke_grant(
binding_id: &str,
grant_id: &str,
client: &DaemonClient,
output_format: OutputFormat,
) -> Result<()> {
client.revoke_credential_grant(binding_id, grant_id).await?;
if output_format.is_structured() {
return render_serialized(
output_format,
&serde_json::json!({
"binding_id": binding_id,
"grant_id": grant_id,
"status": "revoked"
}),
);
}
println!(
"{}",
format!("✓ Grant '{grant_id}' revoked from credential '{binding_id}'").green()
);
Ok(())
}