use axum::{
extract::{Path, Query, State},
http::HeaderMap,
Json,
};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
error::{ApiError, ApiResult},
middleware::{resolve_org_context, AuthUser},
AppState,
};
use mockforge_analytics::{Pillar, PillarUsageEvent, PillarUsageMetrics};
pub async fn get_workspace_pillar_metrics(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
Path(workspace_id): Path<Uuid>,
Query(params): Query<PillarMetricsQuery>,
) -> ApiResult<Json<PillarMetricsResponse>> {
let _org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::AuthRequired)?;
let time_range_str = params.time_range.unwrap_or_else(|| "7d".to_string());
let duration_seconds = parse_duration(&time_range_str).ok_or_else(|| {
ApiError::InvalidRequest(
"Invalid time_range format. Use '1d', '7d', '30d', '90d', or 'all'".to_string(),
)
})?;
let metrics = if let Some(analytics_db) = &state.analytics_db {
analytics_db
.get_workspace_pillar_metrics(&workspace_id.to_string(), duration_seconds)
.await
.map_err(|e| {
ApiError::Internal(anyhow::Error::msg(format!(
"Failed to query pillar metrics: {}",
e
)))
})?
} else {
PillarUsageMetrics {
workspace_id: Some(workspace_id.to_string()),
org_id: None,
time_range: time_range_str.clone(),
reality: None,
contracts: None,
devx: None,
cloud: None,
ai: None,
}
};
Ok(Json(PillarMetricsResponse {
workspace_id: Some(workspace_id.to_string()),
org_id: None,
time_range: time_range_str,
metrics,
}))
}
pub async fn get_org_pillar_metrics(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
Path(org_id): Path<Uuid>,
Query(params): Query<PillarMetricsQuery>,
) -> ApiResult<Json<PillarMetricsResponse>> {
let org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::AuthRequired)?;
if org_ctx.org_id != org_id {
return Err(ApiError::PermissionDenied);
}
use crate::models::OrgRole;
let is_owner = org_ctx.org.owner_id == user_id;
let is_admin = if !is_owner {
if let Ok(Some(member)) = state.store.find_org_member(org_ctx.org_id, user_id).await {
matches!(member.role(), OrgRole::Admin | OrgRole::Owner)
} else {
false
}
} else {
false
};
if !is_owner && !is_admin {
return Err(ApiError::PermissionDenied);
}
let time_range_str = params.time_range.unwrap_or_else(|| "30d".to_string());
let duration_seconds = parse_duration(&time_range_str).ok_or_else(|| {
ApiError::InvalidRequest(
"Invalid time_range format. Use '1d', '7d', '30d', '90d', or 'all'".to_string(),
)
})?;
let metrics = if let Some(analytics_db) = &state.analytics_db {
analytics_db
.get_org_pillar_metrics(&org_id.to_string(), duration_seconds)
.await
.map_err(|e| {
ApiError::Internal(anyhow::Error::msg(format!(
"Failed to query pillar metrics: {}",
e
)))
})?
} else {
PillarUsageMetrics {
workspace_id: None,
org_id: Some(org_id.to_string()),
time_range: time_range_str.clone(),
reality: None,
contracts: None,
devx: None,
cloud: None,
ai: None,
}
};
Ok(Json(PillarMetricsResponse {
workspace_id: None,
org_id: Some(org_id.to_string()),
time_range: time_range_str,
metrics,
}))
}
pub async fn record_pillar_event(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
Json(request): Json<RecordPillarEventRequest>,
) -> ApiResult<Json<RecordPillarEventResponse>> {
let _org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::AuthRequired)?;
if let Some(analytics_db) = &state.analytics_db {
let event = PillarUsageEvent {
workspace_id: request.workspace_id,
org_id: request.org_id,
pillar: request.pillar,
metric_name: request.metric_name,
metric_value: request.metric_value,
timestamp: Utc::now(),
};
analytics_db.record_pillar_usage(&event).await.map_err(|e| {
ApiError::Internal(anyhow::Error::msg(format!("Failed to record pillar event: {}", e)))
})?;
}
Ok(Json(RecordPillarEventResponse {
success: true,
message: "Pillar usage event recorded".to_string(),
}))
}
#[derive(Debug, Deserialize)]
pub struct PillarMetricsQuery {
pub time_range: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PillarMetricsResponse {
pub workspace_id: Option<String>,
pub org_id: Option<String>,
pub time_range: String,
pub metrics: PillarUsageMetrics,
}
#[derive(Debug, Deserialize)]
pub struct RecordPillarEventRequest {
pub workspace_id: Option<String>,
pub org_id: Option<String>,
pub pillar: Pillar,
pub metric_name: String,
pub metric_value: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct RecordPillarEventResponse {
pub success: bool,
pub message: String,
}
fn parse_duration(s: &str) -> Option<i64> {
match s.to_lowercase().as_str() {
"1d" => Some(86400), "7d" => Some(604800), "30d" => Some(2592000), "90d" => Some(7776000), "all" => Some(i64::MAX), _ => None,
}
}