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;
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}
31
32/// Response returned when creating an API key.
33/// The raw key is only shown once.
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35#[derive(Debug, Serialize)]
36pub struct CreateApiKeyResponse {
37    /// API key ID.
38    pub id: Uuid,
39    /// The full raw API key (only returned at creation time).
40    pub key: String,
41    /// First characters for identification.
42    pub key_prefix: String,
43    /// Key name.
44    pub name: String,
45    /// Granted scopes.
46    pub scopes: Vec<ApiKeyScope>,
47    /// Expiration date.
48    pub expires_at: Option<DateTime<Utc>>,
49    /// Creation date.
50    pub created_at: DateTime<Utc>,
51}
52
53/// Create a new API key for the authenticated user.
54///
55/// # Errors
56///
57/// - 400 if the name is empty or scopes are invalid
58#[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
124/// Generate a random API key with the `irfl_` prefix.
125fn 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}