Skip to main content

ruvector_scipix/api/
state.rs

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