use anyhow::{Context, Result};
use tracing::info;
use crate::client::build_client_with_credentials;
use crate::commands::GlobalOpts;
use crate::config::Config;
use crate::util::{prompt_password, prompt_username};
pub async fn login(
username: Option<String>,
password: Option<String>,
opts: &GlobalOpts,
) -> Result<()> {
let username = match username {
Some(u) => u.to_lowercase(),
None => prompt_username("Username: ")
.context("Failed to read username")?
.to_lowercase(),
};
let password = match password {
Some(p) => p,
None => prompt_password("Password: ").context("Failed to read password")?,
};
if password.is_empty() {
anyhow::bail!("Password cannot be empty");
}
info!("Authenticating as: {}", username);
println!("Logging in as: {}", username);
let client = build_client_with_credentials(&username, &password).await?;
let token_manager = client
.token_manager()
.context("Client not configured for authentication")?;
token_manager
.authenticate()
.await
.map_err(|e| anyhow::anyhow!("Login failed: {}", e))?;
let tokens = token_manager
.get_tokens()
.context("Failed to retrieve tokens after login")?;
use std::time::Instant;
let expires_in = tokens.expires_at.duration_since(Instant::now()).as_secs();
let expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in as i64);
let mut config = opts.config.clone();
config.set_tokens(
&username,
&tokens.access_token,
&tokens.refresh_token,
expires_in,
);
config.save(None)?;
println!("✓ Login successful!");
println!(" Token stored at: {}", Config::default_path()?.display());
println!(" Token expires at: {}", expires_at.to_rfc3339());
Ok(())
}
pub async fn refresh(
username: Option<String>,
refresh_token: Option<String>,
opts: &GlobalOpts,
) -> Result<()> {
let refresh_token = refresh_token
.or_else(|| opts.config.refresh_token.clone())
.context("No refresh token available. Please login first.")?;
let username = username
.or_else(|| opts.config.username.clone())
.context("No username available. Please login first.")?
.to_lowercase();
info!("Refreshing token for: {}", username);
println!("Refreshing token...");
let client = build_client_with_credentials(&username, "unused").await?;
let token_manager = client
.token_manager()
.context("Client not configured for authentication")?;
use std::time::Instant;
use wme_client::Tokens;
let tokens = Tokens {
id_token: String::new(),
access_token: opts.config.access_token.clone().unwrap_or_default(),
refresh_token,
expires_at: Instant::now(), };
token_manager.set_tokens(tokens);
token_manager
.refresh()
.await
.map_err(|e| anyhow::anyhow!("Token refresh failed: {}", e))?;
let new_tokens = token_manager
.get_tokens()
.context("Failed to retrieve tokens after refresh")?;
let expires_in = new_tokens
.expires_at
.duration_since(Instant::now())
.as_secs();
let mut config = opts.config.clone();
config.set_tokens(
&username,
&new_tokens.access_token,
&new_tokens.refresh_token,
expires_in,
);
config.save(None)?;
println!("✓ Token refreshed successfully!");
println!(" Token expires in: {} seconds", expires_in);
Ok(())
}
pub async fn revoke(refresh_token: Option<String>, opts: &GlobalOpts) -> Result<()> {
let refresh_token = refresh_token.or_else(|| opts.config.refresh_token.clone());
if let Some(token) = refresh_token {
info!("Revoking token");
println!("Revoking token...");
let transport = std::sync::Arc::new(
wme_client::ReqwestTransport::new().context("Failed to create HTTP transport")?,
);
let url = "https://auth.enterprise.wikimedia.com/v1/token-revoke";
let body = serde_json::json!({
"refresh_token": token,
});
use wme_client::HttpTransport;
let response = transport
.request(reqwest::Method::POST, url, None, Some(body.to_string()))
.await;
match response {
Ok(resp) if resp.status().is_success() || resp.status().as_u16() == 204 => {
println!("✓ Token revoked successfully");
}
Ok(resp) => {
println!("⚠ Token revoke returned status: {}", resp.status());
}
Err(e) => {
println!("⚠ Failed to contact revoke endpoint: {}", e);
}
}
}
let mut config = opts.config.clone();
config.clear_tokens();
config.save(None)?;
println!("✓ Local tokens cleared");
Ok(())
}
pub async fn forgot_password(username: &str, _opts: &GlobalOpts) -> Result<()> {
println!("Sending forgot password request for: {}", username);
let transport = std::sync::Arc::new(
wme_client::ReqwestTransport::new().context("Failed to create HTTP transport")?,
);
let url = "https://auth.enterprise.wikimedia.com/v1/forgot-password";
let body = serde_json::json!({
"username": username,
});
use wme_client::HttpTransport;
let response = transport
.request(reqwest::Method::POST, url, None, Some(body.to_string()))
.await
.map_err(|e| anyhow::anyhow!("Forgot password request failed: {}", e))?;
if response.status().is_success() {
println!("✓ Forgot password email sent. Check your inbox for the confirmation code.");
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Forgot password request failed ({}): {}", status, text);
}
Ok(())
}
pub async fn forgot_password_confirm(
username: &str,
password: &str,
code: &str,
_opts: &GlobalOpts,
) -> Result<()> {
println!("Confirming forgot password for: {}", username);
let transport = std::sync::Arc::new(
wme_client::ReqwestTransport::new().context("Failed to create HTTP transport")?,
);
let url = "https://auth.enterprise.wikimedia.com/v1/forgot-password-confirm";
let body = serde_json::json!({
"username": username,
"password": password,
"code": code,
});
use wme_client::HttpTransport;
let response = transport
.request(reqwest::Method::POST, url, None, Some(body.to_string()))
.await
.map_err(|e| anyhow::anyhow!("Forgot password confirm request failed: {}", e))?;
if response.status().is_success() {
println!("✓ Password reset successful! You can now login with your new password.");
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Password reset failed ({}): {}", status, text);
}
Ok(())
}
pub async fn change_password(
previous_password: &str,
proposed_password: &str,
opts: &GlobalOpts,
) -> Result<()> {
let token = opts
.get_token()
.context("Not authenticated. Please run 'wme auth login' first.")?;
println!("Changing password...");
let transport = std::sync::Arc::new(
wme_client::ReqwestTransport::new().context("Failed to create HTTP transport")?,
);
let url = "https://auth.enterprise.wikimedia.com/v1/change-password";
let body = serde_json::json!({
"previous_password": previous_password,
"proposed_password": proposed_password,
});
let mut headers = std::collections::HashMap::new();
headers.insert("Authorization".to_string(), format!("Bearer {}", token));
use wme_client::HttpTransport;
let response = transport
.request(
reqwest::Method::POST,
url,
Some(headers),
Some(body.to_string()),
)
.await
.map_err(|e| anyhow::anyhow!("Change password request failed: {}", e))?;
if response.status().is_success() {
println!("✓ Password changed successfully!");
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Password change failed ({}): {}", status, text);
}
Ok(())
}
pub async fn set_new_password(
username: &str,
session: &str,
new_password: &str,
_opts: &GlobalOpts,
) -> Result<()> {
println!("Setting new password for: {}", username);
let transport = std::sync::Arc::new(
wme_client::ReqwestTransport::new().context("Failed to create HTTP transport")?,
);
let url = "https://auth.enterprise.wikimedia.com/v1/set-new-password";
let body = serde_json::json!({
"username": username,
"session": session,
"new_password": new_password,
});
use wme_client::HttpTransport;
let response = transport
.request(reqwest::Method::POST, url, None, Some(body.to_string()))
.await
.map_err(|e| anyhow::anyhow!("Set new password request failed: {}", e))?;
if response.status().is_success() {
println!("✓ New password set successfully! You can now login.");
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Set new password failed ({}): {}", status, text);
}
Ok(())
}