use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use serde_json::{json, Value}; use crate::{ auth::generate_api_key, models::api_key::{ApiKey, CreateApiKey, UpdateApiKey}, state::AppState, }; pub async fn list_api_keys(State(state): State) -> Result>, StatusCode> { let keys = sqlx::query_as::<_, ApiKey>( "SELECT id, name, key_prefix, permissions_mask, created_at, expires_at, last_used_at FROM api_keys ORDER BY created_at DESC", ) .fetch_all(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(keys)) } pub async fn create_api_key( State(state): State, Json(body): Json, ) -> Result, StatusCode> { let (plain_key, key_prefix, key_hash) = generate_api_key(); let mask = body.permissions_mask.unwrap_or_else(|| "0".into()); sqlx::query( "INSERT INTO api_keys (name, key_prefix, key_hash, permissions_mask, expires_at) VALUES ($1, $2, $3, $4, $5)", ) .bind(&body.name) .bind(&key_prefix) .bind(&key_hash) .bind(&mask) .bind(body.expires_at) .execute(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(json!({ "key": plain_key, "prefix": key_prefix, "name": body.name, "permissions_mask": mask, "note": "Store this key securely — it will not be shown again." }))) } pub async fn update_api_key( State(state): State, Path(id): Path, Json(body): Json, ) -> Result, StatusCode> { if body.name.is_none() && body.permissions_mask.is_none() && body.expires_at.is_none() { return Err(StatusCode::BAD_REQUEST); } let key = sqlx::query_as::<_, ApiKey>( "UPDATE api_keys SET name = COALESCE($1, name), permissions_mask = COALESCE($2, permissions_mask), expires_at = CASE WHEN $3::boolean THEN $4 ELSE expires_at END WHERE id = $5 RETURNING id, name, key_prefix, permissions_mask, created_at, expires_at, last_used_at", ) .bind(body.name.as_deref()) .bind(body.permissions_mask.as_deref()) .bind(body.expires_at.is_some()) .bind(body.expires_at) .bind(id) .fetch_optional(&state.pool) .await .map_err(|e| { tracing::error!("update api_key {}: {}", id, e); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; Ok(Json(key)) } pub async fn revoke_api_key( State(state): State, Path(id): Path, ) -> Result { let rows = sqlx::query("DELETE FROM api_keys WHERE id = $1") .bind(id) .execute(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .rows_affected(); if rows == 0 { Err(StatusCode::NOT_FOUND) } else { Ok(StatusCode::NO_CONTENT) } }