Skip to main content

ironflow_api/routes/api_keys/
delete.rs

1//! `DELETE /api/v1/api-keys/:id` -- Delete an API key.
2
3use axum::extract::{Path, State};
4use axum::http::StatusCode;
5use axum::response::IntoResponse;
6use ironflow_auth::extractor::AuthenticatedUser;
7use uuid::Uuid;
8
9use crate::error::ApiError;
10use crate::state::AppState;
11
12/// Delete an API key owned by the authenticated user.
13///
14/// # Errors
15///
16/// - 404 if the key does not exist or belongs to another user
17#[cfg_attr(
18    feature = "openapi",
19    utoipa::path(
20        delete,
21        path = "/api/v1/api-keys/{id}",
22        tags = ["api-keys"],
23        params(
24            ("id" = Uuid, Path, description = "API key ID")
25        ),
26        responses(
27            (status = 204, description = "API key deleted successfully"),
28            (status = 401, description = "Unauthorized"),
29            (status = 404, description = "API key not found")
30        ),
31        security(("Bearer" = []))
32    )
33)]
34pub async fn delete_api_key(
35    user: AuthenticatedUser,
36    State(state): State<AppState>,
37    Path(id): Path<Uuid>,
38) -> Result<impl IntoResponse, ApiError> {
39    let key = state
40        .store
41        .find_api_key_by_id(id)
42        .await
43        .map_err(ApiError::from)?
44        .ok_or(ApiError::ApiKeyNotFound(id))?;
45
46    if key.user_id != user.user_id {
47        return Err(ApiError::ApiKeyNotFound(id));
48    }
49
50    state
51        .store
52        .delete_api_key(id)
53        .await
54        .map_err(ApiError::from)?;
55
56    Ok(StatusCode::NO_CONTENT)
57}
58
59#[cfg(test)]
60mod tests {
61    use axum::Router;
62    use axum::body::Body;
63    use axum::http::{Request, StatusCode};
64    use axum::routing::delete;
65    use ironflow_auth::jwt::{AccessToken, JwtConfig};
66    use ironflow_core::providers::claude::ClaudeCodeProvider;
67    use ironflow_engine::context::WorkflowContext;
68    use ironflow_engine::engine::Engine;
69    use ironflow_engine::handler::{HandlerFuture, WorkflowHandler};
70    use ironflow_engine::notify::Event;
71    use ironflow_store::entities::{NewApiKey, NewUser};
72    use ironflow_store::memory::InMemoryStore;
73    use ironflow_store::store::Store;
74    use std::sync::Arc;
75    use tokio::sync::broadcast;
76    use tower::ServiceExt;
77    use uuid::Uuid;
78
79    use super::*;
80
81    struct TestWorkflow;
82
83    impl WorkflowHandler for TestWorkflow {
84        fn name(&self) -> &str {
85            "test-workflow"
86        }
87
88        fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
89            Box::pin(async move { Ok(()) })
90        }
91    }
92
93    fn test_jwt_config() -> Arc<JwtConfig> {
94        Arc::new(JwtConfig {
95            secret: "test-secret".to_string(),
96            access_token_ttl_secs: 900,
97            refresh_token_ttl_secs: 604800,
98            cookie_domain: None,
99            cookie_secure: false,
100        })
101    }
102
103    fn test_state() -> AppState {
104        let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
105        let provider = Arc::new(ClaudeCodeProvider::new());
106        let mut engine = Engine::new(store.clone(), provider);
107        engine.register(TestWorkflow).unwrap();
108        let (event_sender, _) = broadcast::channel::<Event>(1);
109        AppState::new(
110            store,
111            Arc::new(engine),
112            test_jwt_config(),
113            "test-worker-token".to_string(),
114            event_sender,
115        )
116    }
117
118    fn make_auth_header(state: &AppState) -> String {
119        let user_id = Uuid::now_v7();
120        let token = AccessToken::for_user(user_id, "testuser", false, &state.jwt_config).unwrap();
121        format!("Bearer {}", token.0)
122    }
123
124    #[tokio::test]
125    async fn delete_api_key_success() {
126        let state = test_state();
127        let user = state
128            .store
129            .create_user(NewUser {
130                email: "test@example.com".to_string(),
131                username: "testuser".to_string(),
132                password_hash: "hash".to_string(),
133                is_admin: None,
134            })
135            .await
136            .unwrap();
137
138        let key = state
139            .store
140            .create_api_key(NewApiKey {
141                user_id: user.id,
142                name: "test-key".to_string(),
143                key_hash: "hash".to_string(),
144                key_prefix: "sk_".to_string(),
145                scopes: vec![],
146                expires_at: None,
147            })
148            .await
149            .unwrap();
150
151        let auth_header = {
152            let token =
153                AccessToken::for_user(user.id, "testuser", false, &state.jwt_config).unwrap();
154            format!("Bearer {}", token.0)
155        };
156
157        let app = Router::new()
158            .route("/{id}", delete(delete_api_key))
159            .with_state(state);
160
161        let req = Request::builder()
162            .uri(format!("/{}", key.id))
163            .method("DELETE")
164            .header("authorization", auth_header)
165            .body(Body::empty())
166            .unwrap();
167
168        let resp = app.oneshot(req).await.unwrap();
169        assert_eq!(resp.status(), StatusCode::NO_CONTENT);
170    }
171
172    #[tokio::test]
173    async fn delete_api_key_not_found() {
174        let state = test_state();
175        let auth_header = make_auth_header(&state);
176
177        let app = Router::new()
178            .route("/{id}", delete(delete_api_key))
179            .with_state(state);
180
181        let key_id = Uuid::now_v7();
182        let req = Request::builder()
183            .uri(format!("/{}", key_id))
184            .method("DELETE")
185            .header("authorization", auth_header)
186            .body(Body::empty())
187            .unwrap();
188
189        let resp = app.oneshot(req).await.unwrap();
190        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
191    }
192
193    #[tokio::test]
194    async fn delete_api_key_not_owned_by_user() {
195        let state = test_state();
196
197        let user1 = state
198            .store
199            .create_user(NewUser {
200                email: "user1@example.com".to_string(),
201                username: "user1".to_string(),
202                password_hash: "hash".to_string(),
203                is_admin: None,
204            })
205            .await
206            .unwrap();
207
208        let user2 = state
209            .store
210            .create_user(NewUser {
211                email: "user2@example.com".to_string(),
212                username: "user2".to_string(),
213                password_hash: "hash".to_string(),
214                is_admin: None,
215            })
216            .await
217            .unwrap();
218
219        let key = state
220            .store
221            .create_api_key(NewApiKey {
222                user_id: user2.id,
223                name: "user2-key".to_string(),
224                key_hash: "hash".to_string(),
225                key_prefix: "sk_".to_string(),
226                scopes: vec![],
227                expires_at: None,
228            })
229            .await
230            .unwrap();
231
232        let auth_header = {
233            let token = AccessToken::for_user(user1.id, "user1", false, &state.jwt_config).unwrap();
234            format!("Bearer {}", token.0)
235        };
236
237        let app = Router::new()
238            .route("/{id}", delete(delete_api_key))
239            .with_state(state);
240
241        let req = Request::builder()
242            .uri(format!("/{}", key.id))
243            .method("DELETE")
244            .header("authorization", auth_header)
245            .body(Body::empty())
246            .unwrap();
247
248        let resp = app.oneshot(req).await.unwrap();
249        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
250    }
251}