use std::sync::Arc;
use axum::{
Json, Router,
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
};
use chrono::{DateTime, Duration, Utc};
use serde::Deserialize;
use serde_json::json;
use crate::api_key::postgres::{ApiKeyStoreError, PgApiKeyStore};
#[derive(Clone)]
pub struct ApiKeyManagementState {
pub store: Arc<PgApiKeyStore>,
}
pub fn api_key_management_router(state: ApiKeyManagementState) -> Router {
Router::new()
.route("/api/v1/admin/api-keys", get(list_keys).post(create_key))
.route("/api/v1/admin/api-keys/{selector}/revoke", post(revoke_key))
.route("/api/v1/admin/api-keys/{selector}/rotate", post(rotate_key))
.with_state(state)
}
impl IntoResponse for ApiKeyStoreError {
fn into_response(self) -> Response {
let (status, code) = match &self {
Self::InvalidInput(_) => (StatusCode::BAD_REQUEST, "invalid_input"),
Self::NotFound => (StatusCode::NOT_FOUND, "api_key_not_found"),
_ => (StatusCode::INTERNAL_SERVER_ERROR, "database_error"),
};
let message = match &self {
Self::InvalidInput(m) => m.clone(),
Self::NotFound => "API key not found".to_string(),
_ => "database error".to_string(),
};
(status, Json(json!({ "error": code, "message": message }))).into_response()
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateKeyRequest {
name: String,
#[serde(default)]
scopes: Vec<String>,
#[serde(default)]
expires_in_secs: Option<i64>,
}
async fn list_keys(State(state): State<ApiKeyManagementState>) -> Response {
match state.store.list_keys().await {
Ok(keys) => (StatusCode::OK, Json(json!({ "keys": keys }))).into_response(),
Err(e) => e.into_response(),
}
}
async fn create_key(
State(state): State<ApiKeyManagementState>,
Json(req): Json<CreateKeyRequest>,
) -> Response {
let expires_at: Option<DateTime<Utc>> = match req.expires_in_secs {
Some(secs) if secs <= 0 => {
return ApiKeyStoreError::InvalidInput("expires_in_secs must be positive".to_string())
.into_response();
},
Some(secs) => Some(Utc::now() + Duration::seconds(secs)),
None => None,
};
match state.store.create_key(&req.name, &req.scopes, expires_at).await {
Ok((full_key, record)) => (
StatusCode::CREATED,
Json(json!({ "key": full_key, "record": record })),
)
.into_response(),
Err(e) => e.into_response(),
}
}
async fn revoke_key(
State(state): State<ApiKeyManagementState>,
Path(selector): Path<String>,
) -> Response {
match state.store.revoke(&selector).await {
Ok(()) => (StatusCode::OK, Json(json!({ "revoked": selector }))).into_response(),
Err(e) => e.into_response(),
}
}
async fn rotate_key(
State(state): State<ApiKeyManagementState>,
Path(selector): Path<String>,
) -> Response {
match state.store.rotate(&selector).await {
Ok(full_key) => {
(StatusCode::OK, Json(json!({ "key": full_key, "selector": selector }))).into_response()
},
Err(e) => e.into_response(),
}
}
#[cfg(test)]
#[path = "api_key_management_tests.rs"]
mod api_key_management_tests;