Skip to main content

pipedash_core/infrastructure/
token_store.rs

1use std::collections::HashMap;
2use std::sync::RwLock;
3
4use async_trait::async_trait;
5
6use crate::domain::{
7    DomainError,
8    DomainResult,
9};
10
11#[async_trait]
12pub trait TokenStore: Send + Sync {
13    async fn store_token(&self, provider_id: i64, token: &str) -> DomainResult<()>;
14
15    async fn get_token(&self, provider_id: i64) -> DomainResult<String>;
16
17    async fn delete_token(&self, provider_id: i64) -> DomainResult<()>;
18
19    async fn get_all_tokens(&self) -> DomainResult<HashMap<i64, String>>;
20
21    async fn get_token_by_name(&self, _name: &str) -> DomainResult<String> {
22        Err(DomainError::InternalError(
23            "This token store does not support name-based lookups".into(),
24        ))
25    }
26
27    async fn export_encrypted(&self, _password: &str) -> DomainResult<Vec<u8>> {
28        Err(DomainError::InternalError(
29            "This token store does not support export".into(),
30        ))
31    }
32
33    async fn import_encrypted(&self, _data: &[u8], _password: &str) -> DomainResult<()> {
34        Err(DomainError::InternalError(
35            "This token store does not support import".into(),
36        ))
37    }
38
39    async fn warmup(&self) -> DomainResult<()> {
40        Ok(())
41    }
42}
43
44pub struct MemoryTokenStore {
45    tokens: RwLock<HashMap<i64, String>>,
46}
47
48impl MemoryTokenStore {
49    pub fn new() -> Self {
50        Self {
51            tokens: RwLock::new(HashMap::new()),
52        }
53    }
54
55    pub fn with_tokens(tokens: HashMap<i64, String>) -> Self {
56        Self {
57            tokens: RwLock::new(tokens),
58        }
59    }
60}
61
62impl Default for MemoryTokenStore {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68#[async_trait]
69impl TokenStore for MemoryTokenStore {
70    async fn store_token(&self, provider_id: i64, token: &str) -> DomainResult<()> {
71        let mut tokens = self
72            .tokens
73            .write()
74            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
75        tokens.insert(provider_id, token.to_string());
76        Ok(())
77    }
78
79    async fn get_token(&self, provider_id: i64) -> DomainResult<String> {
80        let tokens = self
81            .tokens
82            .read()
83            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
84        tokens.get(&provider_id).cloned().ok_or_else(|| {
85            DomainError::DatabaseError(format!("Token not found for provider {}", provider_id))
86        })
87    }
88
89    async fn delete_token(&self, provider_id: i64) -> DomainResult<()> {
90        let mut tokens = self
91            .tokens
92            .write()
93            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
94        tokens.remove(&provider_id);
95        Ok(())
96    }
97
98    async fn get_all_tokens(&self) -> DomainResult<HashMap<i64, String>> {
99        let tokens = self
100            .tokens
101            .read()
102            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
103        Ok(tokens.clone())
104    }
105}
106
107pub struct EnvTokenStore {
108    prefix: String,
109    cache: RwLock<HashMap<i64, String>>,
110}
111
112impl EnvTokenStore {
113    pub fn new() -> Self {
114        Self::with_prefix("PIPEDASH_TOKEN_")
115    }
116
117    pub fn with_prefix(prefix: &str) -> Self {
118        Self {
119            prefix: prefix.to_string(),
120            cache: RwLock::new(HashMap::new()),
121        }
122    }
123
124    fn env_var_name(&self, provider_id: i64) -> String {
125        format!("{}{}", self.prefix, provider_id)
126    }
127}
128
129impl Default for EnvTokenStore {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[async_trait]
136impl TokenStore for EnvTokenStore {
137    async fn store_token(&self, provider_id: i64, token: &str) -> DomainResult<()> {
138        let mut cache = self
139            .cache
140            .write()
141            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
142        cache.insert(provider_id, token.to_string());
143        Ok(())
144    }
145
146    async fn get_token(&self, provider_id: i64) -> DomainResult<String> {
147        {
148            let cache = self
149                .cache
150                .read()
151                .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
152            if let Some(token) = cache.get(&provider_id) {
153                return Ok(token.clone());
154            }
155        }
156
157        let var_name = self.env_var_name(provider_id);
158        std::env::var(&var_name).map_err(|_| {
159            DomainError::DatabaseError(format!(
160                "Token not found for provider {} (env var: {})",
161                provider_id, var_name
162            ))
163        })
164    }
165
166    async fn delete_token(&self, provider_id: i64) -> DomainResult<()> {
167        let mut cache = self
168            .cache
169            .write()
170            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
171        cache.remove(&provider_id);
172        Ok(())
173    }
174
175    async fn get_all_tokens(&self) -> DomainResult<HashMap<i64, String>> {
176        let cache = self
177            .cache
178            .read()
179            .map_err(|e| DomainError::InternalError(format!("Lock poisoned: {}", e)))?;
180        Ok(cache.clone())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[tokio::test]
189    async fn test_memory_store() {
190        let store = MemoryTokenStore::new();
191
192        store.store_token(1, "secret_token").await.unwrap();
193
194        let token = store.get_token(1).await.unwrap();
195        assert_eq!(token, "secret_token");
196
197        let all = store.get_all_tokens().await.unwrap();
198        assert_eq!(all.len(), 1);
199
200        store.delete_token(1).await.unwrap();
201
202        assert!(store.get_token(1).await.is_err());
203    }
204
205    #[tokio::test]
206    async fn test_memory_store_with_initial_tokens() {
207        let mut initial = HashMap::new();
208        initial.insert(1, "token1".to_string());
209        initial.insert(2, "token2".to_string());
210
211        let store = MemoryTokenStore::with_tokens(initial);
212
213        assert_eq!(store.get_token(1).await.unwrap(), "token1");
214        assert_eq!(store.get_token(2).await.unwrap(), "token2");
215    }
216}