cli: make credentials test interactive; add credentials test command
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia87db118ca58c66841265f34253b08ed6a6a6964
This commit is contained in:
parent
d824da52df
commit
d2d6b7c421
6 changed files with 357 additions and 39 deletions
|
|
@ -433,6 +433,8 @@ pub struct CredentialsArgs {
|
||||||
pub enum CredentialsSubcommand {
|
pub enum CredentialsSubcommand {
|
||||||
/// Set API credentials
|
/// Set API credentials
|
||||||
Set(CredentialsSetArgs),
|
Set(CredentialsSetArgs),
|
||||||
|
/// Test stored API credentials
|
||||||
|
Test,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,153 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{PakkerError, Result},
|
error::{PakkerError, Result},
|
||||||
|
http,
|
||||||
model::{PakkerCredentialsFile, set_keyring_secret},
|
model::{PakkerCredentialsFile, set_keyring_secret},
|
||||||
|
ui_utils::{prompt_secret, prompt_yes_no},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn execute(
|
pub async fn execute(
|
||||||
curseforge_api_key: Option<String>,
|
curseforge_api_key: Option<String>,
|
||||||
modrinth_token: Option<String>,
|
modrinth_token: Option<String>,
|
||||||
github_access_token: Option<String>,
|
github_access_token: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut creds = PakkerCredentialsFile::load()?;
|
let mut cf_key = curseforge_api_key;
|
||||||
let updated_any = curseforge_api_key.is_some()
|
let mut mr_token = modrinth_token;
|
||||||
|| modrinth_token.is_some()
|
let mut gh_token = github_access_token;
|
||||||
|| github_access_token.is_some();
|
|
||||||
|
|
||||||
if let Some(key) = curseforge_api_key {
|
let any_cli_args =
|
||||||
let key = key.trim().to_string();
|
cf_key.is_some() || mr_token.is_some() || gh_token.is_some();
|
||||||
if key.is_empty() {
|
|
||||||
return Err(PakkerError::InternalError(
|
// Enter interactive mode when no CLI args provided
|
||||||
"CurseForge API key cannot be empty".to_string(),
|
if !any_cli_args {
|
||||||
));
|
println!("No credentials provided via command line.");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if let Some(key) =
|
||||||
|
prompt_secret("CurseForge API key (press Enter to skip)")?
|
||||||
|
{
|
||||||
|
cf_key = Some(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Setting CurseForge API key...");
|
if let Some(token) = prompt_secret("Modrinth token (press Enter to skip)")?
|
||||||
set_keyring_secret("curseforge_api_key", &key)?;
|
{
|
||||||
creds.curseforge_api_key = Some(key);
|
mr_token = Some(token);
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(token) = modrinth_token {
|
|
||||||
let token = token.trim().to_string();
|
|
||||||
if token.is_empty() {
|
|
||||||
return Err(PakkerError::InternalError(
|
|
||||||
"Modrinth token cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Setting Modrinth token...");
|
if let Some(token) =
|
||||||
set_keyring_secret("modrinth_token", &token)?;
|
prompt_secret("GitHub access token (press Enter to skip)")?
|
||||||
creds.modrinth_token = Some(token);
|
{
|
||||||
}
|
gh_token = Some(token);
|
||||||
|
|
||||||
if let Some(token) = github_access_token {
|
|
||||||
let token = token.trim().to_string();
|
|
||||||
if token.is_empty() {
|
|
||||||
return Err(PakkerError::InternalError(
|
|
||||||
"GitHub access token cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Setting GitHub access token...");
|
|
||||||
set_keyring_secret("github_access_token", &token)?;
|
|
||||||
creds.github_access_token = Some(token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updated_any =
|
||||||
|
cf_key.is_some() || mr_token.is_some() || gh_token.is_some();
|
||||||
|
|
||||||
if !updated_any {
|
if !updated_any {
|
||||||
println!(
|
println!("No credentials to save.");
|
||||||
"No credentials provided. Use --cf-api-key, --modrinth-token, or \
|
|
||||||
--gh-access-token."
|
|
||||||
);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify credentials before saving
|
||||||
|
let client = http::create_http_client();
|
||||||
|
let mut verified = Vec::new();
|
||||||
|
|
||||||
|
if let Some(ref key) = cf_key {
|
||||||
|
print!("Verifying CurseForge API key... ");
|
||||||
|
match verify_curseforge(&client, key).await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("valid");
|
||||||
|
verified.push("CurseForge");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("failed ({e})");
|
||||||
|
if !prompt_yes_no(
|
||||||
|
"CurseForge key appears invalid. Save anyway?",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)? {
|
||||||
|
cf_key = None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref token) = mr_token {
|
||||||
|
print!("Verifying Modrinth token... ");
|
||||||
|
match verify_modrinth(&client, token).await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("valid");
|
||||||
|
verified.push("Modrinth");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("failed ({e})");
|
||||||
|
if !prompt_yes_no(
|
||||||
|
"Modrinth token appears invalid. Save anyway?",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)? {
|
||||||
|
mr_token = None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref token) = gh_token {
|
||||||
|
print!("Verifying GitHub access token... ");
|
||||||
|
match verify_github(&client, token).await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("valid");
|
||||||
|
verified.push("GitHub");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("failed ({e})");
|
||||||
|
if !prompt_yes_no(
|
||||||
|
"GitHub token appears invalid. Save anyway?",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)? {
|
||||||
|
gh_token = None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut creds = PakkerCredentialsFile::load()?;
|
||||||
|
|
||||||
|
if let Some(key) = cf_key {
|
||||||
|
let key = key.trim().to_string();
|
||||||
|
if !key.is_empty() {
|
||||||
|
set_keyring_secret("curseforge_api_key", &key)?;
|
||||||
|
creds.curseforge_api_key = Some(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = mr_token {
|
||||||
|
let token = token.trim().to_string();
|
||||||
|
if !token.is_empty() {
|
||||||
|
set_keyring_secret("modrinth_token", &token)?;
|
||||||
|
creds.modrinth_token = Some(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = gh_token {
|
||||||
|
let token = token.trim().to_string();
|
||||||
|
if !token.is_empty() {
|
||||||
|
set_keyring_secret("github_access_token", &token)?;
|
||||||
|
creds.github_access_token = Some(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
creds.save()?;
|
creds.save()?;
|
||||||
|
|
||||||
println!("Credentials saved.");
|
println!();
|
||||||
|
if verified.is_empty() {
|
||||||
|
println!("Credentials saved (unverified).");
|
||||||
|
} else {
|
||||||
|
println!("Credentials saved and verified: {}", verified.join(", "));
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"Credentials file: {}",
|
"Credentials file: {}",
|
||||||
PakkerCredentialsFile::get_path()?.display()
|
PakkerCredentialsFile::get_path()?.display()
|
||||||
|
|
@ -71,3 +156,61 @@ pub fn execute(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn verify_curseforge(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
api_key: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.curseforge.com/v1/mods/238222")
|
||||||
|
.header("x-api-key", api_key)
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_modrinth(client: &reqwest::Client, token: &str) -> Result<()> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.modrinth.com/v2/user")
|
||||||
|
.header("Authorization", token)
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_github(client: &reqwest::Client, token: &str) -> Result<()> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.github.com/user")
|
||||||
|
.header("Authorization", format!("Bearer {token}"))
|
||||||
|
.header("User-Agent", "pakker")
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
151
src/cli/commands/credentials_test.rs
Normal file
151
src/cli/commands/credentials_test.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
|
use crate::{error::Result, http, model::credentials::ResolvedCredentials};
|
||||||
|
|
||||||
|
pub async fn execute() -> Result<()> {
|
||||||
|
let creds = ResolvedCredentials::load();
|
||||||
|
let client = http::create_http_client();
|
||||||
|
|
||||||
|
let spinner = ProgressBar::new_spinner();
|
||||||
|
spinner.set_style(
|
||||||
|
ProgressStyle::default_spinner()
|
||||||
|
.template("{spinner:.green} {msg}")
|
||||||
|
.expect("spinner template is valid"),
|
||||||
|
);
|
||||||
|
spinner.enable_steady_tick(Duration::from_millis(80));
|
||||||
|
spinner.set_message("Testing credentials...");
|
||||||
|
|
||||||
|
let mut all_valid = true;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
// Test CurseForge
|
||||||
|
if let Some(key) = creds.curseforge_api_key() {
|
||||||
|
spinner.set_message("Testing CurseForge API key...");
|
||||||
|
match test_curseforge(&client, key).await {
|
||||||
|
Ok(()) => results.push(("CurseForge API Key", true, None)),
|
||||||
|
Err(e) => {
|
||||||
|
results.push(("CurseForge API Key", false, Some(e.to_string())));
|
||||||
|
all_valid = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.push(("CurseForge API Key", false, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Modrinth
|
||||||
|
if let Some(token) = creds.modrinth_token() {
|
||||||
|
spinner.set_message("Testing Modrinth token...");
|
||||||
|
match test_modrinth(&client, token).await {
|
||||||
|
Ok(()) => results.push(("Modrinth Token", true, None)),
|
||||||
|
Err(e) => {
|
||||||
|
results.push(("Modrinth Token", false, Some(e.to_string())));
|
||||||
|
all_valid = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.push(("Modrinth Token", false, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GitHub
|
||||||
|
if let Some(token) = creds.github_access_token() {
|
||||||
|
spinner.set_message("Testing GitHub access token...");
|
||||||
|
match test_github(&client, token).await {
|
||||||
|
Ok(()) => results.push(("GitHub Access Token", true, None)),
|
||||||
|
Err(e) => {
|
||||||
|
results.push(("GitHub Access Token", false, Some(e.to_string())));
|
||||||
|
all_valid = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.push(("GitHub Access Token", false, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
|
||||||
|
println!("{}", "Credential Test Results:".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for (name, valid, error) in results {
|
||||||
|
if let Some(err) = error {
|
||||||
|
println!(
|
||||||
|
" {} {} ({err})",
|
||||||
|
format!("{name}:").yellow(),
|
||||||
|
"invalid".red()
|
||||||
|
);
|
||||||
|
} else if valid {
|
||||||
|
println!(" {} {}", format!("{name}:").yellow(), "valid".green());
|
||||||
|
} else {
|
||||||
|
println!(" {} {}", format!("{name}:").yellow(), "not configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if all_valid {
|
||||||
|
println!("{}", "All configured credentials are valid.".green());
|
||||||
|
} else {
|
||||||
|
println!("{}", "Some credentials are invalid or expired.".red());
|
||||||
|
println!("Use 'pakker credentials set' to update them.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_curseforge(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
api_key: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Use a well-known mod (JEI) to verify key works for mod lookups
|
||||||
|
let response = client
|
||||||
|
.get("https://api.curseforge.com/v1/mods/238222")
|
||||||
|
.header("x-api-key", api_key)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(crate::error::PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_modrinth(client: &reqwest::Client, token: &str) -> Result<()> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.modrinth.com/v2/user")
|
||||||
|
.header("Authorization", token)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(crate::error::PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_github(client: &reqwest::Client, token: &str) -> Result<()> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.github.com/user")
|
||||||
|
.header("Authorization", format!("Bearer {token}"))
|
||||||
|
.header("User-Agent", "pakker")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(crate::error::PakkerError::PlatformApiError(format!(
|
||||||
|
"HTTP {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod cfg;
|
||||||
pub mod cfg_prj;
|
pub mod cfg_prj;
|
||||||
pub mod credentials;
|
pub mod credentials;
|
||||||
pub mod credentials_set;
|
pub mod credentials_set;
|
||||||
|
pub mod credentials_test;
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod fetch;
|
pub mod fetch;
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,10 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
set_args.modrinth_token,
|
set_args.modrinth_token,
|
||||||
set_args.gh_access_token,
|
set_args.gh_access_token,
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
Some(cli::CredentialsSubcommand::Test) => {
|
||||||
|
cli::commands::credentials_test::execute().await
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
cli::commands::credentials::execute(
|
cli::commands::credentials::execute(
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,23 @@ pub fn prompt_curseforge_api_key(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt for a generic secret/token using a secure password input.
|
||||||
|
/// Returns the secret if provided, None if empty or cancelled.
|
||||||
|
pub fn prompt_secret(prompt: &str) -> io::Result<Option<String>> {
|
||||||
|
use dialoguer::Password;
|
||||||
|
|
||||||
|
let secret: String = Password::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.interact()
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
|
||||||
|
if secret.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(secret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue