ironflow_api/routes/secrets/
list.rs1use axum::extract::{Query, State};
4use axum::response::IntoResponse;
5use serde::Deserialize;
6
7use ironflow_auth::extractor::Authenticated;
8
9use crate::entities::SecretResponse;
10use crate::error::ApiError;
11use crate::response::ok_paged;
12use crate::state::AppState;
13
14#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
16#[derive(Debug, Deserialize)]
17pub struct ListSecretsQuery {
18 pub prefix: Option<String>,
20 pub page: Option<u32>,
22 pub per_page: Option<u32>,
24}
25
26#[cfg_attr(
30 feature = "openapi",
31 utoipa::path(
32 get,
33 path = "/api/v1/secrets",
34 tags = ["secrets"],
35 params(ListSecretsQuery),
36 responses(
37 (status = 200, description = "Secrets listed", body = Vec<SecretResponse>),
38 (status = 401, description = "Unauthorized"),
39 (status = 403, description = "Forbidden")
40 ),
41 security(("Bearer" = []))
42 )
43)]
44pub async fn list_secrets(
45 auth: Authenticated,
46 State(state): State<AppState>,
47 Query(query): Query<ListSecretsQuery>,
48) -> Result<impl IntoResponse, ApiError> {
49 if !auth.is_admin() {
50 return Err(ApiError::Forbidden);
51 }
52
53 let prefix = query.prefix.as_deref().unwrap_or("");
54 let page = query.page.unwrap_or(1).max(1);
55 let per_page = query.per_page.unwrap_or(50).clamp(1, 100);
56
57 let result = state.store.list_secrets(prefix, page, per_page).await?;
58
59 let data: Vec<SecretResponse> = result.items.into_iter().map(SecretResponse::from).collect();
60
61 Ok(ok_paged(data, result.page, result.per_page, result.total))
62}
63
64#[cfg(test)]
65mod tests {
66 use axum::Router;
67 use axum::body::Body;
68 use axum::http::{Request, StatusCode};
69 use axum::routing::get;
70 use http_body_util::BodyExt;
71 use ironflow_auth::jwt::{AccessToken, JwtConfig};
72 use ironflow_core::providers::claude::ClaudeCodeProvider;
73 use ironflow_engine::context::WorkflowContext;
74 use ironflow_engine::engine::Engine;
75 use ironflow_engine::handler::{HandlerFuture, WorkflowHandler};
76 use ironflow_engine::notify::Event;
77 use ironflow_store::memory::InMemoryStore;
78 use ironflow_store::store::Store;
79 use serde_json::Value as JsonValue;
80 use std::sync::Arc;
81 use tokio::sync::broadcast;
82 use tower::ServiceExt;
83 use uuid::Uuid;
84
85 use super::*;
86
87 struct TestWorkflow;
88
89 impl WorkflowHandler for TestWorkflow {
90 fn name(&self) -> &str {
91 "test-workflow"
92 }
93
94 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
95 Box::pin(async move { Ok(()) })
96 }
97 }
98
99 fn test_jwt_config() -> Arc<JwtConfig> {
100 Arc::new(JwtConfig {
101 secret: "test-secret".to_string(),
102 access_token_ttl_secs: 900,
103 refresh_token_ttl_secs: 604800,
104 cookie_domain: None,
105 cookie_secure: false,
106 })
107 }
108
109 fn test_state() -> AppState {
110 let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
111 let provider = Arc::new(ClaudeCodeProvider::new());
112 let mut engine = Engine::new(store.clone(), provider);
113 engine.register(TestWorkflow).unwrap();
114 let (event_sender, _) = broadcast::channel::<Event>(1);
115 AppState::new(
116 store,
117 Arc::new(engine),
118 test_jwt_config(),
119 "test-worker-token".to_string(),
120 event_sender,
121 )
122 }
123
124 fn make_admin_token(state: &AppState) -> String {
125 let user_id = Uuid::now_v7();
126 let token = AccessToken::for_user(user_id, "admin", true, &state.jwt_config).unwrap();
127 format!("Bearer {}", token.0)
128 }
129
130 fn make_regular_token(state: &AppState) -> String {
131 let user_id = Uuid::now_v7();
132 let token = AccessToken::for_user(user_id, "user", false, &state.jwt_config).unwrap();
133 format!("Bearer {}", token.0)
134 }
135
136 #[tokio::test]
137 async fn list_secrets_admin_only() {
138 let state = test_state();
139 let auth_header = make_regular_token(&state);
140
141 let app = Router::new()
142 .route("/", get(list_secrets))
143 .with_state(state);
144
145 let req = Request::builder()
146 .uri("/")
147 .method("GET")
148 .header("authorization", auth_header)
149 .body(Body::empty())
150 .unwrap();
151
152 let resp = app.oneshot(req).await.unwrap();
153 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
154 }
155
156 #[tokio::test]
157 async fn list_secrets_admin_succeeds() {
158 let state = test_state();
159 let auth_header = make_admin_token(&state);
160
161 let app = Router::new()
162 .route("/", get(list_secrets))
163 .with_state(state);
164
165 let req = Request::builder()
166 .uri("/")
167 .method("GET")
168 .header("authorization", auth_header)
169 .body(Body::empty())
170 .unwrap();
171
172 let resp = app.oneshot(req).await.unwrap();
173 assert_eq!(resp.status(), StatusCode::OK);
174
175 let body = resp.into_body().collect().await.unwrap().to_bytes();
176 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
177 assert!(json_val["data"].is_array());
178 assert!(json_val["meta"].is_object());
179 }
180
181 #[tokio::test]
182 async fn list_secrets_includes_pagination() {
183 let state = test_state();
184 let auth_header = make_admin_token(&state);
185
186 let app = Router::new()
187 .route("/", get(list_secrets))
188 .with_state(state);
189
190 let req = Request::builder()
191 .uri("/?page=1&per_page=10")
192 .method("GET")
193 .header("authorization", auth_header)
194 .body(Body::empty())
195 .unwrap();
196
197 let resp = app.oneshot(req).await.unwrap();
198 assert_eq!(resp.status(), StatusCode::OK);
199
200 let body = resp.into_body().collect().await.unwrap().to_bytes();
201 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
202 assert_eq!(json_val["meta"]["page"], 1);
203 assert_eq!(json_val["meta"]["per_page"], 10);
204 }
205
206 #[tokio::test]
207 async fn list_secrets_clamps_per_page_max() {
208 let state = test_state();
209 let auth_header = make_admin_token(&state);
210
211 let app = Router::new()
212 .route("/", get(list_secrets))
213 .with_state(state);
214
215 let req = Request::builder()
216 .uri("/?per_page=500")
217 .method("GET")
218 .header("authorization", auth_header)
219 .body(Body::empty())
220 .unwrap();
221
222 let resp = app.oneshot(req).await.unwrap();
223 assert_eq!(resp.status(), StatusCode::OK);
224
225 let body = resp.into_body().collect().await.unwrap().to_bytes();
226 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
227 assert_eq!(json_val["meta"]["per_page"], 100);
228 }
229}