//! HTTP handlers for API key management. //! //! These endpoints allow users to create, view, refresh, and revoke their API keys. //! API keys are used for daemon authentication and programmatic access. use axum::{ extract::State, http::StatusCode, response::IntoResponse, Json, }; use crate::server::auth::{ create_api_key, generate_api_key, get_active_api_key, refresh_api_key, revoke_api_key, ApiKeyError, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse, RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse, UserOnly, }; use crate::server::messages::ApiError; use crate::server::state::SharedState; /// Create a new API key for the authenticated user. /// /// Each user can only have one active API key at a time. If an existing key /// exists, this will return a 409 Conflict error - use the refresh endpoint /// to replace the existing key, or revoke it first. #[utoipa::path( post, path = "/api/v1/auth/api-keys", request_body = CreateApiKeyRequest, responses( (status = 201, description = "API key created", body = CreateApiKeyResponse), (status = 401, description = "Not authenticated", body = ApiError), (status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError), (status = 409, description = "API key already exists", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []) ), tag = "API Keys" )] pub async fn create_api_key_handler( State(state): State, UserOnly(user): UserOnly, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; // Generate a new API key let generated = generate_api_key(); match create_api_key(pool, user.user_id, &generated, req.name.as_deref()).await { Ok(key) => { let response = CreateApiKeyResponse { id: key.id, key: generated.full_key, prefix: key.key_prefix, name: key.name, created_at: key.created_at, }; (StatusCode::CREATED, Json(response)).into_response() } Err(ApiKeyError::KeyAlreadyExists) => ( StatusCode::CONFLICT, Json(ApiError::new( "KEY_EXISTS", "An active API key already exists. Revoke it first or use refresh.", )), ) .into_response(), Err(ApiKeyError::Database(e)) => { tracing::error!("Failed to create API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } Err(e) => { tracing::error!("Failed to create API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("INTERNAL_ERROR", e.to_string())), ) .into_response() } } } /// Get information about the current active API key. /// /// Returns the key's ID, prefix (for identification), name, and timestamps. /// The full key is never returned - it was only shown once when created. #[utoipa::path( get, path = "/api/v1/auth/api-keys", responses( (status = 200, description = "API key info", body = ApiKeyInfoResponse), (status = 401, description = "Not authenticated", body = ApiError), (status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError), (status = 404, description = "No active API key", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []) ), tag = "API Keys" )] pub async fn get_api_key_handler( State(state): State, UserOnly(user): UserOnly, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match get_active_api_key(pool, user.user_id).await { Ok(Some(key)) => { let response: ApiKeyInfoResponse = key.into(); Json(response).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NO_KEY", "No active API key found")), ) .into_response(), Err(e) => { tracing::error!("Failed to get API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } } } /// Refresh the current API key. /// /// This revokes the existing key (if any) and creates a new one atomically. /// Use this for key rotation without downtime. #[utoipa::path( post, path = "/api/v1/auth/api-keys/refresh", request_body = RefreshApiKeyRequest, responses( (status = 200, description = "API key refreshed", body = RefreshApiKeyResponse), (status = 401, description = "Not authenticated", body = ApiError), (status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []) ), tag = "API Keys" )] pub async fn refresh_api_key_handler( State(state): State, UserOnly(user): UserOnly, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; // Generate a new API key let generated = generate_api_key(); match refresh_api_key(pool, user.user_id, &generated, req.name.as_deref()).await { Ok((key, old_prefix)) => { // Invalidate cache for the old key if we had a cache // (The cache lookup is by hash, but we revoked the old key in DB so it won't match) let response = RefreshApiKeyResponse { id: key.id, key: generated.full_key, prefix: key.key_prefix, name: key.name, created_at: key.created_at, previous_key_revoked: old_prefix.is_some(), }; Json(response).into_response() } Err(ApiKeyError::Database(e)) => { tracing::error!("Failed to refresh API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } Err(e) => { tracing::error!("Failed to refresh API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("INTERNAL_ERROR", e.to_string())), ) .into_response() } } } /// Revoke the current active API key. /// /// After revocation, the key can no longer be used for authentication. /// A new key can be created after revocation. #[utoipa::path( delete, path = "/api/v1/auth/api-keys", responses( (status = 200, description = "API key revoked", body = RevokeApiKeyResponse), (status = 401, description = "Not authenticated", body = ApiError), (status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError), (status = 404, description = "No active API key", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []) ), tag = "API Keys" )] pub async fn revoke_api_key_handler( State(state): State, UserOnly(user): UserOnly, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match revoke_api_key(pool, user.user_id).await { Ok(key) => { let response = RevokeApiKeyResponse { message: "API key revoked successfully".to_string(), revoked_key_prefix: key.key_prefix, }; Json(response).into_response() } Err(ApiKeyError::KeyNotFound) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NO_KEY", "No active API key found")), ) .into_response(), Err(ApiKeyError::Database(e)) => { tracing::error!("Failed to revoke API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } Err(e) => { tracing::error!("Failed to revoke API key: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("INTERNAL_ERROR", e.to_string())), ) .into_response() } } }