298 lines
9 KiB
Rust
298 lines
9 KiB
Rust
use crate::domain::auth::Authenticator;
|
|
use crate::error::{AppError, AppResult};
|
|
use chrono::{DateTime, Duration, Utc};
|
|
use reqwest::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CalendarEvent {
|
|
pub id: Option<i64>,
|
|
pub name: String,
|
|
pub from: DateTime<Utc>,
|
|
pub to: DateTime<Utc>,
|
|
pub user_sub: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateEventRequest {
|
|
pub name: String,
|
|
pub from: String,
|
|
pub to: String,
|
|
pub user_sub: Option<String>,
|
|
}
|
|
struct TokenState {
|
|
access_token: String,
|
|
expires_at: DateTime<Utc>,
|
|
}
|
|
|
|
pub struct CalendarClient {
|
|
base_url: String,
|
|
client: Client,
|
|
authenticator: Arc<Authenticator>,
|
|
token_state: RwLock<Option<TokenState>>,
|
|
}
|
|
|
|
impl CalendarClient {
|
|
pub fn new(base_url: String, authenticator: Arc<Authenticator>) -> Self {
|
|
let client = Client::builder()
|
|
.timeout(std::time::Duration::from_secs(30))
|
|
.connect_timeout(std::time::Duration::from_secs(10))
|
|
.build()
|
|
.unwrap_or_else(|_| Client::new());
|
|
|
|
Self {
|
|
base_url: base_url.trim_end_matches('/').to_string(),
|
|
client,
|
|
authenticator,
|
|
token_state: RwLock::new(None),
|
|
}
|
|
}
|
|
|
|
async fn get_token(&self) -> AppResult<String> {
|
|
{
|
|
let state = self.token_state.read().await;
|
|
if let Some(token) = &*state {
|
|
if token.expires_at > Utc::now() + Duration::seconds(30) {
|
|
tracing::debug!("Using cached Calendar API token");
|
|
return Ok(token.access_token.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut state = self.token_state.write().await;
|
|
// Double check after acquiring write lock
|
|
if let Some(token) = &*state {
|
|
if token.expires_at > Utc::now() + Duration::seconds(30) {
|
|
return Ok(token.access_token.clone());
|
|
}
|
|
}
|
|
|
|
tracing::info!("Refreshing Calendar API token via Client Credentials flow");
|
|
let token_data = self
|
|
.authenticator
|
|
.client_credentials("profile")
|
|
.await
|
|
.map_err(|e| AppError::Internal(format!("Failed to get client credentials: {}", e)))?;
|
|
|
|
let access_token = token_data["access_token"]
|
|
.as_str()
|
|
.ok_or_else(|| AppError::Internal("Missing access_token in response".into()))?
|
|
.to_string();
|
|
|
|
let expires_in = token_data["expires_in"].as_i64().unwrap_or(3600);
|
|
|
|
let expires_at = Utc::now() + Duration::seconds(expires_in);
|
|
|
|
*state = Some(TokenState {
|
|
access_token: access_token.clone(),
|
|
expires_at,
|
|
});
|
|
|
|
Ok(access_token)
|
|
}
|
|
|
|
pub async fn list_events(
|
|
&self,
|
|
user_sub: Option<String>,
|
|
upcoming: Option<bool>,
|
|
) -> AppResult<Vec<CalendarEvent>> {
|
|
let token = self.get_token().await?;
|
|
let mut url = format!("{}/service/v1/events", self.base_url);
|
|
let mut params = Vec::new();
|
|
if let Some(uid) = &user_sub {
|
|
params.push(format!("user_sub={}", uid));
|
|
}
|
|
if let Some(u) = upcoming {
|
|
params.push(format!("upcoming={}", u));
|
|
}
|
|
|
|
if !params.is_empty() {
|
|
url.push_str("?");
|
|
url.push_str(¶ms.join("&"));
|
|
}
|
|
|
|
tracing::info!(method = "GET", %url, "Sending Calendar API request");
|
|
|
|
let res = self
|
|
.client
|
|
.get(&url)
|
|
.bearer_auth(token)
|
|
.send()
|
|
.await
|
|
.map_err(AppError::Network)?;
|
|
|
|
let status = res.status();
|
|
tracing::info!(%status, %url, "Received Calendar API response");
|
|
|
|
if !status.is_success() {
|
|
let error_body = res.text().await.unwrap_or_default();
|
|
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
|
|
return Err(AppError::Internal(format!(
|
|
"Failed to list events: {} - {}",
|
|
status, error_body
|
|
)));
|
|
}
|
|
|
|
let body = res
|
|
.json()
|
|
.await
|
|
.map_err(|e| AppError::Internal(e.to_string()));
|
|
tracing::info!(%status, %url, body = ?body, "Received Calendar API response");
|
|
body
|
|
}
|
|
|
|
pub async fn create_event(
|
|
&self,
|
|
user_sub: Option<String>,
|
|
name: &str,
|
|
from: &str,
|
|
to: &str,
|
|
) -> AppResult<CalendarEvent> {
|
|
let token = self.get_token().await?;
|
|
let url = format!("{}/service/v1/events", self.base_url);
|
|
|
|
let request = CreateEventRequest {
|
|
name: name.to_string(),
|
|
from: from.to_string(),
|
|
to: to.to_string(),
|
|
user_sub,
|
|
};
|
|
|
|
tracing::info!(method = "POST", %url, "Sending Calendar API request");
|
|
|
|
let res = self
|
|
.client
|
|
.post(&url)
|
|
.bearer_auth(token)
|
|
.json(&request)
|
|
.send()
|
|
.await
|
|
.map_err(AppError::Network)?;
|
|
|
|
let status = res.status();
|
|
tracing::info!(%status, %url, "Received Calendar API response");
|
|
|
|
if !status.is_success() {
|
|
let error_body = res.text().await.unwrap_or_default();
|
|
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
|
|
return Err(AppError::Internal(format!(
|
|
"Failed to create event: {} - {}",
|
|
status, error_body
|
|
)));
|
|
}
|
|
|
|
res.json()
|
|
.await
|
|
.map_err(|e| AppError::Internal(e.to_string()))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub async fn get_event(&self, id: i32) -> AppResult<CalendarEvent> {
|
|
let token = self.get_token().await?;
|
|
let url = format!("{}/service/v1/events/{}", self.base_url, id);
|
|
tracing::info!(method = "GET", %url, "Sending Calendar API request");
|
|
|
|
let res = self
|
|
.client
|
|
.get(&url)
|
|
.bearer_auth(token)
|
|
.send()
|
|
.await
|
|
.map_err(AppError::Network)?;
|
|
|
|
let status = res.status();
|
|
tracing::info!(%status, %url, "Received Calendar API response");
|
|
|
|
if !status.is_success() {
|
|
let error_body = res.text().await.unwrap_or_default();
|
|
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
|
|
return Err(AppError::Internal(format!(
|
|
"Failed to get event: {} - {}",
|
|
status, error_body
|
|
)));
|
|
}
|
|
|
|
res.json()
|
|
.await
|
|
.map_err(|e| AppError::Internal(e.to_string()))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub async fn update_event(
|
|
&self,
|
|
id: i32,
|
|
user_sub: Option<String>,
|
|
name: &str,
|
|
from: &str,
|
|
to: &str,
|
|
) -> AppResult<CalendarEvent> {
|
|
let token = self.get_token().await?;
|
|
let url = format!("{}/service/v1/events/{}", self.base_url, id);
|
|
|
|
let request = CreateEventRequest {
|
|
name: name.to_string(),
|
|
from: from.to_string(),
|
|
to: to.to_string(),
|
|
user_sub,
|
|
};
|
|
|
|
tracing::info!(method = "PUT", %url, "Sending Calendar API request");
|
|
|
|
let res = self
|
|
.client
|
|
.put(&url)
|
|
.bearer_auth(token)
|
|
.json(&request)
|
|
.send()
|
|
.await
|
|
.map_err(AppError::Network)?;
|
|
|
|
let status = res.status();
|
|
tracing::info!(%status, %url, "Received Calendar API response");
|
|
|
|
if !status.is_success() {
|
|
let error_body = res.text().await.unwrap_or_default();
|
|
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
|
|
return Err(AppError::Internal(format!(
|
|
"Failed to update event: {} - {}",
|
|
status, error_body
|
|
)));
|
|
}
|
|
|
|
res.json()
|
|
.await
|
|
.map_err(|e| AppError::Internal(e.to_string()))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub async fn delete_event(&self, id: i32) -> AppResult<()> {
|
|
let token = self.get_token().await?;
|
|
let url = format!("{}/service/v1/events/{}", self.base_url, id);
|
|
|
|
tracing::info!(method = "DELETE", %url, "Sending Calendar API request");
|
|
|
|
let res = self
|
|
.client
|
|
.delete(&url)
|
|
.bearer_auth(token)
|
|
.send()
|
|
.await
|
|
.map_err(AppError::Network)?;
|
|
|
|
let status = res.status();
|
|
tracing::info!(%status, %url, "Received Calendar API response");
|
|
|
|
if !status.is_success() {
|
|
let error_body = res.text().await.unwrap_or_default();
|
|
tracing::error!(%status, %url, body = %error_body, "Calendar API request failed");
|
|
return Err(AppError::Internal(format!(
|
|
"Failed to delete event: {} - {}",
|
|
status, error_body
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|