use std::env;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use crate::constants::{auth, env as env_constants};
use crate::errors::{AuthError, AuthResult};
#[derive(Debug, Clone)]
pub struct AuthStatus {
pub username_set: bool,
pub password_set: bool,
pub dotenv_file_exists: bool,
pub credentials_valid: Option<bool>,
}
impl AuthStatus {
pub fn has_credentials(&self) -> bool {
self.username_set && self.password_set
}
pub fn status_message(&self) -> String {
match (self.has_credentials(), self.credentials_valid) {
(false, _) => "Missing credentials - run 'auth setup' to configure".to_string(),
(true, None) => "Credentials configured but not verified".to_string(),
(true, Some(true)) => "Credentials configured and verified".to_string(),
(true, Some(false)) => "Credentials configured but invalid".to_string(),
}
}
}
pub fn get_auth_status() -> AuthStatus {
AuthStatus {
username_set: env::var(env_constants::USERNAME).is_ok(),
password_set: env::var(env_constants::PASSWORD).is_ok(),
dotenv_file_exists: Path::new(".env").exists(),
credentials_valid: None,
}
}
pub fn check_credentials() -> bool {
env::var(env_constants::USERNAME).is_ok() && env::var(env_constants::PASSWORD).is_ok()
}
pub fn prompt_credentials() -> AuthResult<(String, String)> {
print!("CEDA Username: ");
io::stdout().flush().map_err(AuthError::CredentialStorage)?;
let mut username = String::new();
io::stdin()
.read_line(&mut username)
.map_err(AuthError::CredentialStorage)?;
let username = username.trim().to_string();
if username.is_empty() {
return Err(AuthError::InvalidUsername {
reason: "Username cannot be empty".to_string(),
});
}
if !is_valid_username(&username) {
return Err(AuthError::InvalidUsername {
reason: "Username should be alphanumeric with optional dots, hyphens, or underscores"
.to_string(),
});
}
let password = rpassword::prompt_password("CEDA Password: ")
.map_err(|e| AuthError::CredentialStorage(io::Error::new(io::ErrorKind::Other, e)))?;
if password.is_empty() {
return Err(AuthError::InvalidUsername {
reason: "Password cannot be empty".to_string(),
});
}
Ok((username, password))
}
fn is_valid_username(username: &str) -> bool {
if username.len() < auth::MIN_USERNAME_LENGTH || username.len() > auth::MAX_USERNAME_LENGTH {
return false;
}
username
.chars()
.all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_')
}
pub fn save_credentials(username: &str, password: &str) -> AuthResult<()> {
let env_path = Path::new(".env");
let mut existing_lines = Vec::new();
let mut username_found = false;
let mut password_found = false;
if env_path.exists() {
let file = File::open(env_path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.starts_with(&format!("{}=", env_constants::USERNAME)) {
existing_lines.push(format!("{}={}", env_constants::USERNAME, username));
username_found = true;
} else if trimmed.starts_with(&format!("{}=", env_constants::PASSWORD)) {
existing_lines.push(format!("{}={}", env_constants::PASSWORD, password));
password_found = true;
} else {
existing_lines.push(line);
}
}
}
if !username_found {
existing_lines.push(format!("{}={}", env_constants::USERNAME, username));
}
if !password_found {
existing_lines.push(format!("{}={}", env_constants::PASSWORD, password));
}
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(env_path)?;
for line in existing_lines {
writeln!(file, "{}", line)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = file.metadata()?.permissions();
perms.set_mode(auth::ENV_FILE_PERMISSIONS);
file.set_permissions(perms)?;
}
unsafe {
env::set_var(env_constants::USERNAME, username);
env::set_var(env_constants::PASSWORD, password);
}
println!("Credentials saved to .env file");
#[cfg(unix)]
println!("File permissions set to owner-only (600)");
#[cfg(not(unix))]
println!(
"Warning: File permissions not set (non-Unix system). Please ensure .env file is protected."
);
Ok(())
}
pub async fn verify_credentials() -> AuthResult<bool> {
if !check_credentials() {
return Err(AuthError::MissingCredentials);
}
println!("Verifying credentials with CEDA...");
match crate::app::CedaClient::new().await {
Ok(_) => {
println!("Credentials verified successfully!");
Ok(true)
}
Err(e) => {
println!("Credential verification failed: {}", e);
Ok(false)
}
}
}
pub async fn setup_credentials() -> AuthResult<()> {
println!("CEDA Authentication Setup");
println!("===============================");
println!();
println!("This will help you configure your CEDA credentials for accessing the archive.");
println!("Your credentials will be stored in a .env file in the current directory.");
println!();
let status = get_auth_status();
if status.has_credentials() {
println!("Warning: Credentials are already configured.");
print!("Do you want to update them? [y/N]: ");
io::stdout().flush().map_err(AuthError::CredentialStorage)?;
let mut response = String::new();
io::stdin()
.read_line(&mut response)
.map_err(AuthError::CredentialStorage)?;
if !response.trim().to_lowercase().starts_with('y') {
println!("Setup cancelled.");
return Ok(());
}
println!();
}
let (username, password) = prompt_credentials()?;
println!();
println!("Saving credentials...");
save_credentials(&username, &password)?;
println!();
println!("Verifying credentials...");
let is_valid = verify_credentials().await?;
if is_valid {
println!();
println!("Setup complete! You can now use CEDA fetcher commands.");
} else {
println!();
println!("Setup failed. Please check your credentials and try again.");
println!(" You can run 'auth setup' again to re-enter your credentials.");
}
Ok(())
}
pub async fn show_auth_status() -> AuthResult<()> {
let mut status = get_auth_status();
println!("CEDA Authentication Status");
println!("=============================");
println!();
if let Ok(username) = env::var(env_constants::USERNAME) {
println!("Username: {} (set)", username);
} else {
println!("Username: Not set");
}
println!(
"Password: {}",
if status.password_set {
"Set"
} else {
"Not set"
}
);
println!(
".env file: {}",
if status.dotenv_file_exists {
"Exists"
} else {
"Not found"
}
);
println!();
if status.has_credentials() {
println!("Testing credentials...");
let is_valid = verify_credentials().await?;
status.credentials_valid = Some(is_valid);
println!();
}
println!("Status: {}", status.status_message());
if !status.has_credentials() {
println!();
println!("To configure credentials, run: midas_fetcher auth setup");
} else if status.credentials_valid == Some(false) {
println!();
println!("To update credentials, run: midas_fetcher auth setup");
}
Ok(())
}
pub async fn ensure_authenticated() -> AuthResult<()> {
if !check_credentials() {
println!("This command requires CEDA authentication credentials.");
println!();
print!("Would you like to set up authentication now? [Y/n]: ");
io::stdout().flush().map_err(AuthError::CredentialStorage)?;
let mut response = String::new();
io::stdin()
.read_line(&mut response)
.map_err(AuthError::CredentialStorage)?;
if response.trim().to_lowercase().starts_with('n') {
return Err(AuthError::MissingCredentials);
}
println!();
setup_credentials().await?;
if !check_credentials() {
return Err(AuthError::MissingCredentials);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_valid_username() {
assert!(is_valid_username("testuser"));
assert!(is_valid_username("test.user"));
assert!(is_valid_username("test-user"));
assert!(is_valid_username("test_user"));
assert!(is_valid_username("test123"));
assert!(is_valid_username("user.name123"));
assert!(!is_valid_username("")); assert!(!is_valid_username("ab")); assert!(!is_valid_username("test user")); assert!(!is_valid_username("test@user")); assert!(!is_valid_username(&"a".repeat(51))); }
#[test]
fn test_auth_status_structure() {
let status = get_auth_status();
let message = status.status_message();
assert!(!message.is_empty());
let has_creds = status.has_credentials();
assert_eq!(has_creds, status.username_set && status.password_set);
}
#[test]
fn test_auth_status_messages() {
let mut status = AuthStatus {
username_set: false,
password_set: false,
dotenv_file_exists: false,
credentials_valid: None,
};
assert!(status.status_message().contains("Missing credentials"));
status.username_set = true;
status.password_set = true;
assert!(status.status_message().contains("not verified"));
status.credentials_valid = Some(true);
assert!(status.status_message().contains("verified"));
status.credentials_valid = Some(false);
assert!(status.status_message().contains("invalid"));
}
#[tokio::test]
async fn test_verify_credentials_missing() {
unsafe {
env::remove_var(env_constants::USERNAME);
env::remove_var(env_constants::PASSWORD);
}
let result = verify_credentials().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AuthError::MissingCredentials));
}
#[test]
fn test_check_credentials() {
let original_username = env::var(env_constants::USERNAME).ok();
let original_password = env::var(env_constants::PASSWORD).ok();
unsafe {
env::remove_var(env_constants::USERNAME);
env::remove_var(env_constants::PASSWORD);
assert!(!check_credentials());
env::set_var(env_constants::USERNAME, "testuser");
assert!(!check_credentials());
env::set_var(env_constants::PASSWORD, "testpass");
assert!(check_credentials());
if let Some(username) = original_username {
env::set_var(env_constants::USERNAME, username);
} else {
env::remove_var(env_constants::USERNAME);
}
if let Some(password) = original_password {
env::set_var(env_constants::PASSWORD, password);
} else {
env::remove_var(env_constants::PASSWORD);
}
}
}
#[test]
fn test_save_credentials_new_file() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let env_path = temp_dir.path().join(".env");
let original_dir = env::current_dir()?;
env::set_current_dir(&temp_dir)?;
let result = save_credentials("testuser", "testpass");
assert!(result.is_ok());
assert!(env_path.exists());
let contents = std::fs::read_to_string(&env_path)?;
assert!(contents.contains("CEDA_USERNAME=testuser"));
assert!(contents.contains("CEDA_PASSWORD=testpass"));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&env_path)?;
let permissions = metadata.permissions();
assert_eq!(permissions.mode() & 0o777, 0o600);
}
env::set_current_dir(original_dir)?;
Ok(())
}
}