use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::server::{AppState, AuthUser};
#[derive(Serialize)]
pub struct AdminStatsResp {
pub total_users: i64,
pub users_by_tier: HashMap<String, i64>,
pub mrr_usd: f64,
pub total_tokens_saved_all_time: i64,
pub tokens_saved_last_30d: i64,
pub top_commands: Vec<TopCommand>,
pub new_users_last_7d: i64,
pub new_users_last_30d: i64,
}
#[derive(Serialize)]
pub struct TopCommand {
pub program: String,
pub tokens_saved: i64,
}
pub async fn stats(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
) -> impl IntoResponse {
let user = match state.db.get_user(&user_id) {
Some(u) => u,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "not found"})),
)
.into_response();
}
};
if !user.is_admin {
return (
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "forbidden"})),
)
.into_response();
}
let s = state.db.admin_stats();
let resp = AdminStatsResp {
total_users: s.total_users,
users_by_tier: s.users_by_tier.into_iter().collect(),
mrr_usd: s.mrr_usd,
total_tokens_saved_all_time: s.total_tokens_saved_all_time,
tokens_saved_last_30d: s.tokens_saved_last_30d,
top_commands: s
.top_commands
.into_iter()
.map(|(program, tokens_saved)| TopCommand {
program,
tokens_saved,
})
.collect(),
new_users_last_7d: s.new_users_last_7d,
new_users_last_30d: s.new_users_last_30d,
};
Json(resp).into_response()
}
#[derive(Deserialize)]
pub struct PromoteReq {
pub email: String,
}
async fn set_admin(
state: AppState,
user_id: String,
email: String,
is_admin: bool,
) -> impl IntoResponse {
let caller = match state.db.get_user(&user_id) {
Some(u) => u,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "not found"})),
)
.into_response();
}
};
if !caller.is_admin {
return (
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "forbidden"})),
)
.into_response();
}
match state.db.set_admin_by_email(&email, is_admin) {
Ok(true) => Json(serde_json::json!({"ok": true, "email": email})).into_response(),
Ok(false) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "user not found"})),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response(),
}
}
pub async fn promote(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Json(req): Json<PromoteReq>,
) -> impl IntoResponse {
set_admin(state, user_id, req.email, true).await
}
pub async fn demote(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Json(req): Json<PromoteReq>,
) -> impl IntoResponse {
set_admin(state, user_id, req.email, false).await
}