//! `POST /api/unattended-password` — agent-side reporting of the per-boot //! "permanent password" used for unattended access (no logged-in user to //! click the approval popup). hello-agent generates a random password //! every time the service starts and posts it here so the admin UI can //! surface it for support staff. //! //! Auth: same per-peer signed-API gate as `/api/sysinfo` and //! `/api/heartbeat` — see [`crate::api::device_auth`]. Managed peers //! (`peer.managed = 1`) must carry a valid Ed25519 signature; stock //! clients keep posting unsigned and the first valid signature TOFU- //! promotes the peer. use crate::api::device_auth::{self, AuthOutcome}; use crate::api::error::ApiError; use crate::api::state::AppState; use axum::body::Bytes; use axum::extract::Extension; use axum::http::HeaderMap; use serde_json::Value; use std::sync::Arc; /// Body: `{"id": "...", "uuid": "...", "password": "..."}` /// Response (bare string, like sysinfo): `"OK"` or `"ID_NOT_FOUND"`. pub async fn unattended_password( Extension(state): Extension>, headers: HeaderMap, body: Bytes, ) -> Result { let outcome = device_auth::verify( &state, "POST", "/api/unattended-password", &headers, &body, ) .await?; let payload: Value = serde_json::from_slice(&body) .map_err(|_| ApiError::BadRequest("invalid json".into()))?; let body_id = payload .get("id") .and_then(|v| v.as_str()) .unwrap_or_default(); let uuid = payload .get("uuid") .and_then(|v| v.as_str()) .unwrap_or_default(); let password = payload .get("password") .and_then(|v| v.as_str()) .unwrap_or_default(); if body_id.is_empty() || uuid.is_empty() || password.is_empty() { return Err(ApiError::BadRequest( "id, uuid, and password are required".into(), )); } // Bind the trusted identity to the body. For signed requests the body // id must match the header id, or the agent is trying to overwrite // someone else's displayed password. For unsigned requests we just // need to ensure the peer isn't already locked down as managed. let id = match outcome { AuthOutcome::Verified { id: signed_id } => { if body_id != signed_id { return Err(ApiError::Unauthorized); } signed_id } AuthOutcome::LegacyUnsigned => { device_auth::enforce_managed_for_id(&state, body_id).await?; body_id.to_string() } }; let peer = state .db .get_peer(&id) .await .map_err(|e| ApiError::Internal(e.to_string()))?; if peer.is_none() { return Ok("ID_NOT_FOUND".to_string()); } state .db .set_unattended_password(&id, uuid, password) .await .map_err(|e| ApiError::Internal(e.to_string()))?; Ok("OK".to_string()) }