parent
d3cdcea0df
commit
9aca4cadf0
13 changed files with 483 additions and 21 deletions
292
src/domain/calendar/mod.rs
Normal file
292
src/domain/calendar/mod.rs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
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 {
|
||||
Self {
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
client: Client::new(),
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue