coven/storage/cloud/
oauth_session.rs1use tokio::sync::RwLock;
9use tracing::{info, warn};
10
11use super::CloudHomeError;
12use crate::clock::ClockRef;
13use crate::keys::{CloudHomeCredentials, KeyService};
14use crate::oauth::{self, OAuthConfig, OAuthTokens};
15
16pub struct OAuthSession {
18 tokens: RwLock<OAuthTokens>,
19 key_service: KeyService,
20 clock: ClockRef,
21 config: OAuthConfig,
22 provider_label: &'static str,
24}
25
26impl OAuthSession {
27 pub fn new(
28 tokens: OAuthTokens,
29 key_service: KeyService,
30 clock: ClockRef,
31 config: OAuthConfig,
32 provider_label: &'static str,
33 ) -> Self {
34 Self {
35 tokens: RwLock::new(tokens),
36 key_service,
37 clock,
38 config,
39 provider_label,
40 }
41 }
42
43 async fn access_token(&self) -> Result<String, CloudHomeError> {
45 let tokens = self.tokens.read().await;
46 match tokens.expires_at {
47 Some(expires_at) if self.clock.now().timestamp() < expires_at - 60 => {
48 return Ok(tokens.access_token.clone());
49 }
50 None => return Ok(tokens.access_token.clone()),
52 _ => {}
53 }
54 drop(tokens);
55 self.refresh().await
56 }
57
58 async fn refresh(&self) -> Result<String, CloudHomeError> {
60 let mut tokens = self.tokens.write().await;
61
62 if let Some(expires_at) = tokens.expires_at {
64 if self.clock.now().timestamp() < expires_at - 60 {
65 return Ok(tokens.access_token.clone());
66 }
67 }
68
69 let refresh_token = tokens.refresh_token.as_deref().ok_or_else(|| {
70 CloudHomeError::Storage(
71 "no refresh token available, re-authorization needed".to_string(),
72 )
73 })?;
74
75 let new_tokens = oauth::refresh(&self.config, refresh_token, self.clock.as_ref())
76 .await
77 .map_err(|e| CloudHomeError::Storage(format!("OAuth refresh failed: {e}")))?;
78
79 let json = serde_json::to_string(&new_tokens)
80 .map_err(|e| CloudHomeError::Storage(format!("serialize tokens: {e}")))?;
81 if let Err(e) = self
82 .key_service
83 .set_cloud_home_credentials(&CloudHomeCredentials::OAuth { token_json: json })
84 {
85 warn!("Failed to persist refreshed OAuth tokens: {e}");
86 }
87
88 let access_token = new_tokens.access_token.clone();
89 *tokens = new_tokens;
90
91 info!("Refreshed {} OAuth tokens", self.provider_label);
92 Ok(access_token)
93 }
94
95 pub async fn api_call(
98 &self,
99 build_request: impl Fn(&str) -> reqwest::RequestBuilder,
100 ) -> Result<reqwest::Response, CloudHomeError> {
101 let token = self.access_token().await?;
102 let resp = build_request(&token)
103 .send()
104 .await
105 .map_err(|e| CloudHomeError::Storage(format!("request failed: {e}")))?;
106
107 if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
108 let new_token = self.refresh().await?;
109 build_request(&new_token)
110 .send()
111 .await
112 .map_err(|e| CloudHomeError::Storage(format!("retry request failed: {e}")))
113 } else {
114 Ok(resp)
115 }
116 }
117}