ironflow_api/routes/api_keys/
delete.rs1use 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#[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}