use std::net::SocketAddr;
use std::sync::Arc;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use axum::Router;
use tracing::{error, info, warn};
use crate::api::client::ApiClient;
use crate::api::editor::list_bindings as api_list_bindings;
use crate::cli::commands::shared;
use crate::error::OlError;
use crate::runtime::admin_token;
use crate::runtime::reload::{ReloadInputs, SharedRoutes};
use crate::runtime::server::RuntimeContext;
#[derive(Clone)]
pub struct AdminState {
pub ctx: Arc<RuntimeContext>,
pub admin_token: String,
}
pub fn build_router(state: AdminState) -> Router {
Router::new()
.route("/v1/admin/reload", post(handle_reload))
.with_state(state)
}
pub async fn serve(
port: u16,
state: AdminState,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<SocketAddr, OlError> {
let bind: SocketAddr = ([127, 0, 0, 1], port).into();
let listener = tokio::net::TcpListener::bind(bind).await.map_err(|e| {
OlError::new(
crate::error::OL_4272_XDG_DIR_UNWRITABLE,
format!("admin bind {bind}: {e}"),
)
})?;
let local = listener.local_addr().map_err(|e| {
OlError::new(
crate::error::OL_4272_XDG_DIR_UNWRITABLE,
format!("admin local_addr: {e}"),
)
})?;
let app = build_router(state);
info!(addr = %local, "admin endpoint listening");
if let Err(e) = axum::serve(listener, app)
.with_graceful_shutdown(shutdown)
.await
{
error!(error = %e, "admin endpoint serve error");
}
Ok(local)
}
async fn handle_reload(State(state): State<AdminState>, headers: HeaderMap) -> Response {
if !verify_bearer(&state.admin_token, &headers) {
return (StatusCode::UNAUTHORIZED, "missing or invalid bearer token").into_response();
}
info!("admin reload requested");
let live = match shared::make_client().await {
Ok(client) => match refresh_live(&client).await {
Ok(rows) => Some(rows),
Err(e) => {
warn!(error = %e, "could not refresh live bindings during reload");
None
}
},
Err(e) => {
warn!(error = %e, "no API client during reload");
None
}
};
if let Some(rows) = live {
let mut guard = state.ctx.live_bindings.lock().await;
*guard = rows;
}
let live_snapshot = state.ctx.live_bindings.lock().await.clone();
let manifest_known = state.ctx.manifest_secret_ids.lock().await.clone();
let inputs = ReloadInputs {
manifest_path: &state.ctx.manifest_path,
live_bindings: &live_snapshot,
secret_store: state.ctx.secrets.as_ref(),
manifest_secret_ids_fallback: Some(manifest_known),
};
match crate::runtime::reload::reload_into(&state.ctx.routes, &inputs) {
Ok(n) => (
StatusCode::OK,
axum::Json(serde_json::json!({ "status": "reloaded", "binding_count": n })),
)
.into_response(),
Err(e) => {
warn!(error = %e, code = %e.code, "admin reload failed; routes unchanged");
(
StatusCode::CONFLICT,
axum::Json(serde_json::json!({
"status": "reload_failed",
"error": { "code": e.code.code, "message": e.message }
})),
)
.into_response()
}
}
}
fn verify_bearer(expected: &str, headers: &HeaderMap) -> bool {
let Some(auth) = headers.get("authorization").and_then(|v| v.to_str().ok()) else {
return false;
};
let Some(presented) = auth.strip_prefix("Bearer ") else {
return false;
};
admin_token::matches(expected, presented)
}
async fn refresh_live(
client: &ApiClient,
) -> Result<Vec<crate::api::editor::EditorBindingRow>, OlError> {
api_list_bindings(client).await
}
pub fn swap_routes(shared: &SharedRoutes, table: crate::runtime::multi_tool::RouteTable) {
shared.store(table);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_bearer_matches_only_with_correct_prefix_and_value() {
let mut h = HeaderMap::new();
h.insert("authorization", "Bearer secret".parse().unwrap());
assert!(verify_bearer("secret", &h));
assert!(!verify_bearer("other", &h));
let mut bad = HeaderMap::new();
bad.insert("authorization", "Token secret".parse().unwrap());
assert!(!verify_bearer("secret", &bad));
}
#[test]
fn verify_bearer_returns_false_when_header_absent() {
let h = HeaderMap::new();
assert!(!verify_bearer("anything", &h));
}
}