github_bot_sdk/auth/
cache.rs1use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9use super::{GitHubAppId, InstallationId, InstallationToken, JsonWebToken, TokenCache};
10use crate::error::CacheError;
11
12pub struct InMemoryTokenCache {
17 jwt_cache: Arc<RwLock<HashMap<GitHubAppId, CachedToken<JsonWebToken>>>>,
18 installation_cache: Arc<RwLock<HashMap<InstallationId, CachedToken<InstallationToken>>>>,
19}
20
21struct CachedToken<T> {
23 token: T,
24}
25
26impl<T> CachedToken<T> {
27 fn new(token: T) -> Self {
28 Self { token }
29 }
30
31 fn token(&self) -> &T {
32 &self.token
33 }
34
35 fn is_valid(&self) -> bool
36 where
37 T: TokenExpiry,
38 {
39 !self.token.is_expired()
40 }
41}
42
43trait TokenExpiry {
45 fn is_expired(&self) -> bool;
46}
47
48impl TokenExpiry for JsonWebToken {
49 fn is_expired(&self) -> bool {
50 self.is_expired()
51 }
52}
53
54impl TokenExpiry for InstallationToken {
55 fn is_expired(&self) -> bool {
56 self.is_expired()
57 }
58}
59
60impl InMemoryTokenCache {
61 pub fn new() -> Self {
63 Self {
64 jwt_cache: Arc::new(RwLock::new(HashMap::new())),
65 installation_cache: Arc::new(RwLock::new(HashMap::new())),
66 }
67 }
68}
69
70impl Default for InMemoryTokenCache {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76#[async_trait]
77impl TokenCache for InMemoryTokenCache {
78 async fn get_jwt(&self, app_id: GitHubAppId) -> Result<Option<JsonWebToken>, CacheError> {
79 let cache = self
80 .jwt_cache
81 .read()
82 .map_err(|e| CacheError::OperationFailed {
83 message: format!("Failed to acquire read lock: {}", e),
84 })?;
85
86 Ok(cache.get(&app_id).map(|cached| cached.token().clone()))
87 }
88
89 async fn store_jwt(&self, jwt: JsonWebToken) -> Result<(), CacheError> {
90 let mut cache = self
91 .jwt_cache
92 .write()
93 .map_err(|e| CacheError::OperationFailed {
94 message: format!("Failed to acquire write lock: {}", e),
95 })?;
96
97 let app_id = jwt.app_id();
98 cache.insert(app_id, CachedToken::new(jwt));
99
100 Ok(())
101 }
102
103 async fn get_installation_token(
104 &self,
105 installation_id: InstallationId,
106 ) -> Result<Option<InstallationToken>, CacheError> {
107 let cache = self
108 .installation_cache
109 .read()
110 .map_err(|e| CacheError::OperationFailed {
111 message: format!("Failed to acquire read lock: {}", e),
112 })?;
113
114 Ok(cache
115 .get(&installation_id)
116 .map(|cached| cached.token().clone()))
117 }
118
119 async fn store_installation_token(&self, token: InstallationToken) -> Result<(), CacheError> {
120 let mut cache =
121 self.installation_cache
122 .write()
123 .map_err(|e| CacheError::OperationFailed {
124 message: format!("Failed to acquire write lock: {}", e),
125 })?;
126
127 let installation_id = token.installation_id();
128 cache.insert(installation_id, CachedToken::new(token));
129
130 Ok(())
131 }
132
133 async fn invalidate_installation_token(
134 &self,
135 installation_id: InstallationId,
136 ) -> Result<(), CacheError> {
137 let mut cache =
138 self.installation_cache
139 .write()
140 .map_err(|e| CacheError::OperationFailed {
141 message: format!("Failed to acquire write lock: {}", e),
142 })?;
143
144 cache.remove(&installation_id);
145
146 Ok(())
147 }
148
149 fn cleanup_expired_tokens(&self) {
150 if let Ok(mut jwt_cache) = self.jwt_cache.write() {
152 jwt_cache.retain(|_, cached| cached.is_valid());
153 }
154
155 if let Ok(mut inst_cache) = self.installation_cache.write() {
157 inst_cache.retain(|_, cached| cached.is_valid());
158 }
159 }
160}
161
162#[cfg(test)]
163#[path = "cache_tests.rs"]
164mod tests;