use anyhow::Result;
use clap::Args;
use stkd_github::{auth as github_auth, DeviceFlow, GitHubClient};
use stkd_gitlab::auth as gitlab_auth;
use crate::output;
use crate::provider_context::{detect_provider_type, ProviderType};
#[derive(Args)]
pub struct AuthArgs {
#[arg(long, conflicts_with = "gitlab")]
github: bool,
#[arg(long, conflicts_with = "github")]
gitlab: bool,
#[arg(long, default_value = "gitlab.com")]
host: String,
#[arg(long)]
token: Option<String>,
#[arg(long)]
client_id: Option<String>,
#[arg(long)]
logout: bool,
#[arg(long)]
status: bool,
#[arg(long)]
web: bool,
}
pub async fn execute(args: AuthArgs) -> Result<()> {
let provider = if args.github {
ProviderType::GitHub
} else if args.gitlab {
ProviderType::GitLab
} else {
match stkd_core::Repository::open(".") {
Ok(repo) => detect_provider_type(&repo).unwrap_or(ProviderType::GitHub),
Err(_) => ProviderType::GitHub, }
};
match provider {
ProviderType::GitHub => execute_github(args).await,
ProviderType::GitLab => execute_gitlab(args).await,
}
}
async fn execute_github(args: AuthArgs) -> Result<()> {
if args.logout {
github_auth::clear_credentials()?;
output::success("Logged out from GitHub");
return Ok(());
}
if args.status {
return show_github_status().await;
}
if args.web {
let client_id = args.client_id.ok_or_else(|| {
anyhow::anyhow!(
"OAuth device flow requires --client-id.\n\
Create an OAuth App at https://github.com/settings/developers\n\
and provide the Client ID."
)
})?;
return github_oauth_flow(&client_id).await;
}
if let Some(token) = args.token {
return github_token_auth(token).await;
}
output::info("GitHub Authentication");
output::info("");
output::info("Choose authentication method:");
output::info(" 1. Personal Access Token (PAT)");
output::info(" 2. OAuth Device Flow (requires OAuth App)");
output::info("");
let choice = output::select(
"Authentication method",
&["Personal Access Token", "OAuth Device Flow"],
);
match choice {
Some(0) => {
output::info("");
output::info("Create a token at: https://github.com/settings/tokens/new");
output::info("Required scopes: repo, read:org");
output::info("");
let token = output::input("GitHub personal access token")
.ok_or_else(|| anyhow::anyhow!("No token provided"))?;
github_token_auth(token).await
}
Some(1) => {
output::info("");
output::info("OAuth device flow requires an OAuth App Client ID.");
output::info("Create one at: https://github.com/settings/developers");
output::info("");
let client_id = output::input("OAuth App Client ID")
.ok_or_else(|| anyhow::anyhow!("No client ID provided"))?;
github_oauth_flow(&client_id).await
}
_ => {
output::info("Cancelled");
Ok(())
}
}
}
async fn show_github_status() -> Result<()> {
if let Some(token) = github_auth::load_credentials()? {
output::info(&format!(
"GitHub: Authenticated with {} token",
match token.token_type {
github_auth::TokenType::Pat => "personal access",
github_auth::TokenType::OAuth => "OAuth",
}
));
let client = GitHubClient::new(token.to_auth())?;
if client.validate_token().await? {
let user = client.current_user().await?;
output::success(&format!(" Logged in as: {}", user.login));
if let Some(name) = user.name {
output::info(&format!(" Name: {}", name));
}
if let Some(email) = user.email {
output::info(&format!(" Email: {}", email));
}
output::info(&format!(
" Created: {}",
token.created_at.format("%Y-%m-%d %H:%M")
));
if let Some(expires) = token.expires_at {
output::info(&format!(" Expires: {}", expires.format("%Y-%m-%d %H:%M")));
}
} else {
output::warn(" Token is invalid or expired");
output::hint(" Run 'gt auth --logout --github' and re-authenticate");
}
} else {
output::info("GitHub: Not authenticated");
output::hint("Run 'gt auth --github' to authenticate with GitHub");
}
Ok(())
}
async fn github_token_auth(token: String) -> Result<()> {
output::info("Validating GitHub token...");
let auth_token = github_auth::AuthToken::pat(token);
let client = GitHubClient::new(auth_token.to_auth())?;
if !client.validate_token().await? {
output::error("Invalid token");
anyhow::bail!("Token validation failed");
}
let user = client.current_user().await?;
github_auth::save_credentials(&auth_token)?;
output::success(&format!("Authenticated with GitHub as {}", user.login));
Ok(())
}
async fn github_oauth_flow(client_id: &str) -> Result<()> {
output::info("Starting GitHub OAuth device flow...");
let flow = DeviceFlow::new(client_id)?;
let scope = "repo read:org";
let token_response = flow.authenticate(scope).await?;
let auth_token = github_auth::AuthToken::oauth(
token_response.access_token,
None, None, );
let client = GitHubClient::new(auth_token.to_auth())?;
let user = client.current_user().await?;
github_auth::save_credentials(&auth_token)?;
output::success(&format!("Authenticated with GitHub as {}", user.login));
Ok(())
}
async fn execute_gitlab(args: AuthArgs) -> Result<()> {
let host = &args.host;
if args.logout {
gitlab_auth::remove_credentials(host)?;
output::success(&format!("Logged out from GitLab ({})", host));
return Ok(());
}
if args.status {
return show_gitlab_status(host).await;
}
if args.web {
output::warn("OAuth device flow is not yet supported for GitLab.");
output::info("Please use a Personal Access Token instead.");
}
if let Some(token) = args.token {
return gitlab_token_auth(token, host).await;
}
output::info(&format!("GitLab Authentication ({})", host));
output::info("");
output::info(&format!(
"Create a token at: https://{}/- /user_settings/personal_access_tokens",
host
));
output::info("Required scopes: api, read_user");
output::info("");
let token = output::input("GitLab personal access token")
.ok_or_else(|| anyhow::anyhow!("No token provided"))?;
gitlab_token_auth(token, host).await
}
async fn show_gitlab_status(host: &str) -> Result<()> {
if let Some(token) = gitlab_auth::load_credentials(host)? {
output::info(&format!(
"GitLab ({}): Authenticated with {} token",
host, token.token_type
));
let provider = if host == "gitlab.com" {
stkd_gitlab::GitLabProvider::new(&token.token)?
} else {
stkd_gitlab::GitLabProvider::with_host(&token.token, host)?
};
use stkd_provider_api::UserProvider;
match provider.current_user().await {
Ok(user) => {
output::success(&format!(" Logged in as: {}", user.username));
if let Some(name) = user.name {
output::info(&format!(" Name: {}", name));
}
if let Some(email) = user.email {
output::info(&format!(" Email: {}", email));
}
}
Err(_) => {
output::warn(" Token is invalid or expired");
output::hint(&format!(
" Run 'gt auth --logout --gitlab --host {}' and re-authenticate",
host
));
}
}
} else {
output::info(&format!("GitLab ({}): Not authenticated", host));
if host == "gitlab.com" {
output::hint("Run 'gt auth --gitlab' to authenticate with GitLab");
} else {
output::hint(&format!(
"Run 'gt auth --gitlab --host {}' to authenticate",
host
));
}
}
Ok(())
}
async fn gitlab_token_auth(token: String, host: &str) -> Result<()> {
output::info(&format!("Validating GitLab token for {}...", host));
let provider = if host == "gitlab.com" {
stkd_gitlab::GitLabProvider::new(&token)?
} else {
stkd_gitlab::GitLabProvider::with_host(&token, host)?
};
use stkd_provider_api::UserProvider;
let user = provider
.current_user()
.await
.map_err(|_| anyhow::anyhow!("Token validation failed"))?;
let auth_token = gitlab_auth::AuthToken::pat(&token, host);
gitlab_auth::save_credentials(&auth_token)?;
output::success(&format!(
"Authenticated with GitLab ({}) as {}",
host, user.username
));
Ok(())
}