#![allow(dead_code)]
use crate::core::api::ApiError;
use anyhow::{Context, Result};
use colored::*;
use reqwest::StatusCode;
use std::collections::HashMap;
use crate::models::auth::{CredentialSource, SchemeType, SecuritySchemeDetails};
#[derive(Debug, Clone)]
pub struct AuthDiagnostic {
pub error_type: AuthErrorType,
pub scheme_name: String,
pub details: String,
pub suggestions: Vec<String>,
pub help_commands: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AuthErrorType {
MissingCredentials,
InvalidCredentials,
ExpiredToken,
InsufficientScopes,
NetworkError,
ConfigurationError,
UnsupportedScheme,
}
pub struct AuthDiagnostics {
scheme_details: HashMap<String, SecuritySchemeDetails>,
}
impl AuthDiagnostics {
pub fn new(scheme_details: HashMap<String, SecuritySchemeDetails>) -> Self {
Self { scheme_details }
}
pub fn diagnose_from_response(
&self,
status: StatusCode,
headers: &reqwest::header::HeaderMap,
body: &str,
scheme_name: &str,
) -> AuthDiagnostic {
let scheme = self.scheme_details.get(scheme_name);
match status {
StatusCode::UNAUTHORIZED => self.diagnose_401(headers, body, scheme_name, scheme),
StatusCode::FORBIDDEN => self.diagnose_403(headers, body, scheme_name, scheme),
StatusCode::TOO_MANY_REQUESTS => self.diagnose_429(headers, body, scheme_name, scheme),
_ => self.diagnose_generic(status, body, scheme_name, scheme),
}
}
fn diagnose_401(
&self,
headers: &reqwest::header::HeaderMap,
body: &str,
scheme_name: &str,
scheme: Option<&SecuritySchemeDetails>,
) -> AuthDiagnostic {
let mut suggestions = Vec::new();
let mut help_commands = Vec::new();
let mut error_type = AuthErrorType::InvalidCredentials;
let mut details = String::new();
if let Some(www_auth) = headers.get("www-authenticate") {
if let Ok(www_auth_str) = www_auth.to_str() {
details = format!("Server requires: {}", www_auth_str);
if www_auth_str.contains("Bearer") {
if www_auth_str.contains("error=\"invalid_token\"") {
error_type = AuthErrorType::InvalidCredentials;
suggestions.push(
"Your access token appears to be invalid or malformed".to_string(),
);
suggestions.push(
"The token may have been revoked or incorrectly copied".to_string(),
);
} else if www_auth_str.contains("error=\"expired_token\"") {
error_type = AuthErrorType::ExpiredToken;
suggestions.push("Your access token has expired".to_string());
suggestions
.push("You need to refresh or regenerate your token".to_string());
}
}
}
}
let body_lower = body.to_lowercase();
if body_lower.contains("expired") || body_lower.contains("expir") {
error_type = AuthErrorType::ExpiredToken;
suggestions.push("Token or credentials have expired".to_string());
} else if body_lower.contains("invalid") || body_lower.contains("incorrect") {
suggestions.push("Credentials appear to be invalid".to_string());
} else if body_lower.contains("missing") || body_lower.contains("required") {
error_type = AuthErrorType::MissingCredentials;
suggestions.push("Required authentication credentials are missing".to_string());
}
if let Some(scheme_detail) = scheme {
match scheme_detail.scheme_type {
SchemeType::ApiKey => {
suggestions.push("Check that your API key is correct and active".to_string());
suggestions.push(format!(
"Verify the key is being sent in the {} as '{}'",
scheme_detail
.location
.as_ref()
.map(|l| format!("{:?}", l).to_lowercase())
.unwrap_or_else(|| "header".to_string()),
scheme_detail
.name
.as_ref()
.unwrap_or(&"X-API-Key".to_string())
));
help_commands.push(format!(
"mrapids auth connect {} --auth-type api-key",
scheme_name
));
}
SchemeType::Http => {
if scheme_detail.bearer_format.is_some() {
suggestions.push(
"Ensure your Bearer token is valid and properly formatted".to_string(),
);
suggestions.push(
"Token should be sent as 'Authorization: Bearer <token>'".to_string(),
);
help_commands.push(format!(
"mrapids auth connect {} --auth-type bearer",
scheme_name
));
} else {
suggestions.push("Check your Basic auth username and password".to_string());
suggestions.push("Credentials should be base64 encoded".to_string());
help_commands.push(format!(
"mrapids auth connect {} --auth-type basic",
scheme_name
));
}
}
SchemeType::OAuth2 => {
suggestions.push("Your OAuth2 token may need to be refreshed".to_string());
suggestions.push("Check that the token has the required scopes".to_string());
help_commands.push(format!(
"mrapids auth connect {} --auth-type oauth2 --flow client-credentials",
scheme_name
));
help_commands.push(format!("mrapids auth refresh {}", scheme_name));
}
_ => {}
}
}
help_commands.push(format!("mrapids auth validate --scheme {}", scheme_name));
help_commands.push("mrapids auth detect --operations".to_string());
AuthDiagnostic {
error_type,
scheme_name: scheme_name.to_string(),
details,
suggestions,
help_commands,
}
}
fn diagnose_403(
&self,
_headers: &reqwest::header::HeaderMap,
body: &str,
scheme_name: &str,
scheme: Option<&SecuritySchemeDetails>,
) -> AuthDiagnostic {
let mut suggestions = Vec::new();
let mut help_commands = Vec::new();
let error_type = AuthErrorType::InsufficientScopes;
let body_lower = body.to_lowercase();
let details = if body_lower.contains("scope") || body_lower.contains("permission") {
"You don't have the required permissions or scopes for this operation".to_string()
} else if body_lower.contains("rate") || body_lower.contains("limit") {
"You may have hit a rate limit or quota restriction".to_string()
} else {
"Access to this resource is forbidden with your current credentials".to_string()
};
if let Some(scheme_detail) = scheme {
if scheme_detail.scheme_type == SchemeType::OAuth2 {
suggestions.push("Your token may be missing required scopes".to_string());
suggestions
.push("Check the API documentation for required permissions".to_string());
if body.contains("scope") {
if let Some(scope_match) = extract_scopes_from_error(body) {
suggestions.push(format!("Required scopes: {}", scope_match));
help_commands.push(format!(
"mrapids auth connect {} --scopes \"{}\"",
scheme_name, scope_match
));
}
}
}
}
suggestions.push("Your credentials are valid but lack necessary permissions".to_string());
suggestions.push("Contact the API administrator if you need additional access".to_string());
help_commands.push(format!(
"mrapids auth validate --scheme {} --verbose",
scheme_name
));
help_commands.push("mrapids auth detect --operations".to_string());
AuthDiagnostic {
error_type,
scheme_name: scheme_name.to_string(),
details,
suggestions,
help_commands,
}
}
fn diagnose_429(
&self,
headers: &reqwest::header::HeaderMap,
_body: &str,
scheme_name: &str,
_scheme: Option<&SecuritySchemeDetails>,
) -> AuthDiagnostic {
let mut suggestions = Vec::new();
let mut help_commands = Vec::new();
let mut details = "Rate limit exceeded".to_string();
if let Some(retry_after) = headers.get("retry-after") {
if let Ok(retry_str) = retry_after.to_str() {
details = format!("Rate limit exceeded. Retry after: {} seconds", retry_str);
suggestions.push(format!("Wait {} seconds before retrying", retry_str));
}
}
if let Some(limit) = headers.get("x-ratelimit-limit") {
if let Ok(limit_str) = limit.to_str() {
suggestions.push(format!("Rate limit: {} requests per period", limit_str));
}
}
if let Some(remaining) = headers.get("x-ratelimit-remaining") {
if let Ok(remaining_str) = remaining.to_str() {
suggestions.push(format!("Remaining requests: {}", remaining_str));
}
}
suggestions.push("Consider implementing exponential backoff".to_string());
suggestions.push("Reduce the frequency of your API requests".to_string());
help_commands.push("mrapids config set --rate-limit 10".to_string());
help_commands.push("mrapids config set --retry-delay 1000".to_string());
AuthDiagnostic {
error_type: AuthErrorType::NetworkError,
scheme_name: scheme_name.to_string(),
details,
suggestions,
help_commands,
}
}
fn diagnose_generic(
&self,
status: StatusCode,
body: &str,
scheme_name: &str,
_scheme: Option<&SecuritySchemeDetails>,
) -> AuthDiagnostic {
let mut suggestions = Vec::new();
let mut help_commands = Vec::new();
let details = format!(
"HTTP {} error: {}",
status.as_u16(),
if body.len() > 100 {
format!("{}...", &body[..100])
} else {
body.to_string()
}
);
match status.as_u16() {
400..=499 => {
suggestions.push("Client error - check your request configuration".to_string());
suggestions.push("Verify the API endpoint and parameters".to_string());
}
500..=599 => {
suggestions
.push("Server error - the API service may be experiencing issues".to_string());
suggestions.push("Try again later or contact the API provider".to_string());
}
_ => {
suggestions.push("Unexpected error occurred".to_string());
}
}
help_commands.push(format!(
"mrapids auth validate --scheme {} --debug",
scheme_name
));
help_commands.push("mrapids config show".to_string());
AuthDiagnostic {
error_type: AuthErrorType::NetworkError,
scheme_name: scheme_name.to_string(),
details,
suggestions,
help_commands,
}
}
pub fn display(&self, diagnostic: &AuthDiagnostic) {
println!(
"\n{} {}",
"✗".red().bold(),
"Authentication Error".red().bold()
);
println!("{}", "═".repeat(60).red());
println!("\n{}: {}", "Scheme".yellow(), diagnostic.scheme_name.bold());
println!("{}: {:?}", "Type".yellow(), diagnostic.error_type);
println!("{}: {}", "Details".yellow(), diagnostic.details);
if !diagnostic.suggestions.is_empty() {
println!("\n{}", "Troubleshooting Suggestions:".cyan().bold());
for (i, suggestion) in diagnostic.suggestions.iter().enumerate() {
println!(" {}. {}", i + 1, suggestion);
}
}
if !diagnostic.help_commands.is_empty() {
println!("\n{}", "Try these commands:".green().bold());
for cmd in &diagnostic.help_commands {
println!(" $ {}", cmd.bright_white());
}
}
println!("\n{}", "─".repeat(60).dimmed());
}
pub fn prevalidate_credentials(
&self,
scheme_name: &str,
source: &CredentialSource,
) -> Result<()> {
let _scheme = self
.scheme_details
.get(scheme_name)
.context("Unknown authentication scheme")?;
match source {
CredentialSource::NotConfigured => {
return Err(ApiError::AuthError(format!(
"No credentials configured for '{}'. Run:\n {}",
scheme_name,
format!("mrapids auth connect {}", scheme_name).green()
))
.into());
}
CredentialSource::Environment(var) => {
if std::env::var(var).is_err() {
return Err(ApiError::AuthError(format!(
"Environment variable '{}' is not set. Set it with:\n {}",
var,
format!("export {}=<your-credential>", var).green()
))
.into());
}
}
CredentialSource::ConfigFile(path) => {
if !std::path::Path::new(path).exists() {
return Err(ApiError::AuthError(format!(
"Configuration file '{}' not found. Create it with:\n {}",
path,
format!("mrapids auth connect {}", scheme_name).green()
))
.into());
}
}
_ => {}
}
Ok(())
}
}
fn extract_scopes_from_error(body: &str) -> Option<String> {
let patterns = [
r"required.*scopes?:?\s*([a-zA-Z0-9:_\s,]+)",
r"missing.*scopes?:?\s*([a-zA-Z0-9:_\s,]+)",
r"scopes?.*required:?\s*([a-zA-Z0-9:_\s,]+)",
];
for pattern in patterns {
if let Ok(re) = regex::Regex::new(pattern) {
if let Some(captures) = re.captures(body) {
if let Some(scopes) = captures.get(1) {
return Some(scopes.as_str().trim().to_string());
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::auth::AuthLocation;
use reqwest::header::HeaderMap;
#[test]
fn test_diagnose_401_bearer() {
let mut schemes = HashMap::new();
schemes.insert(
"bearer_auth".to_string(),
SecuritySchemeDetails {
scheme_type: SchemeType::Http,
location: Some(AuthLocation::Header),
name: Some("Authorization".to_string()),
bearer_format: Some("JWT".to_string()),
flows: None,
openid_connect_url: None,
description: None,
},
);
let diagnostics = AuthDiagnostics::new(schemes);
let mut headers = HeaderMap::new();
headers.insert(
"www-authenticate",
"Bearer error=\"invalid_token\"".parse().unwrap(),
);
let diagnostic = diagnostics.diagnose_from_response(
StatusCode::UNAUTHORIZED,
&headers,
"Invalid token",
"bearer_auth",
);
assert_eq!(diagnostic.error_type, AuthErrorType::InvalidCredentials);
assert!(diagnostic
.suggestions
.iter()
.any(|s| s.contains("invalid or malformed")));
assert!(diagnostic
.help_commands
.iter()
.any(|c| c.contains("auth connect")));
}
#[test]
fn test_diagnose_403_scopes() {
let schemes = HashMap::new();
let diagnostics = AuthDiagnostics::new(schemes);
let headers = HeaderMap::new();
let diagnostic = diagnostics.diagnose_from_response(
StatusCode::FORBIDDEN,
&headers,
"Missing required scope: read:users",
"oauth2",
);
assert_eq!(diagnostic.error_type, AuthErrorType::InsufficientScopes);
assert!(diagnostic.details.contains("permissions"));
}
#[test]
fn test_extract_scopes() {
let error = "Error: Missing required scopes: read:users write:posts";
let scopes = extract_scopes_from_error(error);
assert_eq!(scopes, Some("read:users write:posts".to_string()));
}
}