Skip to main content

reasonkit_web/portal/
api_keys.rs

1//! # API Key Management Module
2//!
3//! API key generation, rotation, and access control.
4//!
5//! ## Features
6//!
7//! - Scoped API keys with fine-grained permissions
8//! - Automatic key rotation
9//! - Usage tracking and rate limiting per key
10//! - Key revocation with audit trail
11
12#![allow(unused_variables)] // Stub implementation
13
14use axum::{
15    extract::{Json, Path},
16    http::StatusCode,
17    response::IntoResponse,
18};
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use uuid::Uuid;
22
23/// API key scopes (permissions)
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
25#[serde(rename_all = "snake_case")]
26pub enum ApiKeyScope {
27    /// Read reasoning results
28    ReasoningRead,
29    /// Execute reasoning protocols
30    ReasoningWrite,
31    /// Read user profile
32    ProfileRead,
33    /// Modify user profile
34    ProfileWrite,
35    /// Read settings
36    SettingsRead,
37    /// Modify settings
38    SettingsWrite,
39    /// Create exports
40    ExportCreate,
41    /// Full access (admin)
42    Admin,
43}
44
45/// API key metadata
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ApiKey {
48    /// Key ID (public identifier)
49    pub id: Uuid,
50    /// Key name (user-defined)
51    pub name: String,
52    /// Key prefix (first 8 chars for display)
53    pub prefix: String,
54    /// Hashed key (never expose raw key after creation)
55    #[serde(skip_serializing)]
56    pub key_hash: String,
57    /// User ID who owns this key
58    pub user_id: Uuid,
59    /// Granted scopes
60    pub scopes: Vec<ApiKeyScope>,
61    /// Creation timestamp
62    pub created_at: DateTime<Utc>,
63    /// Last used timestamp
64    pub last_used: Option<DateTime<Utc>>,
65    /// Expiration timestamp (optional)
66    pub expires_at: Option<DateTime<Utc>>,
67    /// Is key active
68    pub active: bool,
69    /// Rate limit (requests per minute)
70    pub rate_limit: Option<u32>,
71    /// Total usage count
72    pub usage_count: u64,
73}
74
75/// API key creation response (only time raw key is shown)
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ApiKeyCreated {
78    pub id: Uuid,
79    pub name: String,
80    /// Raw API key - ONLY shown once at creation
81    pub key: String,
82    pub prefix: String,
83    pub scopes: Vec<ApiKeyScope>,
84    pub created_at: DateTime<Utc>,
85    pub expires_at: Option<DateTime<Utc>>,
86}
87
88/// API key service for database operations
89#[allow(dead_code)] // Reserved for future database validation
90pub struct ApiKeyService {
91    // TODO: Add database connection pool
92    max_keys_per_user: usize,
93}
94
95impl ApiKeyService {
96    pub fn new(max_keys_per_user: usize) -> Self {
97        Self { max_keys_per_user }
98    }
99
100    /// Generate a new API key
101    pub fn generate_key() -> String {
102        use rand::Rng;
103        let mut rng = rand::rng();
104        let key: String = (0..32)
105            .map(|_| {
106                let idx = rng.random_range(0..62);
107                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
108                    .chars()
109                    .nth(idx)
110                    .unwrap()
111            })
112            .collect();
113        format!("rk_{}", key)
114    }
115
116    /// Hash an API key for storage
117    pub fn hash_key(key: &str) -> String {
118        use sha2::{Digest, Sha256};
119        let mut hasher = Sha256::new();
120        hasher.update(key.as_bytes());
121        format!("{:x}", hasher.finalize())
122    }
123
124    /// Create a new API key
125    pub async fn create_key(
126        &self,
127        user_id: Uuid,
128        name: String,
129        scopes: Vec<ApiKeyScope>,
130        expires_at: Option<DateTime<Utc>>,
131        rate_limit: Option<u32>,
132    ) -> Result<ApiKeyCreated, ApiKeyError> {
133        // TODO: Check user's key count
134        // TODO: Store in database
135
136        let raw_key = Self::generate_key();
137        let key_hash = Self::hash_key(&raw_key);
138        let prefix = raw_key[..11].to_string(); // "rk_" + 8 chars
139
140        let created = ApiKeyCreated {
141            id: Uuid::new_v4(),
142            name,
143            key: raw_key,
144            prefix,
145            scopes,
146            created_at: Utc::now(),
147            expires_at,
148        };
149
150        Ok(created)
151    }
152
153    /// List all keys for a user (without secrets)
154    pub async fn list_keys(&self, user_id: Uuid) -> Result<Vec<ApiKey>, ApiKeyError> {
155        // TODO: Query database
156        Ok(vec![])
157    }
158
159    /// Revoke an API key
160    pub async fn revoke_key(&self, user_id: Uuid, key_id: Uuid) -> Result<(), ApiKeyError> {
161        // TODO: Update database
162        Ok(())
163    }
164
165    /// Rotate an API key (revoke old, create new with same scopes)
166    pub async fn rotate_key(
167        &self,
168        user_id: Uuid,
169        key_id: Uuid,
170    ) -> Result<ApiKeyCreated, ApiKeyError> {
171        // TODO: Get old key, create new, revoke old
172        Err(ApiKeyError::NotFound)
173    }
174
175    /// Validate an API key and return its metadata
176    pub async fn validate_key(&self, raw_key: &str) -> Result<ApiKey, ApiKeyError> {
177        let key_hash = Self::hash_key(raw_key);
178        // TODO: Query database by hash
179        Err(ApiKeyError::InvalidKey)
180    }
181
182    /// Record key usage
183    pub async fn record_usage(&self, key_id: Uuid) -> Result<(), ApiKeyError> {
184        // TODO: Update last_used and increment usage_count
185        Ok(())
186    }
187}
188
189impl Default for ApiKeyService {
190    fn default() -> Self {
191        Self::new(10)
192    }
193}
194
195/// API key errors
196#[derive(Debug, thiserror::Error)]
197pub enum ApiKeyError {
198    #[error("API key not found")]
199    NotFound,
200    #[error("Invalid API key")]
201    InvalidKey,
202    #[error("API key expired")]
203    Expired,
204    #[error("API key revoked")]
205    Revoked,
206    #[error("Rate limit exceeded")]
207    RateLimitExceeded,
208    #[error("Insufficient permissions")]
209    InsufficientScope,
210    #[error("Maximum keys exceeded")]
211    MaxKeysExceeded,
212    #[error("Database error: {0}")]
213    DatabaseError(String),
214}
215
216/// HTTP handlers for API key endpoints
217pub mod handlers {
218    use super::*;
219
220    #[derive(Debug, Deserialize)]
221    pub struct CreateKeyRequest {
222        pub name: String,
223        pub scopes: Vec<ApiKeyScope>,
224        #[serde(default)]
225        pub expires_in_days: Option<u32>,
226        pub rate_limit: Option<u32>,
227    }
228
229    /// List all API keys for current user
230    pub async fn list_keys() -> impl IntoResponse {
231        (StatusCode::OK, Json(serde_json::json!({"keys": []})))
232    }
233
234    /// Create a new API key
235    pub async fn create_key(Json(req): Json<CreateKeyRequest>) -> impl IntoResponse {
236        let raw_key = ApiKeyService::generate_key();
237        let prefix = raw_key[..11].to_string();
238
239        let response = ApiKeyCreated {
240            id: Uuid::new_v4(),
241            name: req.name,
242            key: raw_key,
243            prefix,
244            scopes: req.scopes,
245            created_at: Utc::now(),
246            expires_at: req
247                .expires_in_days
248                .map(|d| Utc::now() + chrono::Duration::days(d as i64)),
249        };
250
251        (StatusCode::CREATED, Json(response))
252    }
253
254    /// Revoke an API key
255    pub async fn revoke_key(Path(id): Path<Uuid>) -> impl IntoResponse {
256        (
257            StatusCode::OK,
258            Json(serde_json::json!({"success": true, "revoked_key_id": id})),
259        )
260    }
261
262    /// Rotate an API key
263    pub async fn rotate_key(Path(id): Path<Uuid>) -> impl IntoResponse {
264        // TODO: Implement key rotation
265        StatusCode::NOT_IMPLEMENTED
266    }
267}