Skip to main content

wave_api/
auth.rs

1use std::sync::Arc;
2use tokio::sync::RwLock;
3
4use crate::error::WaveError;
5
6const TOKEN_URL: &str = "https://api.waveapps.com/oauth2/token/";
7
8/// Callback type for token refresh notifications.
9pub type TokenRefreshCallback = Arc<dyn Fn(&str, &str) + Send + Sync>;
10
11/// OAuth2 configuration for the Wave API.
12#[derive(Clone)]
13pub struct OAuthConfig {
14    pub client_id: String,
15    pub client_secret: String,
16    pub access_token: String,
17    pub refresh_token: String,
18    pub redirect_uri: String,
19    /// Optional callback invoked after a token refresh with (new_access_token, new_refresh_token).
20    pub on_token_refresh: Option<TokenRefreshCallback>,
21}
22
23/// Internal auth state shared across clones.
24#[derive(Clone)]
25pub(crate) struct AuthState {
26    pub(crate) config: OAuthConfig,
27    pub(crate) tokens: Arc<RwLock<TokenPair>>,
28}
29
30/// Current access/refresh token pair.
31pub(crate) struct TokenPair {
32    pub access_token: String,
33    pub refresh_token: String,
34}
35
36impl AuthState {
37    pub fn new(config: OAuthConfig) -> Self {
38        let tokens = TokenPair {
39            access_token: config.access_token.clone(),
40            refresh_token: config.refresh_token.clone(),
41        };
42        Self {
43            config,
44            tokens: Arc::new(RwLock::new(tokens)),
45        }
46    }
47
48    pub async fn access_token(&self) -> String {
49        self.tokens.read().await.access_token.clone()
50    }
51
52    /// Refresh the access token using the refresh token.
53    pub async fn refresh(&self, http: &reqwest::Client) -> Result<(), WaveError> {
54        let refresh_token = self.tokens.read().await.refresh_token.clone();
55
56        let params = [
57            ("client_id", self.config.client_id.as_str()),
58            ("client_secret", self.config.client_secret.as_str()),
59            ("refresh_token", refresh_token.as_str()),
60            ("grant_type", "refresh_token"),
61            ("redirect_uri", self.config.redirect_uri.as_str()),
62        ];
63
64        let resp = http
65            .post(TOKEN_URL)
66            .form(&params)
67            .send()
68            .await
69            .map_err(|e| WaveError::TokenRefresh(e.to_string()))?;
70
71        if !resp.status().is_success() {
72            let status = resp.status();
73            let body = resp.text().await.unwrap_or_default();
74            return Err(WaveError::TokenRefresh(format!(
75                "HTTP {status} — {body}"
76            )));
77        }
78
79        let body: serde_json::Value = resp
80            .json()
81            .await
82            .map_err(|e| WaveError::TokenRefresh(e.to_string()))?;
83
84        let new_access = body["access_token"]
85            .as_str()
86            .ok_or_else(|| WaveError::TokenRefresh("missing access_token in response".into()))?;
87        let new_refresh = body["refresh_token"]
88            .as_str()
89            .ok_or_else(|| WaveError::TokenRefresh("missing refresh_token in response".into()))?;
90
91        // Update stored tokens.
92        {
93            let mut tokens = self.tokens.write().await;
94            tokens.access_token = new_access.to_string();
95            tokens.refresh_token = new_refresh.to_string();
96        }
97
98        // Notify callback.
99        if let Some(cb) = &self.config.on_token_refresh {
100            cb(new_access, new_refresh);
101        }
102
103        Ok(())
104    }
105}