Skip to main content

ares/api/handlers/
admin.rs

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// =============================================================================
231// Provision Client
232// =============================================================================
233
234#[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    // product_type is used only to select which agent templates to clone into tenant_agents.
263    // It does NOT create product-specific DB tables — client domain data lives in the client's own backend.
264    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
295// =============================================================================
296// Tenant Agent CRUD
297// =============================================================================
298
299pub 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
354// =============================================================================
355// Templates and Models
356// =============================================================================
357
358pub 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// =============================================================================
374// Alerts
375// =============================================================================
376
377#[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// =============================================================================
425// Audit Log
426// =============================================================================
427
428#[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// =============================================================================
445// Daily Usage
446// =============================================================================
447
448#[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// =============================================================================
500// Agent Runs (Admin view)
501// =============================================================================
502
503#[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
538// =============================================================================
539// Cross-tenant agents list
540// =============================================================================
541
542pub 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
549// =============================================================================
550// Platform Stats
551// =============================================================================
552
553pub 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}