ruvector_scipix/api/
state.rs

1use moka::future::Cache;
2use sha2::{Sha256, Digest};
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use super::{jobs::JobQueue, middleware::{create_rate_limiter, AppRateLimiter}};
8
9/// Shared application state
10#[derive(Clone)]
11pub struct AppState {
12    /// Job queue for async PDF processing
13    pub job_queue: Arc<JobQueue>,
14
15    /// Result cache
16    pub cache: Cache<String, String>,
17
18    /// Rate limiter
19    pub rate_limiter: AppRateLimiter,
20
21    /// Whether authentication is enabled
22    pub auth_enabled: bool,
23
24    /// Map of app_id -> hashed API key
25    /// Keys should be stored as SHA-256 hashes, never in plaintext
26    pub api_keys: Arc<HashMap<String, String>>,
27}
28
29impl AppState {
30    /// Create a new application state instance with authentication disabled
31    pub fn new() -> Self {
32        Self {
33            job_queue: Arc::new(JobQueue::new()),
34            cache: create_cache(),
35            rate_limiter: create_rate_limiter(),
36            auth_enabled: false,
37            api_keys: Arc::new(HashMap::new()),
38        }
39    }
40
41    /// Create state with custom configuration
42    pub fn with_config(max_jobs: usize, cache_size: u64) -> Self {
43        Self {
44            job_queue: Arc::new(JobQueue::with_capacity(max_jobs)),
45            cache: Cache::builder()
46                .max_capacity(cache_size)
47                .time_to_live(Duration::from_secs(3600))
48                .time_to_idle(Duration::from_secs(600))
49                .build(),
50            rate_limiter: create_rate_limiter(),
51            auth_enabled: false,
52            api_keys: Arc::new(HashMap::new()),
53        }
54    }
55
56    /// Create state with authentication enabled
57    pub fn with_auth(api_keys: HashMap<String, String>) -> Self {
58        // Hash all provided API keys
59        let hashed_keys: HashMap<String, String> = api_keys
60            .into_iter()
61            .map(|(app_id, key)| (app_id, hash_api_key(&key)))
62            .collect();
63
64        Self {
65            job_queue: Arc::new(JobQueue::new()),
66            cache: create_cache(),
67            rate_limiter: create_rate_limiter(),
68            auth_enabled: true,
69            api_keys: Arc::new(hashed_keys),
70        }
71    }
72
73    /// Add an API key (hashes the key before storing)
74    pub fn add_api_key(&mut self, app_id: String, api_key: &str) {
75        let hashed = hash_api_key(api_key);
76        Arc::make_mut(&mut self.api_keys).insert(app_id, hashed);
77        self.auth_enabled = true;
78    }
79
80    /// Enable or disable authentication
81    pub fn set_auth_enabled(&mut self, enabled: bool) {
82        self.auth_enabled = enabled;
83    }
84}
85
86impl Default for AppState {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Hash an API key using SHA-256
93fn hash_api_key(key: &str) -> String {
94    let mut hasher = Sha256::new();
95    hasher.update(key.as_bytes());
96    format!("{:x}", hasher.finalize())
97}
98
99/// Create a cache with default configuration
100fn create_cache() -> Cache<String, String> {
101    Cache::builder()
102        // Max 10,000 entries
103        .max_capacity(10_000)
104        // Time to live: 1 hour
105        .time_to_live(Duration::from_secs(3600))
106        // Time to idle: 10 minutes
107        .time_to_idle(Duration::from_secs(600))
108        .build()
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[tokio::test]
116    async fn test_state_creation() {
117        let state = AppState::new();
118        assert!(Arc::strong_count(&state.job_queue) >= 1);
119    }
120
121    #[tokio::test]
122    async fn test_state_with_config() {
123        let state = AppState::with_config(100, 5000);
124        assert!(Arc::strong_count(&state.job_queue) >= 1);
125    }
126
127    #[tokio::test]
128    async fn test_cache_operations() {
129        let state = AppState::new();
130
131        // Insert value
132        state.cache.insert("key1".to_string(), "value1".to_string()).await;
133
134        // Retrieve value
135        let value = state.cache.get(&"key1".to_string()).await;
136        assert_eq!(value, Some("value1".to_string()));
137
138        // Non-existent key
139        let missing = state.cache.get(&"missing".to_string()).await;
140        assert_eq!(missing, None);
141    }
142}