1use crate::db::tenants::UsageSummary;
2use crate::db::tenant_agents::{
3 AgentTemplate, CreateTenantAgentRequest, TenantAgent, UpdateTenantAgentRequest,
4 clone_templates_for_tenant, create_tenant_agent as db_create_tenant_agent,
5 delete_tenant_agent as db_delete_tenant_agent, list_agent_templates,
6 list_tenant_agents as db_list_tenant_agents,
7 update_tenant_agent as db_update_tenant_agent,
8};
9use crate::db::agent_runs;
10use crate::db::alerts as db_alerts;
11use crate::db::audit_log;
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
26pub async fn admin_middleware(
27 req: axum::extract::Request,
28 next: Next,
29) -> Response {
30 let admin_secret = std::env::var("ADMIN_API_KEY").ok();
31
32 let header_secret = req
33 .headers()
34 .get("x-admin-secret")
35 .and_then(|v| v.to_str().ok());
36
37 match (admin_secret, header_secret) {
38 (Some(expected), Some(given)) if expected == given => {
39 next.run(req).await
40 }
41 _ => {
42 Response::builder()
43 .status(StatusCode::UNAUTHORIZED)
44 .header("Content-Type", "application/json")
45 .body(r#"{"error":"Invalid or missing X-Admin-Secret header"}"#.into())
46 .unwrap()
47 }
48 }
49}
50
51#[derive(Debug, Deserialize, Serialize)]
52pub struct CreateTenantRequest {
53 pub name: String,
54 pub tier: String,
55}
56
57#[derive(Debug, Deserialize, Serialize)]
58pub struct CreateApiKeyRequest {
59 pub name: String,
60}
61
62#[derive(Debug, Deserialize, Serialize)]
63pub struct UpdateQuotaRequest {
64 pub tier: String,
65}
66
67#[derive(Debug, Serialize)]
68pub struct TenantResponse {
69 pub id: String,
70 pub name: String,
71 pub tier: String,
72 pub created_at: i64,
73}
74
75impl From<Tenant> for TenantResponse {
76 fn from(t: Tenant) -> Self {
77 Self {
78 id: t.id,
79 name: t.name,
80 tier: t.tier.as_str().to_string(),
81 created_at: t.created_at,
82 }
83 }
84}
85
86#[derive(Debug, Serialize)]
87pub struct ApiKeyResponse {
88 pub id: String,
89 pub tenant_id: String,
90 pub key_prefix: String,
91 pub name: String,
92 pub is_active: bool,
93 pub created_at: i64,
94}
95
96impl From<crate::models::ApiKey> for ApiKeyResponse {
97 fn from(k: crate::models::ApiKey) -> Self {
98 Self {
99 id: k.id,
100 tenant_id: k.tenant_id,
101 key_prefix: k.key_prefix,
102 name: k.name,
103 is_active: k.is_active,
104 created_at: k.created_at,
105 }
106 }
107}
108
109#[derive(Debug, Serialize)]
110pub struct UsageResponse {
111 pub monthly_requests: u64,
112 pub monthly_tokens: u64,
113 pub daily_requests: u64,
114}
115
116impl From<UsageSummary> for UsageResponse {
117 fn from(u: UsageSummary) -> Self {
118 Self {
119 monthly_requests: u.monthly_requests,
120 monthly_tokens: u.monthly_tokens,
121 daily_requests: u.daily_requests,
122 }
123 }
124}
125
126pub async fn create_tenant(
127 State(state): State<AppState>,
128 Json(payload): Json<CreateTenantRequest>,
129) -> Result<Json<TenantResponse>> {
130 let tier = TenantTier::from_str(&payload.tier).ok_or_else(|| {
131 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
132 })?;
133
134 let tenant = state.tenant_db.create_tenant(payload.name, tier).await?;
135
136 let pool = state.tenant_db.pool().clone();
137 let tid = tenant.id.clone();
138 tokio::spawn(async move {
139 let _ = audit_log::log_admin_action(&pool, "create_tenant", "tenant", &tid, None, None).await;
140 });
141
142 Ok(Json(TenantResponse::from(tenant)))
143}
144
145pub async fn list_tenants(
146 State(state): State<AppState>,
147) -> Result<Json<Vec<TenantResponse>>> {
148 let tenants = state.tenant_db.list_tenants().await?;
149 let response: Vec<TenantResponse> = tenants.into_iter().map(|t| t.into()).collect();
150
151 Ok(Json(response))
152}
153
154pub async fn get_tenant(
155 State(state): State<AppState>,
156 Path(tenant_id): Path<String>,
157) -> Result<Json<TenantResponse>> {
158 let tenant = state.tenant_db.get_tenant(&tenant_id).await?
159 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
160
161 Ok(Json(TenantResponse::from(tenant)))
162}
163
164pub async fn create_api_key(
165 State(state): State<AppState>,
166 Path(tenant_id): Path<String>,
167 Json(payload): Json<CreateApiKeyRequest>,
168) -> Result<Json<serde_json::Value>> {
169 let (api_key, raw_key) = state.tenant_db.create_api_key(&tenant_id, payload.name).await?;
170
171 let pool = state.tenant_db.pool().clone();
172 let kid = api_key.id.clone();
173 tokio::spawn(async move {
174 let _ = audit_log::log_admin_action(&pool, "create_api_key", "api_key", &kid, None, None).await;
175 });
176
177 Ok(Json(serde_json::json!({
178 "api_key": api_key,
179 "raw_key": raw_key,
180 "warning": "Store this raw key securely. You will not be able to retrieve it again."
181 })))
182}
183
184pub async fn list_api_keys(
185 State(state): State<AppState>,
186 Path(tenant_id): Path<String>,
187) -> Result<Json<Vec<ApiKeyResponse>>> {
188 let keys = state.tenant_db.list_api_keys(&tenant_id).await?;
189 let response: Vec<ApiKeyResponse> = keys.into_iter().map(|k| k.into()).collect();
190
191 Ok(Json(response))
192}
193
194pub async fn get_tenant_usage(
195 State(state): State<AppState>,
196 Path(tenant_id): Path<String>,
197) -> Result<Json<UsageResponse>> {
198 let _ = state.tenant_db.get_tenant(&tenant_id).await?
199 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
200
201 let usage = state.tenant_db.get_usage_summary(&tenant_id).await?;
202
203 Ok(Json(UsageResponse::from(usage)))
204}
205
206pub async fn update_tenant_quota(
207 State(state): State<AppState>,
208 Path(tenant_id): Path<String>,
209 Json(payload): Json<UpdateQuotaRequest>,
210) -> Result<Json<TenantResponse>> {
211 let tier = TenantTier::from_str(&payload.tier).ok_or_else(|| {
212 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
213 })?;
214
215 state.tenant_db.update_tenant_quota(&tenant_id, tier).await?;
216
217 let tenant = state.tenant_db.get_tenant(&tenant_id).await?
218 .ok_or_else(|| AppError::NotFound("Tenant not found".to_string()))?;
219
220 let pool = state.tenant_db.pool().clone();
221 let tid = tenant_id.clone();
222 let details = format!("{{\"new_tier\":\"{}\"}}", payload.tier);
223 tokio::spawn(async move {
224 let _ = audit_log::log_admin_action(&pool, "update_quota", "tenant", &tid, Some(&details), None).await;
225 });
226
227 Ok(Json(TenantResponse::from(tenant)))
228}
229
230#[derive(Debug, Deserialize)]
235pub struct ProvisionClientRequest {
236 pub name: String,
237 pub tier: String,
238 pub product_type: String,
239 pub api_key_name: String,
240}
241
242#[derive(Debug, Serialize)]
243pub struct ProvisionClientResponse {
244 pub tenant_id: String,
245 pub tenant_name: String,
246 pub tier: String,
247 pub product_type: String,
248 pub api_key_id: String,
249 pub api_key_prefix: String,
250 pub raw_api_key: String,
251 pub agents_created: Vec<String>,
252}
253
254pub async fn provision_client(
255 State(state): State<AppState>,
256 Json(req): Json<ProvisionClientRequest>,
257) -> Result<Json<ProvisionClientResponse>> {
258 let tier = TenantTier::from_str(&req.tier).ok_or_else(|| {
259 AppError::InvalidInput("Invalid tier. Must be: free, dev, pro, or enterprise".to_string())
260 })?;
261
262 let product_type = req.product_type.to_lowercase();
265
266 let tenant = state.tenant_db.create_tenant(req.name, tier).await?;
267
268 let agents = clone_templates_for_tenant(
269 state.tenant_db.pool(),
270 &tenant.id,
271 &product_type,
272 ).await?;
273
274 let (api_key, raw_key) = state.tenant_db.create_api_key(&tenant.id, req.api_key_name).await?;
275
276 let pool = state.tenant_db.pool().clone();
277 let tid = tenant.id.clone();
278 let details = format!("{{\"product_type\":\"{}\",\"tier\":\"{}\"}}", product_type, tenant.tier.as_str());
279 tokio::spawn(async move {
280 let _ = audit_log::log_admin_action(&pool, "provision_client", "tenant", &tid, Some(&details), None).await;
281 });
282
283 Ok(Json(ProvisionClientResponse {
284 tenant_id: tenant.id,
285 tenant_name: tenant.name,
286 tier: tenant.tier.as_str().to_string(),
287 product_type,
288 api_key_id: api_key.id,
289 api_key_prefix: api_key.key_prefix,
290 raw_api_key: raw_key,
291 agents_created: agents.into_iter().map(|a| a.agent_name).collect(),
292 }))
293}
294
295pub async fn list_tenant_agents_handler(
300 State(state): State<AppState>,
301 Path(tenant_id): Path<String>,
302) -> Result<Json<Vec<TenantAgent>>> {
303 let agents = db_list_tenant_agents(state.tenant_db.pool(), &tenant_id).await?;
304 Ok(Json(agents))
305}
306
307pub async fn create_tenant_agent_handler(
308 State(state): State<AppState>,
309 Path(tenant_id): Path<String>,
310 Json(req): Json<CreateTenantAgentRequest>,
311) -> Result<Json<TenantAgent>> {
312 let agent = db_create_tenant_agent(state.tenant_db.pool(), &tenant_id, req).await?;
313
314 let pool = state.tenant_db.pool().clone();
315 let aid = agent.id.clone();
316 tokio::spawn(async move {
317 let _ = audit_log::log_admin_action(&pool, "create_agent", "agent", &aid, None, None).await;
318 });
319
320 Ok(Json(agent))
321}
322
323pub async fn update_tenant_agent_handler(
324 State(state): State<AppState>,
325 Path((tenant_id, agent_name)): Path<(String, String)>,
326 Json(req): Json<UpdateTenantAgentRequest>,
327) -> Result<Json<TenantAgent>> {
328 let agent = db_update_tenant_agent(state.tenant_db.pool(), &tenant_id, &agent_name, req).await?;
329
330 let pool = state.tenant_db.pool().clone();
331 let aid = agent.id.clone();
332 tokio::spawn(async move {
333 let _ = audit_log::log_admin_action(&pool, "update_agent", "agent", &aid, None, None).await;
334 });
335
336 Ok(Json(agent))
337}
338
339pub async fn delete_tenant_agent_handler(
340 State(state): State<AppState>,
341 Path((tenant_id, agent_name)): Path<(String, String)>,
342) -> Result<StatusCode> {
343 db_delete_tenant_agent(state.tenant_db.pool(), &tenant_id, &agent_name).await?;
344
345 let pool = state.tenant_db.pool().clone();
346 let resource_id = format!("{}:{}", tenant_id, agent_name);
347 tokio::spawn(async move {
348 let _ = audit_log::log_admin_action(&pool, "delete_agent", "agent", &resource_id, None, None).await;
349 });
350
351 Ok(StatusCode::NO_CONTENT)
352}
353
354pub async fn list_agent_templates_handler(
359 State(state): State<AppState>,
360 Query(params): Query<HashMap<String, String>>,
361) -> Result<Json<Vec<AgentTemplate>>> {
362 let product_type = params.get("product_type").map(|s| s.as_str());
363 let templates = list_agent_templates(state.tenant_db.pool(), product_type).await?;
364 Ok(Json(templates))
365}
366
367pub async fn list_models_handler(
368 State(state): State<AppState>,
369) -> Result<Json<Vec<ModelInfo>>> {
370 Ok(Json(state.provider_registry.list_models()))
371}
372
373#[derive(Debug, Deserialize)]
378pub struct AlertsQuery {
379 pub severity: Option<String>,
380 pub resolved: Option<bool>,
381 pub limit: Option<i64>,
382}
383
384pub async fn list_alerts(
385 State(state): State<AppState>,
386 Query(q): Query<AlertsQuery>,
387) -> Result<Json<Vec<db_alerts::Alert>>> {
388 let limit = q.limit.unwrap_or(50).min(200);
389 let alerts = db_alerts::list_alerts(
390 state.tenant_db.pool(),
391 q.severity.as_deref(),
392 q.resolved,
393 limit,
394 ).await?;
395 Ok(Json(alerts))
396}
397
398#[derive(Debug, Deserialize)]
399pub struct ResolveAlertRequest {
400 pub resolved_by: Option<String>,
401}
402
403pub async fn resolve_alert(
404 State(state): State<AppState>,
405 Path(alert_id): Path<String>,
406 Json(payload): Json<ResolveAlertRequest>,
407) -> Result<StatusCode> {
408 db_alerts::resolve_alert(
409 state.tenant_db.pool(),
410 &alert_id,
411 payload.resolved_by.as_deref(),
412 ).await?;
413
414 let pool = state.tenant_db.pool().clone();
415 tokio::spawn(async move {
416 let _ = audit_log::log_admin_action(
417 &pool, "resolve_alert", "alert", &alert_id, None, None,
418 ).await;
419 });
420
421 Ok(StatusCode::OK)
422}
423
424#[derive(Debug, Deserialize)]
429pub struct AuditLogQuery {
430 pub limit: Option<i64>,
431 pub offset: Option<i64>,
432}
433
434pub async fn list_audit_log(
435 State(state): State<AppState>,
436 Query(q): Query<AuditLogQuery>,
437) -> Result<Json<Vec<audit_log::AuditLogEntry>>> {
438 let limit = q.limit.unwrap_or(50).min(200);
439 let offset = q.offset.unwrap_or(0);
440 let entries = audit_log::list_audit_log(state.tenant_db.pool(), limit, offset).await?;
441 Ok(Json(entries))
442}
443
444#[derive(Debug, Deserialize)]
449pub struct DailyUsageQuery {
450 pub days: Option<i64>,
451}
452
453#[derive(Debug, Serialize)]
454pub struct DailyUsageEntry {
455 pub date: i64,
456 pub requests: i64,
457 pub tokens: i64,
458}
459
460pub async fn get_daily_usage(
461 State(state): State<AppState>,
462 Path(tenant_id): Path<String>,
463 Query(q): Query<DailyUsageQuery>,
464) -> Result<Json<Vec<DailyUsageEntry>>> {
465 let days = q.days.unwrap_or(30).min(90);
466 let now_ts = std::time::SystemTime::now()
467 .duration_since(std::time::UNIX_EPOCH)
468 .unwrap()
469 .as_secs() as i64;
470 let start_ts = now_ts - (days * 86400);
471
472 let rows = sqlx::query(
473 "SELECT
474 (created_at / 86400) * 86400 as day_ts,
475 COUNT(*) as requests,
476 COALESCE(SUM(input_tokens + output_tokens), 0) as tokens
477 FROM agent_runs
478 WHERE tenant_id = $1 AND created_at >= $2
479 GROUP BY day_ts ORDER BY day_ts"
480 )
481 .bind(&tenant_id)
482 .bind(start_ts)
483 .fetch_all(state.tenant_db.pool())
484 .await
485 .map_err(|e| AppError::Database(e.to_string()))?;
486
487 use sqlx::Row;
488 let entries: Vec<DailyUsageEntry> = rows.iter().map(|row| {
489 DailyUsageEntry {
490 date: row.get("day_ts"),
491 requests: row.get("requests"),
492 tokens: row.get("tokens"),
493 }
494 }).collect();
495
496 Ok(Json(entries))
497}
498
499#[derive(Debug, Deserialize)]
504pub struct AgentRunsQuery {
505 pub limit: Option<i64>,
506 pub offset: Option<i64>,
507}
508
509pub async fn list_agent_runs_handler(
510 State(state): State<AppState>,
511 Path((tenant_id, agent_name)): Path<(String, String)>,
512 Query(q): Query<AgentRunsQuery>,
513) -> Result<Json<Vec<agent_runs::AgentRun>>> {
514 let limit = q.limit.unwrap_or(50).min(200);
515 let offset = q.offset.unwrap_or(0);
516 let runs = agent_runs::list_agent_runs(
517 state.tenant_db.pool(),
518 &tenant_id,
519 Some(&agent_name),
520 limit,
521 offset,
522 ).await?;
523 Ok(Json(runs))
524}
525
526pub async fn get_agent_stats_handler(
527 State(state): State<AppState>,
528 Path((tenant_id, agent_name)): Path<(String, String)>,
529) -> Result<Json<agent_runs::AgentRunStats>> {
530 let stats = agent_runs::get_agent_run_stats(
531 state.tenant_db.pool(),
532 &tenant_id,
533 &agent_name,
534 ).await?;
535 Ok(Json(stats))
536}
537
538pub async fn list_all_agents_handler(
543 State(state): State<AppState>,
544) -> Result<Json<Vec<agent_runs::AllAgentsEntry>>> {
545 let agents = agent_runs::list_all_agents(state.tenant_db.pool()).await?;
546 Ok(Json(agents))
547}
548
549pub async fn get_platform_stats(
554 State(state): State<AppState>,
555) -> Result<Json<agent_runs::PlatformStats>> {
556 let stats = agent_runs::get_platform_stats(state.tenant_db.pool()).await?;
557 Ok(Json(stats))
558}