use anyhow::{Context, Result, bail};
use std::process::Command;
use crate::config::types::JiraConfig;
use crate::jira::auth::{Auth, BasicCredentials};
use crate::jira::oauth;
pub fn resolve_auth(jira: &JiraConfig) -> Result<Auth> {
if jira.auth_method.as_deref() == Some("oauth") {
return resolve_oauth();
}
resolve_basic(jira)
}
fn resolve_oauth() -> Result<Auth> {
match oauth::load_oauth_tokens()? {
Some(creds) => Ok(Auth::OAuth(creds)),
None => bail!(
"No OAuth tokens found.\n\
Run `do-next auth` to authenticate with your browser."
),
}
}
fn resolve_basic(jira: &JiraConfig) -> Result<Auth> {
let email = resolve_email(jira)?;
let api_token = resolve_api_token(jira)?;
Ok(Auth::Basic(BasicCredentials { email, api_token }))
}
fn resolve_email(jira: &JiraConfig) -> Result<String> {
if let Ok(email) = std::env::var("DO_NEXT_JIRA_EMAIL") {
log::debug!("credentials: using DO_NEXT_JIRA_EMAIL env var");
return Ok(email);
}
if let Some(email) = &jira.email {
log::debug!("credentials: using email from config");
return Ok(email.clone());
}
bail!(
"No Jira email configured.\n\
Set DO_NEXT_JIRA_EMAIL env var or add `email` to your Jira config.\n\
Run `do-next auth` to reconfigure."
)
}
fn resolve_api_token(jira: &JiraConfig) -> Result<String> {
if let Ok(token) = std::env::var("DO_NEXT_JIRA_API_TOKEN") {
log::debug!("credentials: using DO_NEXT_JIRA_API_TOKEN env var");
return Ok(token);
}
if let Some(cmd) = &jira.credential_command {
log::debug!("credentials: running credential_command: {cmd}");
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.with_context(|| format!("Failed to run credential_command: {cmd}"))?;
if !output.status.success() {
bail!("credential_command exited with non-zero status");
}
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
log::debug!("credentials: credential_command succeeded");
return Ok(token);
}
if jira.credential_store.as_deref() == Some("keyring") {
let key = jira.credential_key.as_deref().unwrap_or(&jira.base_url);
log::debug!("credentials: looking up keyring entry for key={key}");
let entry =
keyring::Entry::new("do-next", key).context("Failed to create keyring entry")?;
match entry.get_password() {
Ok(secret) => {
log::debug!("credentials: keyring lookup succeeded");
return Ok(secret);
}
Err(keyring::Error::NoEntry) => {
log::debug!("credentials: no keyring entry found, falling through");
}
Err(keyring::Error::NoStorageAccess(e)) => {
log::debug!("credentials: keyring storage not accessible: {e}");
bail!(
"The system keyring is not accessible (key={key}).\n\
The secret service may not be running or the keyring may be locked.\n\
\n\
Possible fixes:\n\
• Ensure your keyring daemon is running (gnome-keyring-daemon, kwallet, pass-secret-service)\n\
• Unlock the keyring or GPG agent and try again\n\
• Set the DO_NEXT_JIRA_API_TOKEN environment variable\n\
• Add credentials to ~/.config/do-next/credentials.json5\n\
\n\
Run with --log <file> for details."
);
}
Err(keyring::Error::PlatformFailure(e)) => {
log::debug!("credentials: keyring platform failure: {e}");
bail!(
"The keyring returned an error while reading the secret (key={key}).\n\
The keyring may be locked or the stored entry may be corrupted.\n\
\n\
Possible fixes:\n\
• Unlock your keyring or GPG agent and try again\n\
• Re-run `do-next auth` to store a fresh API token\n\
• Set the DO_NEXT_JIRA_API_TOKEN environment variable\n\
• Add credentials to ~/.config/do-next/credentials.json5\n\
\n\
Run with --log <file> for details."
);
}
Err(e) => {
log::debug!("credentials: keyring error: {e}");
bail!(
"Unexpected keyring error (key={key}): {e}\n\
\n\
Possible fixes:\n\
• Re-run `do-next auth` to store a fresh API token\n\
• Set the DO_NEXT_JIRA_API_TOKEN environment variable\n\
• Add credentials to ~/.config/do-next/credentials.json5"
);
}
}
}
log::debug!("credentials: checking credentials file");
if let Some(token) = load_credentials_file()? {
log::debug!("credentials: loaded from credentials file");
return Ok(token);
}
bail!(
"No Jira API token found.\n\
Set DO_NEXT_JIRA_API_TOKEN env var or run `do-next auth` to configure credentials."
)
}
#[derive(serde::Deserialize)]
struct CredentialsFile {
jira: Option<CredentialsFileJira>,
}
#[derive(serde::Deserialize)]
struct CredentialsFileJira {
api_token: Option<String>,
}
fn load_credentials_file() -> Result<Option<String>> {
let path = dirs::config_dir()
.context("Cannot determine config directory")?
.join("do-next")
.join("credentials.json5");
if !path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let file: CredentialsFile =
json5::from_str(&content).context("Failed to parse credentials.json5")?;
let Some(jira) = file.jira else {
return Ok(None);
};
Ok(jira.api_token)
}