Skip to main content

ironflow_api/routes/api_keys/
create.rs

1//! `POST /api/v1/api-keys` -- Create a new API key.
2
3use axum::Json;
4use axum::extract::State;
5use axum::http::StatusCode;
6use axum::response::IntoResponse;
7use chrono::{DateTime, Utc};
8use ironflow_auth::extractor::AuthenticatedUser;
9use ironflow_auth::password;
10use ironflow_store::entities::{ApiKeyScope, NewApiKey};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::error::ApiError;
16use crate::response::ok;
17use crate::state::AppState;
18use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
19
20/// Request body for creating an API key.
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[derive(Debug, Deserialize)]
23pub struct CreateApiKeyRequest {
24    /// Human-readable name for this key.
25    pub name: String,
26    /// Scopes to grant.
27    pub scopes: Vec<ApiKeyScope>,
28    /// Optional expiration date (ISO 8601).
29    pub expires_at: Option<DateTime<Utc>>,
30    /// Optional per-key rate limit override (requests per minute).
31    /// When set, the server uses this value instead of the global rate
32    /// limit for requests authenticated with this key.
33    pub rate_limit_override: Option<u32>,
34}
35
36/// Response returned when creating an API key.
37/// The raw key is only shown once.
38#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[derive(Debug, Serialize)]
40pub struct CreateApiKeyResponse {
41    /// API key ID.
42    pub id: Uuid,
43    /// The full raw API key (only returned at creation time).
44    pub key: String,
45    /// First characters for identification.
46    pub key_prefix: String,
47    /// Key name.
48    pub name: String,
49    /// Granted scopes.
50    pub scopes: Vec<ApiKeyScope>,
51    /// Expiration date.
52    pub expires_at: Option<DateTime<Utc>>,
53    /// Per-key rate limit override (requests per minute), if set.
54    pub rate_limit_override: Option<u32>,
55    /// Creation date.
56    pub created_at: DateTime<Utc>,
57}
58
59/// Create a new API key for the authenticated user.
60///
61/// # Errors
62///
63/// - 400 if the name is empty or scopes are invalid
64#[cfg_attr(
65    feature = "openapi",
66    utoipa::path(
67        post,
68        path = "/api/v1/api-keys",
69        tags = ["api-keys"],
70        request_body(content = CreateApiKeyRequest, description = "API key configuration"),
71        responses(
72            (status = 201, description = "API key created successfully", body = CreateApiKeyResponse),
73            (status = 400, description = "Invalid input"),
74            (status = 401, description = "Unauthorized"),
75            (status = 403, description = "Forbidden (member trying to assign forbidden scopes)")
76        ),
77        security(("Bearer" = []))
78    )
79)]
80pub async fn create_api_key(
81    user: AuthenticatedUser,
82    State(state): State<AppState>,
83    Json(req): Json<CreateApiKeyRequest>,
84) -> Result<impl IntoResponse, ApiError> {
85    if req.name.trim().is_empty() {
86        return Err(ApiError::BadRequest("name must not be empty".to_string()));
87    }
88
89    if req.scopes.is_empty() {
90        return Err(ApiError::BadRequest(
91            "at least one scope is required".to_string(),
92        ));
93    }
94
95    if !user.is_admin && !ApiKeyScope::all_allowed_for_member(&req.scopes) {
96        return Err(ApiError::Forbidden);
97    }
98
99    if let Some(override_val) = req.rate_limit_override
100        && override_val > 10_000
101    {
102        return Err(ApiError::BadRequest(
103            "rate_limit_override must be between 0 and 10000".to_string(),
104        ));
105    }
106
107    let raw_key = generate_api_key();
108    let key_prefix = raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN].to_string();
109    let key_hash =
110        password::hash(&raw_key).map_err(|e| ApiError::Internal(format!("hashing: {e}")))?;
111
112    let api_key = state
113        .store
114        .create_api_key(NewApiKey {
115            user_id: user.user_id,
116            name: req.name,
117            key_hash,
118            key_prefix: key_prefix.clone(),
119            scopes: req.scopes,
120            expires_at: req.expires_at,
121            rate_limit_override: req.rate_limit_override,
122        })
123        .await
124        .map_err(ApiError::from)?;
125
126    let response = CreateApiKeyResponse {
127        id: api_key.id,
128        key: raw_key,
129        key_prefix,
130        name: api_key.name,
131        scopes: api_key.scopes,
132        expires_at: api_key.expires_at,
133        rate_limit_override: api_key.rate_limit_override,
134        created_at: api_key.created_at,
135    };
136
137    Ok((StatusCode::CREATED, ok(response)))
138}
139
140/// Generate a random API key with the `irfl_` prefix.
141fn generate_api_key() -> String {
142    let mut rng = rand::rng();
143    let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
144    let encoded = hex::encode(&random_bytes);
145    format!("{API_KEY_PREFIX}{encoded}")
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn generate_api_key_has_correct_prefix() {
154        let key = generate_api_key();
155        assert!(key.starts_with(API_KEY_PREFIX));
156    }
157
158    #[test]
159    fn generate_api_key_is_long_enough_for_prefix_extraction() {
160        let key = generate_api_key();
161        let expected_prefix_len = API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN;
162        assert!(
163            key.len() >= expected_prefix_len,
164            "key length {} is shorter than expected prefix length {}",
165            key.len(),
166            expected_prefix_len
167        );
168
169        let prefix = &key[..expected_prefix_len];
170        assert_eq!(prefix.len(), 13);
171        assert!(prefix.starts_with(API_KEY_PREFIX));
172    }
173
174    #[test]
175    fn generate_api_key_prefix_fits_varchar_16() {
176        let key = generate_api_key();
177        let prefix = &key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
178        assert!(
179            prefix.len() <= 16,
180            "prefix length {} exceeds VARCHAR(16)",
181            prefix.len()
182        );
183    }
184
185    #[test]
186    fn generate_api_key_produces_unique_keys() {
187        let key1 = generate_api_key();
188        let key2 = generate_api_key();
189        assert_ne!(key1, key2);
190    }
191}