use crate::state::AppState;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct UnsealRequest {
pub passphrase: String,
}
#[derive(Deserialize)]
pub struct SetSecretRequest {
pub value: String,
}
#[derive(Deserialize)]
pub struct TransitEncryptRequest {
pub key_name: String,
pub plaintext: String, }
#[derive(Deserialize)]
pub struct TransitDecryptRequest {
pub key_name: String,
pub ciphertext: String, }
#[derive(Deserialize)]
pub struct CreateTransitKeyRequest {
pub name: String,
}
#[derive(Deserialize, Default)]
pub struct ListQuery {
pub prefix: Option<String>,
pub limit: Option<usize>,
}
#[derive(Serialize)]
pub struct VaultStatusResponse {
pub sealed: bool,
pub secret_count: usize,
pub transit_key_count: usize,
pub uptime_secs: Option<u64>,
}
pub async fn vault_status(State(state): State<AppState>) -> impl IntoResponse {
let status = state.vault.status();
Json(serde_json::json!({
"sealed": status.sealed,
"secret_count": status.secret_count,
"transit_key_count": status.transit_key_count,
"uptime_secs": status.uptime_secs,
}))
}
pub async fn vault_unseal(
State(state): State<AppState>,
Json(req): Json<UnsealRequest>,
) -> impl IntoResponse {
match state.vault.unseal(&req.passphrase) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({"status": "unsealed"})),
),
Err(e) => (
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn vault_seal(State(state): State<AppState>) -> impl IntoResponse {
match state.vault.seal() {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({"status": "sealed"})),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn list_secrets(
State(state): State<AppState>,
Query(params): Query<ListQuery>,
) -> impl IntoResponse {
let prefix = params.prefix.as_deref().unwrap_or("");
match state.vault.list(prefix, "api") {
Ok(keys) => (StatusCode::OK, Json(serde_json::json!({"keys": keys}))),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn get_secret(
State(state): State<AppState>,
Path(key): Path<String>,
) -> impl IntoResponse {
match state.vault.get(&key, "api") {
Ok(value) => (
StatusCode::OK,
Json(serde_json::json!({"key": key, "value": value})),
),
Err(e) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn set_secret(
State(state): State<AppState>,
Path(key): Path<String>,
Json(req): Json<SetSecretRequest>,
) -> impl IntoResponse {
match state.vault.set(&key, &req.value, "api") {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({"status": "ok", "key": key})),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn delete_secret(
State(state): State<AppState>,
Path(key): Path<String>,
) -> impl IntoResponse {
match state.vault.delete(&key, "api") {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({"status": "deleted", "key": key})),
),
Err(e) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn transit_encrypt(
State(state): State<AppState>,
Json(req): Json<TransitEncryptRequest>,
) -> impl IntoResponse {
match state
.vault
.transit_encrypt(&req.key_name, req.plaintext.as_bytes())
{
Ok(ciphertext) => {
let encoded = data_encoding_hex(&ciphertext);
(
StatusCode::OK,
Json(serde_json::json!({"ciphertext": encoded})),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn transit_decrypt(
State(state): State<AppState>,
Json(req): Json<TransitDecryptRequest>,
) -> impl IntoResponse {
match hex_decode(&req.ciphertext) {
Ok(ciphertext) => match state.vault.transit_decrypt(&req.key_name, &ciphertext) {
Ok(plaintext) => {
let text = String::from_utf8_lossy(&plaintext).to_string();
(StatusCode::OK, Json(serde_json::json!({"plaintext": text})))
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
},
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": format!("Invalid hex: {}", e)})),
),
}
}
pub async fn create_transit_key(
State(state): State<AppState>,
Json(req): Json<CreateTransitKeyRequest>,
) -> impl IntoResponse {
match state.vault.transit_create_key(&req.name) {
Ok(()) => (
StatusCode::CREATED,
Json(serde_json::json!({"status": "created", "key_name": req.name})),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
),
}
}
pub async fn list_transit_keys(State(state): State<AppState>) -> impl IntoResponse {
let keys = state.vault.transit_list_keys();
Json(serde_json::json!({"keys": keys}))
}
pub async fn vault_audit(
State(state): State<AppState>,
Query(params): Query<ListQuery>,
) -> impl IntoResponse {
let limit = params.limit.unwrap_or(100);
let entries = state.vault.audit_entries(limit);
Json(serde_json::json!({"entries": entries}))
}
fn data_encoding_hex(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
if hex.len() % 2 != 0 {
return Err("odd length".to_string());
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| e.to_string()))
.collect()
}