use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use chrono::{DateTime, Utc};
use ironflow_auth::extractor::AuthenticatedUser;
use ironflow_auth::password;
use ironflow_store::entities::{ApiKeyScope, NewApiKey};
use rand::Rng;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::ApiError;
use crate::response::ok;
use crate::state::AppState;
use ironflow_auth::extractor::API_KEY_PREFIX;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Deserialize)]
pub struct CreateApiKeyRequest {
pub name: String,
pub scopes: Vec<ApiKeyScope>,
pub expires_at: Option<DateTime<Utc>>,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct CreateApiKeyResponse {
pub id: Uuid,
pub key: String,
pub key_prefix: String,
pub name: String,
pub scopes: Vec<ApiKeyScope>,
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[cfg_attr(
feature = "openapi",
utoipa::path(
post,
path = "/api/v1/api-keys",
tags = ["api-keys"],
request_body(content = CreateApiKeyRequest, description = "API key configuration"),
responses(
(status = 201, description = "API key created successfully", body = CreateApiKeyResponse),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden (member trying to assign forbidden scopes)")
),
security(("Bearer" = []))
)
)]
pub async fn create_api_key(
user: AuthenticatedUser,
State(state): State<AppState>,
Json(req): Json<CreateApiKeyRequest>,
) -> Result<impl IntoResponse, ApiError> {
if req.name.trim().is_empty() {
return Err(ApiError::BadRequest("name must not be empty".to_string()));
}
if req.scopes.is_empty() {
return Err(ApiError::BadRequest(
"at least one scope is required".to_string(),
));
}
if !user.is_admin && !ApiKeyScope::all_allowed_for_member(&req.scopes) {
return Err(ApiError::Forbidden);
}
let raw_key = generate_api_key();
let key_prefix = raw_key[..API_KEY_PREFIX.len() + 8].to_string();
let key_hash =
password::hash(&raw_key).map_err(|e| ApiError::Internal(format!("hashing: {e}")))?;
let api_key = state
.api_key_store
.create_api_key(NewApiKey {
user_id: user.user_id,
name: req.name,
key_hash,
key_prefix: key_prefix.clone(),
scopes: req.scopes,
expires_at: req.expires_at,
})
.await
.map_err(ApiError::from)?;
let response = CreateApiKeyResponse {
id: api_key.id,
key: raw_key,
key_prefix,
name: api_key.name,
scopes: api_key.scopes,
expires_at: api_key.expires_at,
created_at: api_key.created_at,
};
Ok((StatusCode::CREATED, ok(response)))
}
fn generate_api_key() -> String {
let mut rng = rand::rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
let encoded = hex::encode(&random_bytes);
format!("{API_KEY_PREFIX}{encoded}")
}