From 25e1275af1b742cc7866fba91152d9a4734a6f94 Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 6 Feb 2026 02:08:37 +0000 Subject: Fix: Directives API --- makima/src/server/auth.rs | 10 ++++ makima/src/server/handlers/directives.rs | 85 +++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) (limited to 'makima/src/server') diff --git a/makima/src/server/auth.rs b/makima/src/server/auth.rs index b694df6..90b7bda 100644 --- a/makima/src/server/auth.rs +++ b/makima/src/server/auth.rs @@ -837,6 +837,11 @@ pub async fn log_api_key_event( // Internal Helper Functions // ============================================================================= +/// Public wrapper for resolve_owner_id, used by SSE endpoints that authenticate via query params. +pub async fn resolve_owner_id_public(pool: &PgPool, user_id: Uuid, email: Option<&str>) -> Result { + resolve_owner_id(pool, user_id, email).await +} + /// Resolve owner_id from user_id by looking up the users table. /// If the user doesn't exist, auto-creates them on first login. /// Uses ON CONFLICT to handle race conditions when multiple requests arrive simultaneously. @@ -894,6 +899,11 @@ async fn resolve_owner_id(pool: &PgPool, user_id: Uuid, email: Option<&str>) -> } } +/// Public wrapper for validate_api_key, used by SSE endpoints that authenticate via query params. +pub async fn validate_api_key_public(pool: &PgPool, key: &str) -> Result<(Uuid, Uuid), AuthError> { + validate_api_key(pool, key).await +} + /// Validate an API key and return (user_id, owner_id). async fn validate_api_key(pool: &PgPool, key: &str) -> Result<(Uuid, Uuid), AuthError> { let key_hash = hash_api_key(key); diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 6f6c3f1..4a78ab5 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -39,6 +39,14 @@ pub struct ListEventsQuery { pub limit: Option, } +/// Query parameters for SSE stream authentication +/// EventSource API cannot set custom headers, so auth is passed via query params +#[derive(Debug, Deserialize)] +pub struct StreamAuthQuery { + pub token: Option, + pub api_key: Option, +} + /// Query parameters for listing evaluations #[derive(Debug, Deserialize)] pub struct ListEvaluationsQuery { @@ -117,7 +125,14 @@ pub async fn list_directives( match repository::list_directives_for_owner(pool, auth.owner_id, params.status.as_deref()).await { - Ok(directives) => Json(directives).into_response(), + Ok(directives) => { + let total = directives.len() as i64; + Json(serde_json::json!({ + "directives": directives, + "total": total, + })) + .into_response() + } Err(e) => { tracing::error!("Failed to list directives: {}", e); ( @@ -1052,10 +1067,13 @@ pub async fn list_events( /// SSE stream of events for a directive /// GET /api/v1/directives/:id/events/stream +/// +/// EventSource API cannot set custom headers, so authentication is accepted +/// via query parameters: ?token= or ?api_key= pub async fn stream_events( State(state): State, - Authenticated(auth): Authenticated, Path(id): Path, + Query(auth_params): Query, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( @@ -1065,6 +1083,69 @@ pub async fn stream_events( .into_response(); }; + // Authenticate via query params (EventSource cannot set headers) + let auth = if let Some(ref token) = auth_params.token { + // JWT token + let verifier = match state.jwt_verifier.as_ref() { + Some(v) => v, + None => { + return ( + StatusCode::UNAUTHORIZED, + Json(ApiError::new("AUTH_NOT_CONFIGURED", "Authentication not configured")), + ) + .into_response() + } + }; + let claims = match verifier.verify(token) { + Ok(c) => c, + Err(_) => { + return ( + StatusCode::UNAUTHORIZED, + Json(ApiError::new("INVALID_TOKEN", "Invalid authentication token")), + ) + .into_response() + } + }; + match crate::server::auth::resolve_owner_id_public(pool, claims.sub, claims.email.as_deref()).await { + Ok(owner_id) => crate::server::auth::AuthenticatedUser { + user_id: claims.sub, + owner_id, + auth_source: crate::server::auth::AuthSource::Jwt, + email: claims.email, + }, + Err(_) => { + return ( + StatusCode::UNAUTHORIZED, + Json(ApiError::new("USER_NOT_FOUND", "User not found")), + ) + .into_response() + } + } + } else if let Some(ref api_key) = auth_params.api_key { + // API key + match crate::server::auth::validate_api_key_public(pool, api_key).await { + Ok((user_id, owner_id)) => crate::server::auth::AuthenticatedUser { + user_id, + owner_id, + auth_source: crate::server::auth::AuthSource::ApiKey, + email: None, + }, + Err(_) => { + return ( + StatusCode::UNAUTHORIZED, + Json(ApiError::new("INVALID_API_KEY", "Invalid or revoked API key")), + ) + .into_response() + } + } + } else { + return ( + StatusCode::UNAUTHORIZED, + Json(ApiError::new("MISSING_TOKEN", "Authentication required via ?token= or ?api_key= query parameter")), + ) + .into_response(); + }; + // Verify ownership match repository::get_directive_for_owner(pool, id, auth.owner_id).await { Ok(Some(_)) => {} -- cgit v1.2.3