use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use std::sync::Arc;
use tracing::{error, info, warn};
use crate::auth::{AuthContext, Scope};
use crate::models::{
ApiError, CreateKeyRequest, CreateKeyResponse, KeyEntry, ListKeysResponse, RevokeKeyResponse,
};
use crate::storage::{KeyRecord, KeyStore, hash_key, key_prefix};
pub async fn create_key(
Extension(auth_ctx): Extension<AuthContext>,
State(store): State<Arc<dyn KeyStore>>,
Json(request): Json<CreateKeyRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ApiError>)> {
check_admin(&auth_ctx)?;
if request.description.trim().is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(ApiError::validation("description must not be empty")),
));
}
let plaintext = perfgate_auth::generate_api_key(false);
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let record = KeyRecord {
id: id.clone(),
key_hash: hash_key(&plaintext),
key_prefix: key_prefix(&plaintext),
role: request.role,
project: request.project.clone(),
pattern: request.pattern.clone(),
description: request.description.clone(),
created_at: now,
expires_at: request.expires_at,
revoked_at: None,
};
store.create_key(&record).await.map_err(|e| {
error!(error = %e, "Failed to create API key");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::internal_error(&e.to_string())),
)
})?;
info!(
key_id = %id,
role = %record.role,
project = %record.project,
"API key created"
);
Ok((
StatusCode::CREATED,
Json(CreateKeyResponse {
id,
key: plaintext,
description: request.description,
role: request.role,
project: request.project,
pattern: request.pattern,
created_at: now,
expires_at: request.expires_at,
}),
))
}
pub async fn list_keys(
Extension(auth_ctx): Extension<AuthContext>,
State(store): State<Arc<dyn KeyStore>>,
) -> Result<impl IntoResponse, (StatusCode, Json<ApiError>)> {
check_admin(&auth_ctx)?;
let records = store.list_keys().await.map_err(|e| {
error!(error = %e, "Failed to list API keys");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::internal_error(&e.to_string())),
)
})?;
let keys: Vec<KeyEntry> = records
.into_iter()
.map(|r| KeyEntry {
id: r.id,
key_prefix: r.key_prefix,
description: r.description,
role: r.role,
project: r.project,
pattern: r.pattern,
created_at: r.created_at,
expires_at: r.expires_at,
revoked_at: r.revoked_at,
})
.collect();
Ok(Json(ListKeysResponse { keys }))
}
pub async fn revoke_key(
Path(id): Path<String>,
Extension(auth_ctx): Extension<AuthContext>,
State(store): State<Arc<dyn KeyStore>>,
) -> Result<impl IntoResponse, (StatusCode, Json<ApiError>)> {
check_admin(&auth_ctx)?;
let revoked_at = store.revoke_key(&id).await.map_err(|e| {
error!(error = %e, key_id = %id, "Failed to revoke API key");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::internal_error(&e.to_string())),
)
})?;
match revoked_at {
Some(ts) => {
info!(key_id = %id, "API key revoked");
Ok(Json(RevokeKeyResponse { id, revoked_at: ts }))
}
None => {
warn!(key_id = %id, "Attempted to revoke non-existent key");
Err((
StatusCode::NOT_FOUND,
Json(ApiError::not_found(&format!("Key {} not found", id))),
))
}
}
}
fn check_admin(auth_ctx: &AuthContext) -> Result<(), (StatusCode, Json<ApiError>)> {
if !auth_ctx.api_key.has_scope(Scope::Admin) {
warn!(
key_id = %auth_ctx.api_key.id,
"Non-admin attempted key management"
);
return Err((
StatusCode::FORBIDDEN,
Json(ApiError::forbidden("Requires 'admin' permission")),
));
}
Ok(())
}