ironflow_api/routes/api_keys/
create.rs1use 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#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[derive(Debug, Deserialize)]
23pub struct CreateApiKeyRequest {
24 pub name: String,
26 pub scopes: Vec<ApiKeyScope>,
28 pub expires_at: Option<DateTime<Utc>>,
30 pub rate_limit_override: Option<u32>,
34}
35
36#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[derive(Debug, Serialize)]
40pub struct CreateApiKeyResponse {
41 pub id: Uuid,
43 pub key: String,
45 pub key_prefix: String,
47 pub name: String,
49 pub scopes: Vec<ApiKeyScope>,
51 pub expires_at: Option<DateTime<Utc>>,
53 pub rate_limit_override: Option<u32>,
55 pub created_at: DateTime<Utc>,
57}
58
59#[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
140fn 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}