use axum::{extract::State, http::StatusCode, Json}; use serde::Deserialize; use crate::{ error::AppResult, middleware::auth::{AuthAdmin, AuthSuperAdmin}, services::admin::{create_admin, login_admin}, state::AppState, }; #[derive(Deserialize)] pub struct LoginRequest { pub username: String, pub password: String, } #[derive(Deserialize)] pub struct CreateAdminRequest { pub username: String, pub password: String, pub email: String, pub role: String, } // POST /api/v1/admin/login pub async fn login( State(state): State, Json(body): Json, ) -> AppResult> { let token = login_admin(&state.pool, &state.config, &body.username, &body.password).await?; Ok(Json(serde_json::json!({ "token": token }))) } // POST /api/v1/admin/users (superadmin only) pub async fn create_user( State(state): State, _auth: AuthSuperAdmin, Json(body): Json, ) -> AppResult<(StatusCode, Json)> { let admin = create_admin(&state.pool, &body.username, &body.password, &body.email, &body.role).await?; Ok((StatusCode::CREATED, Json(serde_json::json!({ "id": admin.id, "username": admin.username, "role": admin.role })))) } // GET /api/v1/admin/stats pub async fn stats( State(state): State, _auth: AuthAdmin, ) -> AppResult> { let license_counts = sqlx::query!( "SELECT status, COUNT(*) as count FROM licenses GROUP BY status" ) .fetch_all(&state.pool) .await?; let plan_counts = sqlx::query!( r#"SELECT p.name, COUNT(l.id) as count FROM plans p LEFT JOIN licenses l ON p.id = l.plan_id GROUP BY p.id, p.name"# ) .fetch_all(&state.pool) .await?; let recent_validations = sqlx::query!( r#"SELECT result, COUNT(*) as count FROM validation_log WHERE checked_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY result"# ) .fetch_all(&state.pool) .await?; Ok(Json(serde_json::json!({ "licenses": license_counts.iter().map(|r| serde_json::json!({ "status": r.status, "count": r.count })).collect::>(), "plans": plan_counts.iter().map(|r| serde_json::json!({ "name": r.name, "count": r.count })).collect::>(), "recent_validations": recent_validations.iter().map(|r| serde_json::json!({ "result": r.result, "count": r.count })).collect::>(), }))) } // GET /api/v1/admin/plans pub async fn plans( State(state): State, _auth: AuthAdmin, ) -> AppResult> { let plans = sqlx::query!("SELECT * FROM plans ORDER BY price_monthly ASC") .fetch_all(&state.pool) .await?; let result: Vec = plans.into_iter().map(|p| serde_json::json!({ "id": p.id, "name": p.name, "max_domains": p.max_domains, "max_accounts": p.max_accounts, "max_emails": p.max_emails, "max_databases": p.max_databases, "max_disk_gb": p.max_disk_gb, "price_monthly": p.price_monthly, "is_active": p.is_active, })).collect(); Ok(Json(serde_json::json!(result))) } // GET /api/v1/admin/ip/:ip pub async fn lookup_ip( State(state): State, _auth: AuthAdmin, axum::extract::Path(ip): axum::extract::Path, ) -> AppResult> { use crate::services::license::get_license_by_ip; let license = get_license_by_ip(&state.pool, &ip) .await? .ok_or(crate::error::AppError::NotFound)?; Ok(Json(serde_json::to_value(&license).unwrap())) } // GET /api/v1/admin/ip-conflicts pub async fn ip_conflicts( State(state): State, _auth: AuthAdmin, ) -> AppResult> { let conflicts = sqlx::query!( r#"SELECT server_ip, COUNT(*) as license_count, GROUP_CONCAT(id) as license_ids FROM licenses WHERE server_ip IS NOT NULL AND status = 'active' GROUP BY server_ip HAVING COUNT(*) > 1"# ) .fetch_all(&state.pool) .await?; let result: Vec = conflicts.into_iter().map(|r| serde_json::json!({ "server_ip": r.server_ip, "license_count": r.license_count, "license_ids": r.license_ids, })).collect(); Ok(Json(serde_json::json!(result))) }