use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use kanade_shared::kv::{BUCKET_SERVER_SETTINGS, KEY_SERVER_SETTINGS};
use kanade_shared::wire::{MAX_AGENT_PRUNE_DAYS, ServerSettings};
use tracing::{info, warn};
use crate::api::AppState;
use crate::audit;
use crate::audit::Caller;
pub async fn get(State(s): State<AppState>) -> Result<Json<ServerSettings>, (StatusCode, String)> {
let kv = open_bucket(&s).await?;
match kv.get(KEY_SERVER_SETTINGS).await {
Ok(Some(bytes)) => serde_json::from_slice::<ServerSettings>(&bytes)
.map(Json)
.map_err(|e| {
warn!(error = %e, "decode server_settings");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("stored server_settings is corrupt: {e}"),
)
}),
Ok(None) => Ok(Json(ServerSettings::default())),
Err(e) => {
warn!(error = %e, "read server_settings");
Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("read server_settings: {e}"),
))
}
}
}
pub async fn defaults() -> Json<ServerSettings> {
Json(ServerSettings::defaults())
}
pub async fn put(
State(s): State<AppState>,
caller: Caller,
Json(mut settings): Json<ServerSettings>,
) -> Result<Json<ServerSettings>, (StatusCode, String)> {
if let Some(days) = settings.agent_prune_days {
if days == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
"agent_prune_days must be >= 1; omit it or send null to disable pruning"
.to_string(),
));
}
if days > MAX_AGENT_PRUNE_DAYS {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
format!("agent_prune_days must be <= {MAX_AGENT_PRUNE_DAYS} (100 years)"),
));
}
}
if let Some(g) = settings.controller_group.as_deref()
&& g.trim().is_empty()
{
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
"controller_group must be a non-empty group name; omit it or send null to unset"
.to_string(),
));
}
if let Some(g) = settings.controller_group.as_mut() {
*g = g.trim().to_string();
}
let kv = open_bucket(&s).await?;
let body = serde_json::to_vec(&settings).map_err(|e| {
warn!(error = %e, "encode server_settings");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("encode server_settings: {e}"),
)
})?;
kv.put(KEY_SERVER_SETTINGS, body.into())
.await
.map_err(|e| {
warn!(error = %e, "write server_settings");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("write server_settings: {e}"),
)
})?;
info!(
agent_prune_days = ?settings.agent_prune_days,
"server_settings replaced",
);
audit::record(
&s.nats,
"operator",
"server_settings_set",
Some(KEY_SERVER_SETTINGS),
Some(&caller),
serde_json::to_value(&settings).unwrap_or(serde_json::Value::Null),
)
.await;
Ok(Json(settings))
}
pub(crate) async fn load(s: &AppState) -> anyhow::Result<ServerSettings> {
use anyhow::Context;
let kv = s
.jetstream
.get_key_value(BUCKET_SERVER_SETTINGS)
.await
.context("open server_settings KV")?;
match kv
.get(KEY_SERVER_SETTINGS)
.await
.context("get server_settings")?
{
Some(bytes) => serde_json::from_slice(&bytes).context("decode server_settings"),
None => Ok(ServerSettings::default()),
}
}
async fn open_bucket(
s: &AppState,
) -> Result<async_nats::jetstream::kv::Store, (StatusCode, String)> {
s.jetstream
.get_key_value(BUCKET_SERVER_SETTINGS)
.await
.map_err(|e| {
warn!(error = %e, bucket = BUCKET_SERVER_SETTINGS, "open server_settings KV bucket");
(
StatusCode::SERVICE_UNAVAILABLE,
format!("server_settings KV bucket unavailable: {e}"),
)
})
}