Mercury/src/routes/admin/api_keys.rs
Matthew L McPeak ebeba5fe29
Some checks failed
ci / test (push) Failing after 6s
ci / build-ui (push) Successful in 12s
ci / publish (push) Has been skipped
Initial Commit
2026-06-17 21:34:16 -04:00

103 lines
3 KiB
Rust

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<AppState>) -> Result<Json<Vec<ApiKey>>, 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<AppState>,
Json(body): Json<CreateApiKey>,
) -> Result<Json<Value>, 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<AppState>,
Path(id): Path<i32>,
Json(body): Json<UpdateApiKey>,
) -> Result<Json<ApiKey>, 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<AppState>,
Path(id): Path<i32>,
) -> Result<StatusCode, StatusCode> {
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)
}
}