use crate::error::{PixivError, Result};
use crate::network::HttpClient;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct AuthClient {
client: HttpClient,
auth_url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthResponse {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
pub user: User,
#[serde(skip)]
pub obtained_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub account: String,
pub email: Option<String>,
pub profile_image_urls: ProfileImageUrls,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProfileImageUrls {
pub px_16x16: Option<String>,
pub px_50x50: Option<String>,
pub px_170x170: Option<String>,
}
impl AuthClient {
pub fn new() -> Result<Self> {
let client = HttpClient::new()?;
Ok(Self {
client,
auth_url: "https://oauth.secure.pixiv.net/auth/token".to_string(),
})
}
pub async fn login(&mut self, username: &str, password: &str) -> Result<AuthResponse> {
debug!(username = %username, "Attempting login");
let security_headers = self.client.generate_security_headers();
let mut form_data = HashMap::new();
form_data.insert("client_id", "MOBrBDS8blbauoSck0ZfDbtuzpyT");
form_data.insert("client_secret", "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj");
form_data.insert("grant_type", "password");
form_data.insert("username", username);
form_data.insert("password", password);
form_data.insert("get_secure_url", "true");
let mut request = self.client.client.post(&self.auth_url);
for (key, value) in security_headers {
request = request.header(&key, value);
}
request = request.form(&form_data);
let response = request.send().await?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_else(|_| "Failed to get error information".to_string());
warn!(error = %error_text, "Login failed");
return Err(PixivError::AuthError(format!("Login failed: {}", error_text)));
}
let mut auth_response: AuthResponse = response.json().await?;
auth_response.obtained_at = Utc::now();
self.client.set_access_token(auth_response.access_token.clone());
self.client.set_refresh_token(auth_response.refresh_token.clone());
info!(user_id = %auth_response.user.id, "Login successful");
Ok(auth_response)
}
pub async fn refresh_access_token(&mut self, refresh_token: &str) -> Result<AuthResponse> {
debug!("Refreshing access token");
let security_headers = self.client.generate_security_headers();
let mut form_data = HashMap::new();
form_data.insert("client_id", "MOBrBDS8blbauoSck0ZfDbtuzpyT");
form_data.insert("client_secret", "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj");
form_data.insert("grant_type", "refresh_token");
form_data.insert("refresh_token", refresh_token);
form_data.insert("get_secure_url", "true");
let mut request = self.client.client.post(&self.auth_url);
for (key, value) in security_headers {
request = request.header(&key, value);
}
request = request.form(&form_data);
let response = request.send().await?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_else(|_| "Failed to get error information".to_string());
warn!(error = %error_text, "Token refresh failed");
return Err(PixivError::AuthError(format!("Token refresh failed: {}", error_text)));
}
let mut auth_response: AuthResponse = response.json().await?;
auth_response.obtained_at = Utc::now();
self.client.set_access_token(auth_response.access_token.clone());
self.client.set_refresh_token(auth_response.refresh_token.clone());
info!("Access token refreshed successfully");
Ok(auth_response)
}
pub fn is_token_expired(&self, auth_response: &AuthResponse) -> bool {
let now = Utc::now();
let expires_at = auth_response.obtained_at + chrono::Duration::seconds(auth_response.expires_in as i64);
let buffer = chrono::Duration::minutes(5);
now + buffer > expires_at
}
pub fn client_mut(&mut self) -> &mut HttpClient {
&mut self.client
}
pub fn client(&self) -> &HttpClient {
&self.client
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_client_creation() {
let result = AuthClient::new();
assert!(result.is_ok());
}
#[test]
fn test_token_expiry_check() {
let mut auth_response = AuthResponse {
access_token: "test_token".to_string(),
refresh_token: "refresh_token".to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600, user: User {
id: 12345,
name: "Test User".to_string(),
account: "testuser".to_string(),
email: None,
profile_image_urls: ProfileImageUrls {
px_16x16: None,
px_50x50: None,
px_170x170: None,
},
},
obtained_at: Utc::now(),
};
let auth_client = AuthClient::new().unwrap();
assert!(!auth_client.is_token_expired(&auth_response));
auth_response.obtained_at = Utc::now() - chrono::Duration::hours(2);
assert!(auth_client.is_token_expired(&auth_response));
}
}