use serde::{Deserialize, Serialize};
use crate::error::{ApiErrorResponse, Error, Result};
use crate::internal::{V1_API_URL, build_authenticated_client, build_rate_limiter};
use crate::models::{Credentials, FullUser};
use super::{Authenticated, Client, Unauthenticated};
#[derive(Serialize)]
struct LoginRequest<'a> {
auth_type: &'static str,
email: &'a str,
password: &'a str,
device_id: &'a str,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct LoginResponse {
payload: LoginPayload,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct LoginPayload {
user: FullUser,
}
impl Client<Unauthenticated> {
pub async fn login(self, mut credentials: Credentials) -> Result<Client<Authenticated>> {
let token = if let Some(token) = credentials.token() {
token.to_string()
} else if let Some(password) = credentials.password() {
let token = self
.perform_login(&credentials.email, password, &credentials.device_id)
.await?;
credentials.set_token(token.clone());
token
} else {
return Err(Error::auth(
"Credentials must have either password or token",
));
};
let http = build_authenticated_client(
self.config.platform,
self.config.language,
self.config.crossplay,
&token,
)
.map_err(Error::Network)?;
let limiter = if self.config.rate_limit == 3 {
self.limiter
} else {
build_rate_limiter(self.config.rate_limit)
};
Ok(Client::new_authenticated(
http,
self.config,
limiter,
credentials,
))
}
async fn perform_login(&self, email: &str, password: &str, device_id: &str) -> Result<String> {
let request = LoginRequest {
auth_type: "header",
email,
password,
device_id,
};
let response = self
.http
.post(format!("{}/auth/signin", V1_API_URL))
.header("Authorization", "JWT")
.json(&request)
.send()
.await
.map_err(Error::Network)?;
let status = response.status();
let headers = response.headers().clone();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&body) {
return Err(Error::auth_with_details(
format!("Login failed: {}", status),
error_response,
));
}
return Err(Error::auth(format!(
"Login failed with status {}: {}",
status, body
)));
}
let auth_header = headers
.get("Authorization")
.ok_or_else(|| Error::auth("No Authorization header in response"))?
.to_str()
.map_err(|_| Error::auth("Invalid Authorization header encoding"))?;
let token = auth_header
.strip_prefix("JWT ")
.ok_or_else(|| Error::auth("Invalid Authorization header format"))?
.to_string();
Ok(token)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_login_request_serialization() {
let request = LoginRequest {
auth_type: "header",
email: "test@example.com",
password: "password123",
device_id: "device-123",
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"auth_type\":\"header\""));
assert!(json.contains("\"email\":\"test@example.com\""));
}
}