summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/directives.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-06 02:08:37 +0000
committersoryu <soryu@soryu.co>2026-02-06 02:08:37 +0000
commit25e1275af1b742cc7866fba91152d9a4734a6f94 (patch)
treee92c7e168f4e73c302fb63217ea20bf8cfa2ba7e /makima/src/server/handlers/directives.rs
parent8f725a7c64fbeb85ebeb59b54d2f774e9a0a59d6 (diff)
downloadsoryu-25e1275af1b742cc7866fba91152d9a4734a6f94.tar.gz
soryu-25e1275af1b742cc7866fba91152d9a4734a6f94.zip
Fix: Directives API
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
-rw-r--r--makima/src/server/handlers/directives.rs85
1 files changed, 83 insertions, 2 deletions
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<i64>,
}
+/// 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<String>,
+ pub api_key: Option<String>,
+}
+
/// 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=<jwt> or ?api_key=<key>
pub async fn stream_events(
State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
Path(id): Path<Uuid>,
+ Query(auth_params): Query<StreamAuthQuery>,
) -> 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(_)) => {}