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;
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() + 8].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 .api_key_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}