use crate::error::{ZitiError, ZitiResult};
use crate::identity::IdentityManager;
use crate::transport::http::controller_client;
use serde::Deserialize;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use time::OffsetDateTime;
#[derive(Debug, Clone)]
pub struct ApiSession {
pub id: String,
pub token: String,
pub expires_at: SystemTime,
}
impl ApiSession {
pub fn new(id: String, token: String, expires_at: SystemTime) -> Self {
Self {
id,
token,
expires_at,
}
}
pub fn is_expired(&self) -> bool {
self.is_expired_with_buffer(Duration::from_secs(30))
}
pub fn is_expired_with_buffer(&self, buffer: Duration) -> bool {
SystemTime::now() + buffer >= self.expires_at
}
}
#[derive(Debug, Deserialize)]
struct AuthResponse {
data: AuthData,
}
#[derive(Debug, Deserialize)]
struct AuthData {
id: String,
token: String,
#[serde(rename = "expiresAt")]
expires_at: String,
}
pub async fn authenticate(identity_manager: &IdentityManager) -> ZitiResult<ApiSession> {
let client = controller_client(identity_manager, Duration::from_secs(30)).await?;
let auth_url = format!(
"{}/authenticate?method=cert",
identity_manager.zt_api().trim_end_matches('/')
);
let response = client
.post(&auth_url)
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.map_err(|e| ZitiError::AuthenticationFailed {
reason: format!("Failed to send authentication request: {}", e),
})?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(ZitiError::AuthenticationFailed {
reason: format!(
"Authentication failed with status {}: {}",
status, error_text
),
});
}
let auth_response: AuthResponse =
response
.json()
.await
.map_err(|e| ZitiError::AuthenticationFailed {
reason: format!("Failed to parse authentication response: {}", e),
})?;
let expires_at = parse_ziti_timestamp(&auth_response.data.expires_at)?;
Ok(ApiSession::new(
auth_response.data.id,
auth_response.data.token,
expires_at,
))
}
fn parse_ziti_timestamp(timestamp: &str) -> ZitiResult<SystemTime> {
let offset_dt = OffsetDateTime::parse(
timestamp,
&time::format_description::well_known::Iso8601::DEFAULT,
)
.map_err(|e| {
ZitiError::ConfigError(format!("Failed to parse timestamp {}: {}", timestamp, e))
})?;
let unix_seconds = offset_dt.unix_timestamp();
if unix_seconds < 0 {
return Err(ZitiError::ConfigError(format!(
"Timestamp {} predates the Unix epoch",
timestamp
)));
}
let duration_since_epoch = Duration::from_secs(unix_seconds as u64);
Ok(UNIX_EPOCH + duration_since_epoch)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[test]
fn test_api_session_creation() {
let id = "test-session-id".to_string();
let token = "test-token".to_string();
let expires_at = SystemTime::now() + Duration::from_secs(3600);
let session = ApiSession::new(id.clone(), token.clone(), expires_at);
assert_eq!(session.id, id);
assert_eq!(session.token, token);
assert_eq!(session.expires_at, expires_at);
}
#[test]
fn test_api_session_is_expired() {
let future_time = SystemTime::now() + Duration::from_secs(3600);
let session = ApiSession::new(
"test-id".to_string(),
"test-token".to_string(),
future_time,
);
assert!(!session.is_expired());
let past_time = SystemTime::now() - Duration::from_secs(3600);
let expired_session = ApiSession::new(
"test-id".to_string(),
"test-token".to_string(),
past_time,
);
assert!(expired_session.is_expired());
}
#[test]
fn test_api_session_is_expired_with_buffer() {
let future_time = SystemTime::now() + Duration::from_secs(60);
let session = ApiSession::new(
"test-id".to_string(),
"test-token".to_string(),
future_time,
);
assert!(!session.is_expired_with_buffer(Duration::from_secs(10)));
assert!(session.is_expired_with_buffer(Duration::from_secs(120)));
}
#[test]
fn test_parse_ziti_timestamp() {
let timestamp = "2023-12-07T10:30:00.000Z";
let result = parse_ziti_timestamp(timestamp);
assert!(result.is_ok());
let system_time = result.unwrap();
let duration_since_epoch = system_time.duration_since(UNIX_EPOCH).unwrap();
assert!(duration_since_epoch.as_secs() > 0);
}
#[test]
fn test_parse_invalid_timestamp() {
let invalid_timestamp = "invalid-timestamp";
let result = parse_ziti_timestamp(invalid_timestamp);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ZitiError::ConfigError(_)));
}
#[test]
fn test_auth_response_deserialization() {
let json_data = r#"
{
"data": {
"id": "session-123",
"token": "token-abc",
"expiresAt": "2023-12-07T10:30:00.000Z"
}
}
"#;
let result: Result<AuthResponse, _> = serde_json::from_str(json_data);
assert!(result.is_ok());
let auth_response = result.unwrap();
assert_eq!(auth_response.data.id, "session-123");
assert_eq!(auth_response.data.token, "token-abc");
assert_eq!(auth_response.data.expires_at, "2023-12-07T10:30:00.000Z");
}
}