1use crate::db::agent_runs;
2use crate::db::agent_versions;
3use crate::db::alerts as db_alerts;
4use crate::db::audit_log;
5use crate::db::tenant_agents::{
6 clone_templates_for_tenant, create_tenant_agent as db_create_tenant_agent,
7 delete_tenant_agent as db_delete_tenant_agent, list_agent_templates,
8 list_tenant_agents as db_list_tenant_agents, update_tenant_agent as db_update_tenant_agent,
9 AgentTemplate, CreateTenantAgentRequest, TenantAgent, UpdateTenantAgentRequest,
10};
11use crate::db::tenants::UsageSummary;
12use crate::llm::provider_registry::ModelInfo;
13use crate::models::{Tenant, TenantTier};
14use crate::types::{AppError, Result};
15use crate::AppState;
16use axum::{
17 extract::{Path, Query, State},
18 http::StatusCode,
19 middleware::Next,
20 response::Response,
21 Json,
22};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Debug, Deserialize)]
28struct AdminClaims {
29 pub sub: String,
30 pub email: String,
31 pub exp: usize,
32 pub iat: usize,
33 #[serde(default)]
34 pub roles: HashMap<String, Vec<RoleEntry>>,
35}
36
37#[derive(Debug, Deserialize)]
38struct RoleEntry {
39 pub role: String,
40 #[allow(dead_code)]
41 pub resource_id: Option<String>,
42}
43
44fn has_admin_role(claims: &AdminClaims) -> bool {
46 for product in ["admin", "ares", "eruka"] {
47 if let Some(entries) = claims.roles.get(product) {
48 if entries.iter().any(|e| e.role == "admin") {
49 return true;
50 }
51 }
52 }
53 false
54}
55
56pub async fn admin_middleware(req: axum::extract::Request, next: Next) -> Response {
57 let admin_secret = std::env::var("ADMIN_API_KEY").ok();
59 let header_secret = req
60 .headers()
61 .get("x-admin-secret")
62 .and_then(|v| v.to_str().ok())
63 .map(String::from);
64
65 if let (Some(expected), Some(given)) = (&admin_secret, &header_secret) {
66 if expected == given {
67 return next.run(req).await;
68 }
69 }
70
71 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_default();
73 if !jwt_secret.is_empty() {
74 if let Some(token) = req
75 .headers()
76 .get("authorization")
77 .and_then(|v| v.to_str().ok())
78 .and_then(|v| v.strip_prefix("Bearer "))
79 {
80 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
81 validation.leeway = 60;
82 if let Ok(data) = jsonwebtoken::decode::<AdminClaims>(
83 token,
84 &jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
85 &validation,
86 ) {
87 if has_admin_role(&data.claims) {
88 return next.run(req).await;
89 }
90 }
91 }
92 }
93
94 Response::builder()
95 .status(StatusCode::UNAUTHORIZED)
96 .header("Content-Type", "application/json")
97 .body(r#"{"error":"Admin access requires X-Admin-Secret header or JWT with admin role"}"#.into())
98 .unwrap()
99}
100
101#[derive(Debug, Deserialize, Serialize)]
102pub struct CreateTenantRequest {
103 pub name: String,
104 pub tier: String,
105}
106
107#[derive(Debug, Deserialize, Serialize)]
108pub struct CreateApiKeyRequest {
109 pub name: String,
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113pub struct UpdateQuotaRequest {
114 pub tier: String,
115}
116
117#[derive(Debug, Serialize)]
118pub struct TenantResponse {
119 pub id: String,
120 pub name: String,
121 pub tier: String,
122 pub created_at: i64,
123}
124
125impl From<Tenant> for TenantResponse {
126 fn from(t: Tenant) -> Self {
127 Self {
128 id: t.id,
129 name: t.name,
130 tier: t.tier.as_str().to_string(),
131 created_at: t.created_at,
132 }
133 }
134}
135
136#[derive(Debug, Serialize)]
137pub struct ApiKeyResponse {
138 pub id: String,
139 pub tenant_id: String,
140 pub key_prefix: String,
141 pub name: String,
142 pub is_active: bool,
143 pub created_at: i64,
144}
145
146impl From<crate::models::ApiKey> for ApiKeyResponse {
147 fn from(k: crate::models::ApiKey) -> Self {
148 Self {
149 id: k.id,
150 tenant_id: k.tenant_id,
151 key_prefix: k.key_prefix,
152 name: k.name,
153 is_active: k.is_active,
154 created_at: k.created_at,
155 }
156 }
157}
158
159#[derive(Debug, Serialize)]
160pub struct UsageResponse {
161 pub monthly_requests: u64,
162 pub monthly_tokens: u64,
163 pub daily_requests: u64,
164}
165
166impl From<UsageSummary> for UsageResponse {
167 fn from(u: UsageSummary) -> Self {
168 Self {
169 monthly_requests: u.monthly_requests,
170 monthly_tokens: u.monthly_tokens,
171 daily_requests: u.daily_requests,
172 }
173 }
174}
175
176pub async fn create_tenant(
177 State(state): State<AppState>,
178 Json(payload): Json<CreateTenantRequest>,
179) -> Result<Json<TenantResponse>> {
180 let tier = TenantTier::from_str(&payload.tier).ok_or_else(|| {
181 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
182 })?;
183
184 let tenant = state.tenant_db.create_tenant(payload.name, tier).await?;
185
186 let pool = state.tenant_db.pool().clone();
187 let tid = tenant.id.clone();
188 tokio::spawn(async move {
189 let _ =
190 audit_log::log_admin_action(&pool, "create_tenant", "tenant", &tid, None, None).await;
191 });
192
193 Ok(Json(TenantResponse::from(tenant)))
194}
195
196pub async fn list_tenants(State(state): State<AppState>) -> Result<Json<Vec<TenantResponse>>> {
197 let tenants = state.tenant_db.list_tenants().await?;
198 let response: Vec<TenantResponse> = tenants.into_iter().map(|t| t.into()).collect();
199
200 Ok(Json(response))
201}
202
203pub async fn get_tenant(
204 State(state): State<AppState>,
205 Path(tenant_id): Path<String>,
206) -> Result<Json<TenantResponse>> {
207 let tenant = state
208 .tenant_db
209 .get_tenant(&tenant_id)
210 .await?
211 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
212
213 Ok(Json(TenantResponse::from(tenant)))
214}
215
216pub async fn create_api_key(
217 State(state): State<AppState>,
218 Path(tenant_id): Path<String>,
219 Json(payload): Json<CreateApiKeyRequest>,
220) -> Result<Json<serde_json::Value>> {
221 let (api_key, raw_key) = state
222 .tenant_db
223 .create_api_key(&tenant_id, payload.name)
224 .await?;
225
226 let pool = state.tenant_db.pool().clone();
227 let kid = api_key.id.clone();
228 tokio::spawn(async move {
229 let _ =
230 audit_log::log_admin_action(&pool, "create_api_key", "api_key", &kid, None, None).await;
231 });
232
233 Ok(Json(serde_json::json!({
234 "api_key": api_key,
235 "raw_key": raw_key,
236 "warning": "Store this raw key securely. You will not be able to retrieve it again."
237 })))
238}
239
240pub async fn list_api_keys(
241 State(state): State<AppState>,
242 Path(tenant_id): Path<String>,
243) -> Result<Json<Vec<ApiKeyResponse>>> {
244 let keys = state.tenant_db.list_api_keys(&tenant_id).await?;
245 let response: Vec<ApiKeyResponse> = keys.into_iter().map(|k| k.into()).collect();
246
247 Ok(Json(response))
248}
249
250pub async fn get_tenant_usage(
251 State(state): State<AppState>,
252 Path(tenant_id): Path<String>,
253) -> Result<Json<UsageResponse>> {
254 let _ = state
255 .tenant_db
256 .get_tenant(&tenant_id)
257 .await?
258 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
259
260 let usage = state.tenant_db.get_usage_summary(&tenant_id).await?;
261
262 Ok(Json(UsageResponse::from(usage)))
263}
264
265pub async fn update_tenant_quota(
266 State(state): State<AppState>,
267 Path(tenant_id): Path<String>,
268 Json(payload): Json<UpdateQuotaRequest>,
269) -> Result<Json<TenantResponse>> {
270 let tier = TenantTier::from_str(&payload.tier).ok_or_else(|| {
271 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
272 })?;
273
274 state
275 .tenant_db
276 .update_tenant_quota(&tenant_id, tier)
277 .await?;
278
279 let tenant = state
280 .tenant_db
281 .get_tenant(&tenant_id)
282 .await?
283 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
284
285 let pool = state.tenant_db.pool().clone();
286 let tid = tenant_id.clone();
287 let details = format!("{{\"new_tier\":\"{}\"}}", payload.tier);
288 tokio::spawn(async move {
289 let _ = audit_log::log_admin_action(
290 &pool,
291 "update_quota",
292 "tenant",
293 &tid,
294 Some(&details),
295 None,
296 )
297 .await;
298 });
299
300 Ok(Json(TenantResponse::from(tenant)))
301}
302
303#[derive(Debug, Deserialize)]
308pub struct ProvisionClientRequest {
309 pub name: String,
310 pub tier: String,
311 pub product_type: String,
312 pub api_key_name: String,
313}
314
315#[derive(Debug, Serialize)]
316pub struct ProvisionClientResponse {
317 pub tenant_id: String,
318 pub tenant_name: String,
319 pub tier: String,
320 pub product_type: String,
321 pub api_key_id: String,
322 pub api_key_prefix: String,
323 pub raw_api_key: String,
324 pub agents_created: Vec<String>,
325}
326
327pub async fn provision_client(
328 State(state): State<AppState>,
329 Json(req): Json<ProvisionClientRequest>,
330) -> Result<Json<ProvisionClientResponse>> {
331 let tier = TenantTier::from_str(&req.tier).ok_or_else(|| {
332 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
333 })?;
334
335 let product_type = req.product_type.to_lowercase();
338
339 let tenant = state.tenant_db.create_tenant(req.name, tier).await?;
340
341 let agents =
342 clone_templates_for_tenant(state.tenant_db.pool(), &tenant.id, &product_type).await?;
343
344 let (api_key, raw_key) = state
345 .tenant_db
346 .create_api_key(&tenant.id, req.api_key_name)
347 .await?;
348
349 let pool = state.tenant_db.pool().clone();
350 let tid = tenant.id.clone();
351 let details = format!(
352 "{{\"product_type\":\"{}\",\"tier\":\"{}\"}}",
353 product_type,
354 tenant.tier.as_str()
355 );
356 tokio::spawn(async move {
357 let _ = audit_log::log_admin_action(
358 &pool,
359 "provision_client",
360 "tenant",
361 &tid,
362 Some(&details),
363 None,
364 )
365 .await;
366 });
367
368 Ok(Json(ProvisionClientResponse {
369 tenant_id: tenant.id,
370 tenant_name: tenant.name,
371 tier: tenant.tier.as_str().to_string(),
372 product_type,
373 api_key_id: api_key.id,
374 api_key_prefix: api_key.key_prefix,
375 raw_api_key: raw_key,
376 agents_created: agents.into_iter().map(|a| a.agent_name).collect(),
377 }))
378}
379
380pub async fn list_tenant_agents_handler(
385 State(state): State<AppState>,
386 Path(tenant_id): Path<String>,
387) -> Result<Json<Vec<TenantAgent>>> {
388 let agents = db_list_tenant_agents(state.tenant_db.pool(), &tenant_id).await?;
389 Ok(Json(agents))
390}
391
392pub async fn create_tenant_agent_handler(
393 State(state): State<AppState>,
394 Path(tenant_id): Path<String>,
395 Json(req): Json<CreateTenantAgentRequest>,
396) -> Result<Json<TenantAgent>> {
397 let agent = db_create_tenant_agent(state.tenant_db.pool(), &tenant_id, req).await?;
398
399 let pool = state.tenant_db.pool().clone();
400 let aid = agent.id.clone();
401 tokio::spawn(async move {
402 let _ = audit_log::log_admin_action(&pool, "create_agent", "agent", &aid, None, None).await;
403 });
404
405 Ok(Json(agent))
406}
407
408pub async fn update_tenant_agent_handler(
409 State(state): State<AppState>,
410 Path((tenant_id, agent_name)): Path<(String, String)>,
411 Json(req): Json<UpdateTenantAgentRequest>,
412) -> Result<Json<TenantAgent>> {
413 let agent =
414 db_update_tenant_agent(state.tenant_db.pool(), &tenant_id, &agent_name, req).await?;
415
416 let pool = state.tenant_db.pool().clone();
417 let aid = agent.id.clone();
418 tokio::spawn(async move {
419 let _ = audit_log::log_admin_action(&pool, "update_agent", "agent", &aid, None, None).await;
420 });
421
422 Ok(Json(agent))
423}
424
425pub async fn delete_tenant_agent_handler(
426 State(state): State<AppState>,
427 Path((tenant_id, agent_name)): Path<(String, String)>,
428) -> Result<StatusCode> {
429 db_delete_tenant_agent(state.tenant_db.pool(), &tenant_id, &agent_name).await?;
430
431 let pool = state.tenant_db.pool().clone();
432 let resource_id = format!("{}:{}", tenant_id, agent_name);
433 tokio::spawn(async move {
434 let _ =
435 audit_log::log_admin_action(&pool, "delete_agent", "agent", &resource_id, None, None)
436 .await;
437 });
438
439 Ok(StatusCode::NO_CONTENT)
440}
441
442pub async fn list_agent_templates_handler(
447 State(state): State<AppState>,
448 Query(params): Query<HashMap<String, String>>,
449) -> Result<Json<Vec<AgentTemplate>>> {
450 let product_type = params.get("product_type").map(|s| s.as_str());
451 let templates = list_agent_templates(state.tenant_db.pool(), product_type).await?;
452 Ok(Json(templates))
453}
454
455pub async fn list_models_handler(State(state): State<AppState>) -> Result<Json<Vec<ModelInfo>>> {
456 Ok(Json(state.provider_registry.list_models()))
457}
458
459#[derive(Debug, Deserialize)]
464pub struct AlertsQuery {
465 pub severity: Option<String>,
466 pub resolved: Option<bool>,
467 pub limit: Option<i64>,
468}
469
470pub async fn list_alerts(
471 State(state): State<AppState>,
472 Query(q): Query<AlertsQuery>,
473) -> Result<Json<Vec<db_alerts::Alert>>> {
474 let limit = q.limit.unwrap_or(50).min(200);
475 let alerts = db_alerts::list_alerts(
476 state.tenant_db.pool(),
477 q.severity.as_deref(),
478 q.resolved,
479 limit,
480 )
481 .await?;
482 Ok(Json(alerts))
483}
484
485#[derive(Debug, Deserialize)]
486pub struct ResolveAlertRequest {
487 pub resolved_by: Option<String>,
488}
489
490pub async fn resolve_alert(
491 State(state): State<AppState>,
492 Path(alert_id): Path<String>,
493 Json(payload): Json<ResolveAlertRequest>,
494) -> Result<StatusCode> {
495 db_alerts::resolve_alert(
496 state.tenant_db.pool(),
497 &alert_id,
498 payload.resolved_by.as_deref(),
499 )
500 .await?;
501
502 let pool = state.tenant_db.pool().clone();
503 tokio::spawn(async move {
504 let _ = audit_log::log_admin_action(&pool, "resolve_alert", "alert", &alert_id, None, None)
505 .await;
506 });
507
508 Ok(StatusCode::OK)
509}
510
511#[derive(Debug, Deserialize)]
516pub struct AuditLogQuery {
517 pub limit: Option<i64>,
518 pub offset: Option<i64>,
519}
520
521pub async fn list_audit_log(
522 State(state): State<AppState>,
523 Query(q): Query<AuditLogQuery>,
524) -> Result<Json<Vec<audit_log::AuditLogEntry>>> {
525 let limit = q.limit.unwrap_or(50).min(200);
526 let offset = q.offset.unwrap_or(0);
527 let entries = audit_log::list_audit_log(state.tenant_db.pool(), limit, offset).await?;
528 Ok(Json(entries))
529}
530
531#[derive(Debug, Deserialize)]
536pub struct DailyUsageQuery {
537 pub days: Option<i64>,
538}
539
540#[derive(Debug, Serialize)]
541pub struct DailyUsageEntry {
542 pub date: i64,
543 pub requests: i64,
544 pub tokens: i64,
545}
546
547pub async fn get_daily_usage(
548 State(state): State<AppState>,
549 Path(tenant_id): Path<String>,
550 Query(q): Query<DailyUsageQuery>,
551) -> Result<Json<Vec<DailyUsageEntry>>> {
552 let days = q.days.unwrap_or(30).min(90);
553 let now_ts = std::time::SystemTime::now()
554 .duration_since(std::time::UNIX_EPOCH)
555 .unwrap()
556 .as_secs() as i64;
557 let start_ts = now_ts - (days * 86400);
558
559 let rows = sqlx::query(
560 "SELECT
561 (created_at / 86400) * 86400 as day_ts,
562 COUNT(*) as requests,
563 COALESCE(SUM(input_tokens + output_tokens)::bigint, 0) as tokens
564 FROM agent_runs
565 WHERE tenant_id = $1 AND created_at >= $2
566 GROUP BY day_ts ORDER BY day_ts",
567 )
568 .bind(&tenant_id)
569 .bind(start_ts)
570 .fetch_all(state.tenant_db.pool())
571 .await
572 .map_err(|e| AppError::Database(e.to_string()))?;
573
574 use sqlx::Row;
575 let entries: Vec<DailyUsageEntry> = rows
576 .iter()
577 .map(|row| DailyUsageEntry {
578 date: row.get("day_ts"),
579 requests: row.get("requests"),
580 tokens: row.get("tokens"),
581 })
582 .collect();
583
584 Ok(Json(entries))
585}
586
587#[derive(Debug, Deserialize)]
592pub struct AgentRunsQuery {
593 pub limit: Option<i64>,
594 pub offset: Option<i64>,
595}
596
597pub async fn list_agent_runs_handler(
598 State(state): State<AppState>,
599 Path((tenant_id, agent_name)): Path<(String, String)>,
600 Query(q): Query<AgentRunsQuery>,
601) -> Result<Json<Vec<agent_runs::AgentRun>>> {
602 let limit = q.limit.unwrap_or(50).min(200);
603 let offset = q.offset.unwrap_or(0);
604 let runs = agent_runs::list_agent_runs(
605 state.tenant_db.pool(),
606 &tenant_id,
607 Some(&agent_name),
608 limit,
609 offset,
610 )
611 .await?;
612 Ok(Json(runs))
613}
614
615pub async fn get_agent_stats_handler(
616 State(state): State<AppState>,
617 Path((tenant_id, agent_name)): Path<(String, String)>,
618) -> Result<Json<agent_runs::AgentRunStats>> {
619 let stats =
620 agent_runs::get_agent_run_stats(state.tenant_db.pool(), &tenant_id, &agent_name).await?;
621 Ok(Json(stats))
622}
623
624pub async fn list_all_agents_handler(
629 State(state): State<AppState>,
630) -> Result<Json<Vec<agent_runs::AllAgentsEntry>>> {
631 let agents = agent_runs::list_all_agents(state.tenant_db.pool()).await?;
632 Ok(Json(agents))
633}
634
635pub async fn get_platform_stats(
640 State(state): State<AppState>,
641) -> Result<Json<agent_runs::PlatformStats>> {
642 let stats = agent_runs::get_platform_stats(state.tenant_db.pool()).await?;
643 Ok(Json(stats))
644}
645
646pub async fn list_agent_versions_handler(
653 State(state): State<AppState>,
654 Path(agent_id): Path<String>,
655) -> Result<Json<Vec<agent_versions::AgentVersionRecord>>> {
656 let records = agent_versions::get_agent_version_history(
657 state.tenant_db.pool(),
658 &agent_id,
659 50,
660 )
661 .await
662 .map_err(|e| AppError::Database(e.to_string()))?;
663
664 Ok(Json(records))
665}
666
667pub async fn rollback_agent_handler(
671 State(state): State<AppState>,
672 Path((agent_id, version)): Path<(String, String)>,
673) -> Result<Json<serde_json::Value>> {
674 let history = agent_versions::get_agent_version_history(
676 state.tenant_db.pool(),
677 &agent_id,
678 100,
679 )
680 .await
681 .map_err(|e| AppError::Database(e.to_string()))?;
682
683 let record = history
684 .into_iter()
685 .find(|r| r.version == version)
686 .ok_or_else(|| {
687 AppError::NotFound(format!(
688 "No version '{}' found for agent '{}'",
689 version, agent_id
690 ))
691 })?;
692
693 let agent_config: crate::utils::toon_config::ToonAgentConfig =
695 serde_json::from_value(record.config_json).map_err(|e| {
696 AppError::InvalidInput(format!("Failed to deserialize agent config: {}", e))
697 })?;
698
699 state.dynamic_config.upsert_agent(agent_config.clone());
701
702 let pool = state.tenant_db.pool().clone();
704 let _ = agent_versions::record_agent_versions(&pool, &[agent_config], "rollback").await;
705
706 let pool2 = state.tenant_db.pool().clone();
708 let aid = agent_id.clone();
709 let ver = version.clone();
710 tokio::spawn(async move {
711 let _ = audit_log::log_admin_action(
712 &pool2,
713 "agent_rollback",
714 "agent",
715 &aid,
716 Some(&format!("Rolled back to version {}", ver)),
717 None,
718 )
719 .await;
720 });
721
722 tracing::info!(agent_id = %agent_id, version = %version, "Agent rolled back");
723
724 Ok(Json(serde_json::json!({
725 "agent_id": agent_id,
726 "version": version,
727 "status": "rolled_back"
728 })))
729}
730
731#[derive(Debug, Deserialize)]
732pub struct EmergencyStopRequest {
733 pub active: bool,
734}
735
736pub async fn emergency_stop_handler(
740 State(state): State<AppState>,
741 Json(payload): Json<EmergencyStopRequest>,
742) -> Result<Json<serde_json::Value>> {
743 state
744 .emergency_stop
745 .store(payload.active, std::sync::atomic::Ordering::Relaxed);
746
747 let action = if payload.active {
748 "emergency_stop_enabled"
749 } else {
750 "emergency_stop_disabled"
751 };
752 tracing::warn!(active = payload.active, "Emergency stop toggled");
753
754 let pool = state.tenant_db.pool().clone();
755 tokio::spawn(async move {
756 let _ = audit_log::log_admin_action(&pool, action, "platform", "all_agents", None, None)
757 .await;
758 });
759
760 Ok(Json(serde_json::json!({
761 "emergency_stop": payload.active,
762 "message": if payload.active {
763 "All agents are now in emergency stop mode. /api/v1/chat requests will return 503."
764 } else {
765 "Emergency stop cleared. Agents are operational."
766 }
767 })))
768}