1use std::path::PathBuf;
7use std::sync::Arc;
8
9use anyhow::Result;
10use greentic_setup::admin::routes::{
11 AdminResponse, BundleDeployRequest, BundleRemoveRequest, BundleStatus, BundleStatusResponse,
12};
13use greentic_setup::admin::tls::AdminTlsConfig;
14use http_body_util::Full;
15use hyper::body::{Bytes, Incoming};
16use hyper::{Method, Request, Response, StatusCode};
17use serde_json::{Value, json};
18
19pub struct AdminState {
21 pub tls_config: AdminTlsConfig,
22 pub bundle_root: PathBuf,
23 pub tenant: String,
24 pub team: Option<String>,
25 pub env: String,
26}
27
28pub async fn handle_admin_request(
38 req: Request<Incoming>,
39 path: &str,
40 state: &Arc<AdminState>,
41) -> Result<Response<Full<Bytes>>> {
42 let method = req.method().clone();
43 let sub_path = path
44 .strip_prefix("/admin")
45 .unwrap_or("")
46 .trim_end_matches('/');
47
48 match (method, sub_path) {
49 (Method::GET, "/status") => handle_status(state).await,
50 (Method::POST, "/deploy") => handle_deploy(req, state).await,
51 (Method::POST, "/remove") => handle_remove(req, state).await,
52 (Method::POST, "/qa/spec") => handle_qa_spec(req, state).await,
53 (Method::POST, "/qa/validate") => handle_qa_validate(req, state).await,
54 (Method::POST, "/qa/submit") => handle_qa_submit(req, state).await,
55 _ => json_response(
56 StatusCode::NOT_FOUND,
57 &AdminResponse::<()>::err("not found"),
58 ),
59 }
60}
61
62async fn handle_status(state: &Arc<AdminState>) -> Result<Response<Full<Bytes>>> {
63 let bundle_exists = state.bundle_root.exists();
64 let providers_dir = state.bundle_root.join("providers");
65
66 let mut provider_count = 0usize;
67 if providers_dir.exists() {
68 if let Ok(entries) = std::fs::read_dir(&providers_dir) {
69 for entry in entries.flatten() {
70 let path = entry.path();
71 if path.is_dir() {
72 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
73 if !name.starts_with('_') && !name.starts_with('.') {
74 provider_count += 1;
75 }
76 }
77 }
78 }
79 }
80 }
81
82 let status = BundleStatusResponse {
83 status: if bundle_exists {
84 BundleStatus::Active
85 } else {
86 BundleStatus::Error
87 },
88 bundle_path: state.bundle_root.clone(),
89 pack_count: 0,
90 tenant_count: 1,
91 provider_count,
92 };
93
94 json_response(StatusCode::OK, &AdminResponse::ok(status))
95}
96
97async fn handle_deploy(
98 req: Request<Incoming>,
99 state: &Arc<AdminState>,
100) -> Result<Response<Full<Bytes>>> {
101 let body = read_body(req).await?;
102 let request: BundleDeployRequest = match serde_json::from_slice(&body) {
103 Ok(r) => r,
104 Err(e) => {
105 return json_response(
106 StatusCode::BAD_REQUEST,
107 &AdminResponse::<()>::err(format!("invalid request: {e}")),
108 );
109 }
110 };
111
112 let engine = greentic_setup::SetupEngine::new(greentic_setup::engine::SetupConfig {
114 tenant: state.tenant.clone(),
115 team: state.team.clone(),
116 env: state.env.clone(),
117 offline: false,
118 verbose: false,
119 });
120
121 let setup_request = greentic_setup::engine::SetupRequest {
122 bundle: request.bundle_path,
123 bundle_name: request.bundle_name,
124 pack_refs: request.pack_refs,
125 tenants: request.tenants,
126 ..Default::default()
127 };
128
129 let mode = if state.bundle_root.exists() {
130 greentic_setup::SetupMode::Update
131 } else {
132 greentic_setup::SetupMode::Create
133 };
134
135 match engine.plan(mode, &setup_request, false) {
136 Ok(plan) => {
137 let summary = json!({
138 "mode": format!("{mode:?}"),
139 "steps": plan.steps.len(),
140 "bundle": state.bundle_root.display().to_string(),
141 });
142 json_response(StatusCode::OK, &AdminResponse::ok(summary))
143 }
144 Err(e) => json_response(
145 StatusCode::INTERNAL_SERVER_ERROR,
146 &AdminResponse::<()>::err(e.to_string()),
147 ),
148 }
149}
150
151async fn handle_remove(
152 req: Request<Incoming>,
153 state: &Arc<AdminState>,
154) -> Result<Response<Full<Bytes>>> {
155 let body = read_body(req).await?;
156 let request: BundleRemoveRequest = match serde_json::from_slice(&body) {
157 Ok(r) => r,
158 Err(e) => {
159 return json_response(
160 StatusCode::BAD_REQUEST,
161 &AdminResponse::<()>::err(format!("invalid request: {e}")),
162 );
163 }
164 };
165
166 if !state.bundle_root.exists() {
167 return json_response(
168 StatusCode::NOT_FOUND,
169 &AdminResponse::<()>::err("bundle not found"),
170 );
171 }
172
173 let engine = greentic_setup::SetupEngine::new(greentic_setup::engine::SetupConfig {
174 tenant: state.tenant.clone(),
175 team: state.team.clone(),
176 env: state.env.clone(),
177 offline: true,
178 verbose: false,
179 });
180
181 let setup_request = greentic_setup::engine::SetupRequest {
182 bundle: state.bundle_root.clone(),
183 ..Default::default()
184 };
185
186 match engine.plan(greentic_setup::SetupMode::Remove, &setup_request, false) {
187 Ok(plan) => {
188 let _ = &request; let summary = json!({
190 "mode": "remove",
191 "steps": plan.steps.len(),
192 });
193 json_response(StatusCode::OK, &AdminResponse::ok(summary))
194 }
195 Err(e) => json_response(
196 StatusCode::INTERNAL_SERVER_ERROR,
197 &AdminResponse::<()>::err(e.to_string()),
198 ),
199 }
200}
201
202async fn handle_qa_spec(
203 req: Request<Incoming>,
204 state: &Arc<AdminState>,
205) -> Result<Response<Full<Bytes>>> {
206 let body = read_body(req).await?;
207 let request: Value = serde_json::from_slice(&body).unwrap_or_default();
208 let provider_id = request
209 .get("provider_id")
210 .and_then(Value::as_str)
211 .unwrap_or("");
212
213 if provider_id.is_empty() {
214 return json_response(
215 StatusCode::BAD_REQUEST,
216 &AdminResponse::<()>::err("provider_id required"),
217 );
218 }
219
220 let providers_dir = state.bundle_root.join("providers");
222 let pack_path = find_provider_pack(&providers_dir, provider_id);
223
224 match pack_path {
225 Some(path) => {
226 let form_spec =
227 greentic_setup::setup_to_formspec::pack_to_form_spec(&path, provider_id);
228 match form_spec {
229 Some(spec) => {
230 let json = serde_json::to_value(&spec).unwrap_or_default();
231 json_response(StatusCode::OK, &AdminResponse::ok(json))
232 }
233 None => json_response(
234 StatusCode::NOT_FOUND,
235 &AdminResponse::<()>::err("no setup spec found for provider"),
236 ),
237 }
238 }
239 None => json_response(
240 StatusCode::NOT_FOUND,
241 &AdminResponse::<()>::err(format!("provider pack not found: {provider_id}")),
242 ),
243 }
244}
245
246async fn handle_qa_validate(
247 req: Request<Incoming>,
248 state: &Arc<AdminState>,
249) -> Result<Response<Full<Bytes>>> {
250 let body = read_body(req).await?;
251 let request: Value = serde_json::from_slice(&body).unwrap_or_default();
252 let provider_id = request
253 .get("provider_id")
254 .and_then(Value::as_str)
255 .unwrap_or("");
256 let answers = request.get("answers").cloned().unwrap_or_default();
257
258 let providers_dir = state.bundle_root.join("providers");
259 let pack_path = find_provider_pack(&providers_dir, provider_id);
260
261 match pack_path {
262 Some(path) => {
263 let form_spec =
264 greentic_setup::setup_to_formspec::pack_to_form_spec(&path, provider_id);
265 match form_spec {
266 Some(spec) => {
267 match greentic_setup::qa::wizard::validate_answers_against_form_spec(
268 &spec, &answers,
269 ) {
270 Ok(()) => json_response(
271 StatusCode::OK,
272 &AdminResponse::ok(json!({"valid": true})),
273 ),
274 Err(e) => json_response(
275 StatusCode::OK,
276 &AdminResponse::ok(json!({"valid": false, "error": e.to_string()})),
277 ),
278 }
279 }
280 None => json_response(
281 StatusCode::OK,
282 &AdminResponse::ok(json!({"valid": true, "note": "no spec found"})),
283 ),
284 }
285 }
286 None => json_response(
287 StatusCode::NOT_FOUND,
288 &AdminResponse::<()>::err(format!("provider not found: {provider_id}")),
289 ),
290 }
291}
292
293async fn handle_qa_submit(
294 req: Request<Incoming>,
295 state: &Arc<AdminState>,
296) -> Result<Response<Full<Bytes>>> {
297 let body = read_body(req).await?;
298 let request: Value = serde_json::from_slice(&body).unwrap_or_default();
299 let provider_id = request
300 .get("provider_id")
301 .and_then(Value::as_str)
302 .unwrap_or("")
303 .to_string();
304 let answers = request.get("answers").cloned().unwrap_or_default();
305
306 if provider_id.is_empty() {
307 return json_response(
308 StatusCode::BAD_REQUEST,
309 &AdminResponse::<()>::err("provider_id required"),
310 );
311 }
312
313 let persisted = crate::qa_persist::persist_all_config_as_secrets(
315 &state.bundle_root,
316 &state.env,
317 &state.tenant,
318 state.team.as_deref(),
319 &provider_id,
320 &answers,
321 None,
322 )
323 .await;
324
325 match persisted {
326 Ok(keys) => json_response(
327 StatusCode::OK,
328 &AdminResponse::ok(json!({
329 "persisted_keys": keys,
330 "provider_id": provider_id,
331 })),
332 ),
333 Err(e) => json_response(
334 StatusCode::INTERNAL_SERVER_ERROR,
335 &AdminResponse::<()>::err(e.to_string()),
336 ),
337 }
338}
339
340fn find_provider_pack(providers_dir: &std::path::Path, provider_id: &str) -> Option<PathBuf> {
343 for dir_name in &["messaging", "events", "oauth", "secrets", "mcp"] {
345 let pack = providers_dir
346 .join(dir_name)
347 .join(format!("{provider_id}.gtpack"));
348 if pack.exists() {
349 return Some(pack);
350 }
351 }
352 let flat = providers_dir.join(format!("{provider_id}.gtpack"));
354 if flat.exists() {
355 return Some(flat);
356 }
357 None
358}
359
360fn json_response<T: serde::Serialize>(
361 status: StatusCode,
362 body: &T,
363) -> Result<Response<Full<Bytes>>> {
364 let json = serde_json::to_vec(body)?;
365 Ok(Response::builder()
366 .status(status)
367 .header("content-type", "application/json")
368 .body(Full::new(Bytes::from(json)))
369 .unwrap())
370}
371
372async fn read_body(req: Request<Incoming>) -> Result<Vec<u8>> {
373 use http_body_util::BodyExt;
374 let body = req.into_body().collect().await?.to_bytes();
375 Ok(body.to_vec())
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn find_provider_pack_returns_none_for_missing() {
384 let tmp = tempfile::tempdir().unwrap();
385 assert!(find_provider_pack(tmp.path(), "nonexistent").is_none());
386 }
387
388 #[test]
389 fn find_provider_pack_finds_flat_pack() {
390 let tmp = tempfile::tempdir().unwrap();
391 let pack = tmp.path().join("messaging-telegram.gtpack");
392 std::fs::write(&pack, "pack").unwrap();
393 assert_eq!(
394 find_provider_pack(tmp.path(), "messaging-telegram"),
395 Some(pack)
396 );
397 }
398}