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}
31
32#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35#[derive(Debug, Serialize)]
36pub struct CreateApiKeyResponse {
37 pub id: Uuid,
39 pub key: String,
41 pub key_prefix: String,
43 pub name: String,
45 pub scopes: Vec<ApiKeyScope>,
47 pub expires_at: Option<DateTime<Utc>>,
49 pub created_at: DateTime<Utc>,
51}
52
53#[cfg_attr(
59 feature = "openapi",
60 utoipa::path(
61 post,
62 path = "/api/v1/api-keys",
63 tags = ["api-keys"],
64 request_body(content = CreateApiKeyRequest, description = "API key configuration"),
65 responses(
66 (status = 201, description = "API key created successfully", body = CreateApiKeyResponse),
67 (status = 400, description = "Invalid input"),
68 (status = 401, description = "Unauthorized"),
69 (status = 403, description = "Forbidden (member trying to assign forbidden scopes)")
70 ),
71 security(("Bearer" = []))
72 )
73)]
74pub async fn create_api_key(
75 user: AuthenticatedUser,
76 State(state): State<AppState>,
77 Json(req): Json<CreateApiKeyRequest>,
78) -> Result<impl IntoResponse, ApiError> {
79 if req.name.trim().is_empty() {
80 return Err(ApiError::BadRequest("name must not be empty".to_string()));
81 }
82
83 if req.scopes.is_empty() {
84 return Err(ApiError::BadRequest(
85 "at least one scope is required".to_string(),
86 ));
87 }
88
89 if !user.is_admin && !ApiKeyScope::all_allowed_for_member(&req.scopes) {
90 return Err(ApiError::Forbidden);
91 }
92
93 let raw_key = generate_api_key();
94 let key_prefix = raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN].to_string();
95 let key_hash =
96 password::hash(&raw_key).map_err(|e| ApiError::Internal(format!("hashing: {e}")))?;
97
98 let api_key = state
99 .store
100 .create_api_key(NewApiKey {
101 user_id: user.user_id,
102 name: req.name,
103 key_hash,
104 key_prefix: key_prefix.clone(),
105 scopes: req.scopes,
106 expires_at: req.expires_at,
107 })
108 .await
109 .map_err(ApiError::from)?;
110
111 let response = CreateApiKeyResponse {
112 id: api_key.id,
113 key: raw_key,
114 key_prefix,
115 name: api_key.name,
116 scopes: api_key.scopes,
117 expires_at: api_key.expires_at,
118 created_at: api_key.created_at,
119 };
120
121 Ok((StatusCode::CREATED, ok(response)))
122}
123
124fn generate_api_key() -> String {
126 let mut rng = rand::rng();
127 let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
128 let encoded = hex::encode(&random_bytes);
129 format!("{API_KEY_PREFIX}{encoded}")
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn generate_api_key_has_correct_prefix() {
138 let key = generate_api_key();
139 assert!(key.starts_with(API_KEY_PREFIX));
140 }
141
142 #[test]
143 fn generate_api_key_is_long_enough_for_prefix_extraction() {
144 let key = generate_api_key();
145 let expected_prefix_len = API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN;
146 assert!(
147 key.len() >= expected_prefix_len,
148 "key length {} is shorter than expected prefix length {}",
149 key.len(),
150 expected_prefix_len
151 );
152
153 let prefix = &key[..expected_prefix_len];
154 assert_eq!(prefix.len(), 13);
155 assert!(prefix.starts_with(API_KEY_PREFIX));
156 }
157
158 #[test]
159 fn generate_api_key_prefix_fits_varchar_16() {
160 let key = generate_api_key();
161 let prefix = &key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
162 assert!(
163 prefix.len() <= 16,
164 "prefix length {} exceeds VARCHAR(16)",
165 prefix.len()
166 );
167 }
168
169 #[test]
170 fn generate_api_key_produces_unique_keys() {
171 let key1 = generate_api_key();
172 let key2 = generate_api_key();
173 assert_ne!(key1, key2);
174 }
175}