use crate::auth::{oauth, store, SecureString};
use crate::cli::args::AuthCommands;
use crate::cli::UI;
use crate::platform;
use crate::platform::server_registry::ServerRegistry;
use anyhow::Result;
use std::collections::HashSet;
pub async fn execute(action: AuthCommands, ui: &UI) -> Result<()> {
match action {
AuthCommands::Login {
provider,
token,
host,
} => login(provider, token, host, ui).await,
AuthCommands::Logout { provider } => logout(provider, ui),
AuthCommands::Status => status(ui).await,
}
}
async fn login(
provider: Option<String>,
token: Option<String>,
custom_host: Option<String>,
ui: &UI,
) -> Result<()> {
let provider_name = provider.as_deref().unwrap_or("github");
if let Some(ref host_url) = custom_host {
return login_custom_host(provider_name, host_url, token, ui).await;
}
let host = match provider_name {
"github" | "gh" => "github.com",
"gitlab" | "gl" => "gitlab.com",
other => {
anyhow::bail!("Unknown provider: {}. Use 'github' or 'gitlab'.", other);
}
};
ui.header("SecureGit Authentication");
ui.blank();
ui.field("Provider", provider_name);
if let Some(token_str) = token {
ui.field("Method", "Token");
ui.blank();
let secure_token = SecureString::from_string(token_str);
let spinner = ui.spinner("Validating token...");
let user = validate_token(host, &secure_token).await?;
ui.finish_progress(&spinner, "Token validated");
store::store_token(host, &secure_token)?;
ui.blank();
ui.success(format!("Authenticated as {}", user));
ui.blank();
ui.field("User", &user);
ui.field("Stored", "~/.config/securegit/credentials.json");
ui.blank();
} else {
ui.field("Method", "Device Flow");
ui.blank();
let result = match host {
"github.com" => {
oauth::github_device_flow(
|code, uri| {
ui.blank();
ui.info(format!("Open {}", uri));
ui.info(format!("Enter code: {}", code));
ui.blank();
},
|| {},
)
.await?
}
"gitlab.com" => {
oauth::gitlab_device_flow(
|code, uri| {
ui.blank();
ui.info(format!("Open {}", uri));
ui.info(format!("Enter code: {}", code));
ui.blank();
},
|| {},
)
.await?
}
_ => unreachable!(),
};
store::store_token(host, &result.token)?;
ui.blank();
ui.success(format!("Authenticated as {}", result.user));
ui.blank();
ui.field("User", &result.user);
ui.field("Scope", &result.scope);
ui.field("Stored", "~/.config/securegit/credentials.json");
ui.blank();
}
Ok(())
}
async fn login_custom_host(
provider_name: &str,
host_url: &str,
token: Option<String>,
ui: &UI,
) -> Result<()> {
ui.header("SecureGit Authentication (Self-Hosted)");
ui.blank();
ui.field("Provider", provider_name);
ui.field("Host", host_url);
let token_str = token.ok_or_else(|| {
anyhow::anyhow!("--token is required for self-hosted instances (no device flow support)")
})?;
ui.field("Method", "Token");
ui.blank();
let secure_token = SecureString::from_string(token_str);
let spinner = ui.spinner("Validating token...");
let user = validate_token_generic(host_url, provider_name, &secure_token).await?;
ui.finish_progress(&spinner, "Token validated");
store::store_token(host_url, &secure_token)?;
ui.blank();
ui.success(format!("Authenticated as {}", user));
ui.blank();
ui.field("User", &user);
ui.field("Stored", "~/.config/securegit/credentials.json");
ui.blank();
Ok(())
}
fn logout(provider: Option<String>, ui: &UI) -> Result<()> {
ui.header("SecureGit Logout");
ui.blank();
if let Some(provider_name) = provider {
let host = match provider_name.as_str() {
"github" | "gh" => "github.com",
"gitlab" | "gl" => "gitlab.com",
other => {
anyhow::bail!("Unknown provider: {}", other);
}
};
store::delete_token(host)?;
ui.status_item(true, format!("Removed credentials for {}", host));
} else {
store::delete_all_tokens()?;
ui.status_item(true, "Removed all stored credentials");
}
ui.blank();
Ok(())
}
async fn status(ui: &UI) -> Result<()> {
ui.header("Authentication Status");
ui.blank();
let mut hosts: Vec<String> = vec!["github.com".to_string(), "gitlab.com".to_string()];
let mut seen: HashSet<String> = hosts.iter().cloned().collect();
for h in store::list_stored_hosts() {
if seen.insert(h.clone()) {
hosts.push(h);
}
}
let registry = ServerRegistry::load().ok();
if let Some(ref reg) = registry {
for server in ®.servers {
if let Ok(url) = url::Url::parse(&server.api_url) {
if let Some(h) = url.host_str() {
let h = h.to_string();
if seen.insert(h.clone()) {
hosts.push(h);
}
}
}
}
}
let mut any_authenticated = false;
for host in &hosts {
let provider = match host.as_str() {
"github.com" => "GitHub".to_string(),
"gitlab.com" => "GitLab".to_string(),
other => {
if let Some(ref reg) = registry {
reg.servers
.iter()
.find(|s| {
url::Url::parse(&s.api_url)
.ok()
.and_then(|u| u.host_str().map(|h| h == other))
.unwrap_or(false)
})
.map(|s| s.name.clone())
.unwrap_or_else(|| other.to_string())
} else {
other.to_string()
}
}
};
if let Some(token) = crate::auth::token_for_host(host) {
let source = if store::get_token(host).is_some() {
"stored credentials"
} else {
"environment variable"
};
let user = match host.as_str() {
"github.com" | "gitlab.com" => validate_token(host, &token)
.await
.unwrap_or_else(|_| "unknown".to_string()),
other => {
let platform_str = if let Some(ref reg) = registry {
reg.servers
.iter()
.find(|s| {
url::Url::parse(&s.api_url)
.ok()
.and_then(|u| u.host_str().map(|h| h == other))
.unwrap_or(false)
})
.map(|s| s.platform.to_string())
.unwrap_or_else(|| {
if other.contains("github") {
"github".to_string()
} else {
"gitlab".to_string()
}
})
} else if other.contains("github") {
"github".to_string()
} else {
"gitlab".to_string()
};
let api_url = if let Some(ref reg) = registry {
reg.servers
.iter()
.find(|s| {
url::Url::parse(&s.api_url)
.ok()
.and_then(|u| u.host_str().map(|h| h == other))
.unwrap_or(false)
})
.map(|s| s.api_url.clone())
} else {
None
};
if let Some(api_url) = api_url {
validate_token_generic(&api_url, &platform_str, &token)
.await
.unwrap_or_else(|_| "unknown".to_string())
} else {
"authenticated".to_string()
}
}
};
ui.field(&provider, format!("{} (via {})", user, source));
any_authenticated = true;
} else {
ui.field(&provider, "not authenticated");
}
}
if let Some(ref reg) = registry {
if !reg.servers.is_empty() {
ui.blank();
ui.field("Servers", "");
for server in ®.servers {
let auth_status = if crate::auth::token_for_server(server).is_some() {
"authenticated"
} else {
"not authenticated"
};
ui.field(
&format!(" {}", server.name),
format!("{} ({})", auth_status, server.api_url),
);
}
}
}
if let Ok(remote) = platform::detect_remote(&std::path::PathBuf::from(".")) {
ui.blank();
ui.field(
"Current repo",
format!("{} ({}/{})", remote.host, remote.owner, remote.repo),
);
}
if !any_authenticated {
ui.blank();
ui.info("Run 'securegit auth login' to authenticate");
}
ui.blank();
Ok(())
}
async fn validate_token(host: &str, token: &SecureString) -> Result<String> {
match host {
"github.com" => {
let client = reqwest::Client::new();
let resp = client
.get("https://api.github.com/user")
.header("Authorization", format!("Bearer {}", token.as_str()))
.header("User-Agent", "securegit")
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("Invalid or expired GitHub token");
}
let data: serde_json::Value = resp.json().await?;
Ok(data["login"].as_str().unwrap_or("unknown").to_string())
}
"gitlab.com" => {
let client = reqwest::Client::new();
let resp = client
.get("https://gitlab.com/api/v4/user")
.header("PRIVATE-TOKEN", token.as_str())
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("Invalid or expired GitLab token");
}
let data: serde_json::Value = resp.json().await?;
Ok(data["username"].as_str().unwrap_or("unknown").to_string())
}
_ => anyhow::bail!("Unknown host: {}", host),
}
}
async fn validate_token_generic(
api_url: &str,
provider: &str,
token: &SecureString,
) -> Result<String> {
let client = reqwest::Client::builder()
.user_agent("securegit")
.timeout(std::time::Duration::from_secs(15))
.build()?;
let base = api_url.trim_end_matches('/');
match provider.to_lowercase().as_str() {
"github" | "gh" => {
let url = format!("{}/user", base);
let resp = client
.get(&url)
.header("Authorization", format!("Bearer {}", token.as_str()))
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("Invalid or expired token for {}", api_url);
}
let data: serde_json::Value = resp.json().await?;
Ok(data["login"].as_str().unwrap_or("unknown").to_string())
}
"gitlab" | "gl" => {
let url = format!("{}/user", base);
let resp = client
.get(&url)
.header("PRIVATE-TOKEN", token.as_str())
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("Invalid or expired token for {}", api_url);
}
let data: serde_json::Value = resp.json().await?;
Ok(data["username"].as_str().unwrap_or("unknown").to_string())
}
other => anyhow::bail!("Unknown provider '{}' for custom host", other),
}
}