diff options
Diffstat (limited to 'makima/src/server/handlers/mesh.rs')
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 284 |
1 files changed, 283 insertions, 1 deletions
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index c840676..0e72bdf 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -20,7 +20,7 @@ use crate::db::models::{ use crate::db::repository::{self, RepositoryError}; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; -use crate::server::state::{DaemonCommand, SharedState, TaskUpdateNotification}; +use crate::server::state::{DaemonCommand, DaemonReauthStatus, SharedState, TaskUpdateNotification}; // ============================================================================= // Authentication Types @@ -4283,3 +4283,285 @@ pub async fn restart_daemon( }) .into_response() } + +// ============================================================================= +// Daemon Reauthorization +// ============================================================================= + +/// Response from the trigger reauth endpoint. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +pub struct TriggerReauthResponse { + /// Whether the reauth command was sent successfully. + pub success: bool, + /// The daemon ID that received the reauth command. + #[serde(rename = "daemonId")] + pub daemon_id: Uuid, + /// Unique request ID for tracking this reauth flow. + #[serde(rename = "requestId")] + pub request_id: Uuid, +} + +/// Request body for submitting an auth code. +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +pub struct SubmitAuthCodeRequest { + /// The auth code obtained from the OAuth login flow. + pub code: String, + /// The request ID from the trigger reauth response. + #[serde(rename = "requestId")] + pub request_id: Uuid, +} + +/// Response from the submit auth code endpoint. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +pub struct SubmitAuthCodeResponse { + /// Whether the auth code was sent to the daemon successfully. + pub success: bool, +} + +/// Response from the reauth status polling endpoint. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +pub struct ReauthStatusResponse { + /// Current status of the reauth flow: "pending", "url_ready", "completed", "failed" + pub status: String, + /// OAuth login URL (present when status is "url_ready") + #[serde(rename = "loginUrl", skip_serializing_if = "Option::is_none")] + pub login_url: Option<String>, + /// Error message (present when status is "failed") + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option<String>, +} + +/// Trigger OAuth re-authentication on a daemon. +/// +/// Sends a reauth command to the specified daemon, which will spawn `claude setup-token` +/// and return the OAuth login URL via a status update. +#[utoipa::path( + post, + path = "/api/v1/mesh/daemons/{id}/reauth", + params( + ("id" = Uuid, Path, description = "Daemon ID") + ), + responses( + (status = 200, description = "Reauth command sent", body = TriggerReauthResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Daemon not found or not connected", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn trigger_daemon_reauth( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify the daemon exists and belongs to this owner + match repository::get_daemon_for_owner(pool, id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Daemon not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Check if daemon is connected + if !state.is_daemon_connected(id) { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new( + "DAEMON_NOT_CONNECTED", + "Daemon is not currently connected", + )), + ) + .into_response(); + } + + // Generate a unique request ID for this reauth flow + let request_id = Uuid::new_v4(); + + // Initialize the status as "pending" + state.set_daemon_reauth_status(id, request_id, "pending".to_string(), None, None); + + // Send reauth command to daemon + let command = DaemonCommand::TriggerReauth { request_id }; + if let Err(e) = state.send_daemon_command(id, command).await { + tracing::error!("Failed to send reauth command to daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + daemon_id = %id, + request_id = %request_id, + owner_id = %auth.owner_id, + "Reauth command sent to daemon" + ); + + Json(TriggerReauthResponse { + success: true, + daemon_id: id, + request_id, + }) + .into_response() +} + +/// Submit an OAuth auth code to a daemon's pending reauth flow. +/// +/// Sends the auth code to the daemon, which will forward it to the `claude setup-token` process. +#[utoipa::path( + post, + path = "/api/v1/mesh/daemons/{id}/reauth/code", + params( + ("id" = Uuid, Path, description = "Daemon ID") + ), + request_body = SubmitAuthCodeRequest, + responses( + (status = 200, description = "Auth code submitted", body = SubmitAuthCodeResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Daemon not found or not connected", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn submit_daemon_auth_code( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(body): Json<SubmitAuthCodeRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify the daemon exists and belongs to this owner + match repository::get_daemon_for_owner(pool, id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Daemon not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Check if daemon is connected + if !state.is_daemon_connected(id) { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new( + "DAEMON_NOT_CONNECTED", + "Daemon is not currently connected", + )), + ) + .into_response(); + } + + // Send auth code command to daemon + let command = DaemonCommand::SubmitAuthCode { + request_id: body.request_id, + code: body.code, + }; + if let Err(e) = state.send_daemon_command(id, command).await { + tracing::error!("Failed to send auth code to daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + daemon_id = %id, + request_id = %body.request_id, + owner_id = %auth.owner_id, + "Auth code submitted to daemon" + ); + + Json(SubmitAuthCodeResponse { success: true }).into_response() +} + +/// Get the status of a daemon reauth request. +/// +/// Used by the frontend to poll for reauth status updates. +#[utoipa::path( + get, + path = "/api/v1/mesh/daemons/{id}/reauth/{request_id}/status", + params( + ("id" = Uuid, Path, description = "Daemon ID"), + ("request_id" = Uuid, Path, description = "Reauth request ID") + ), + responses( + (status = 200, description = "Reauth status", body = ReauthStatusResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Reauth request not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn get_daemon_reauth_status( + State(state): State<SharedState>, + Authenticated(_auth): Authenticated, + Path((id, request_id)): Path<(Uuid, Uuid)>, +) -> impl IntoResponse { + match state.get_daemon_reauth_status(id, request_id) { + Some(status) => Json(ReauthStatusResponse { + status: status.status, + login_url: status.login_url, + error: status.error, + }) + .into_response(), + None => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Reauth request not found")), + ) + .into_response(), + } +} |
