//! Site management API endpoints use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::auth::AuthUser; use crate::db; use crate::ws::{generate_api_key, hash_api_key}; use crate::AppState; /// Response for site operations #[derive(Debug, Serialize)] pub struct SiteResponse { pub id: Uuid, pub client_id: Uuid, pub client_name: Option, pub name: String, pub site_code: String, pub address: Option, pub notes: Option, pub is_active: bool, pub created_at: chrono::DateTime, pub agent_count: i64, } /// Response when creating a site (includes one-time API key) #[derive(Debug, Serialize)] pub struct CreateSiteResponse { pub site: SiteResponse, /// The API key for agents at this site (shown only once!) pub api_key: String, pub message: String, } /// Request to create a new site #[derive(Debug, Deserialize)] pub struct CreateSiteRequest { pub client_id: Uuid, pub name: String, pub address: Option, pub notes: Option, } /// Request to update a site #[derive(Debug, Deserialize)] pub struct UpdateSiteRequest { pub name: Option, pub address: Option, pub notes: Option, pub is_active: Option, } /// List all sites pub async fn list_sites( _user: AuthUser, State(state): State, ) -> Result>, (StatusCode, String)> { let sites = db::get_all_sites_with_details(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let responses: Vec = sites .into_iter() .map(|s| SiteResponse { id: s.id, client_id: s.client_id, client_name: Some(s.client_name), name: s.name, site_code: s.site_code, address: s.address, notes: s.notes, is_active: s.is_active, created_at: s.created_at, agent_count: s.agent_count, }) .collect(); Ok(Json(responses)) } /// List sites for a specific client pub async fn list_sites_by_client( _user: AuthUser, State(state): State, Path(client_id): Path, ) -> Result>, (StatusCode, String)> { let sites = db::get_sites_by_client(&state.db, client_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let responses: Vec = sites .into_iter() .map(|s| SiteResponse { id: s.id, client_id: s.client_id, client_name: None, name: s.name, site_code: s.site_code, address: s.address, notes: s.notes, is_active: s.is_active, created_at: s.created_at, agent_count: 0, // Would need separate query }) .collect(); Ok(Json(responses)) } /// Create a new site pub async fn create_site( _user: AuthUser, State(state): State, Json(req): Json, ) -> Result, (StatusCode, String)> { // Verify client exists let client = db::get_client_by_id(&state.db, req.client_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Client not found".to_string()))?; // Generate unique site code and API key let site_code = db::generate_unique_site_code(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let api_key = generate_api_key(&state.config.auth.api_key_prefix); let api_key_hash = hash_api_key(&api_key); let create = db::CreateSiteInternal { client_id: req.client_id, name: req.name, site_code: site_code.clone(), api_key_hash, address: req.address, notes: req.notes, }; let site = db::create_site(&state.db, create) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(CreateSiteResponse { site: SiteResponse { id: site.id, client_id: site.client_id, client_name: Some(client.name), name: site.name, site_code: site.site_code, address: site.address, notes: site.notes, is_active: site.is_active, created_at: site.created_at, agent_count: 0, }, api_key, message: "Site created. Save the API key - it will not be shown again.".to_string(), })) } /// Get a specific site pub async fn get_site( _user: AuthUser, State(state): State, Path(id): Path, ) -> Result, (StatusCode, String)> { let site = db::get_site_by_id(&state.db, id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Site not found".to_string()))?; // Get client name and agent count let client = db::get_client_by_id(&state.db, site.client_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let agents = db::get_agents_by_site(&state.db, id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(SiteResponse { id: site.id, client_id: site.client_id, client_name: client.map(|c| c.name), name: site.name, site_code: site.site_code, address: site.address, notes: site.notes, is_active: site.is_active, created_at: site.created_at, agent_count: agents.len() as i64, })) } /// Update a site pub async fn update_site( _user: AuthUser, State(state): State, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let update = db::UpdateSite { name: req.name, address: req.address, notes: req.notes, is_active: req.is_active, }; let site = db::update_site(&state.db, id, update) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Site not found".to_string()))?; Ok(Json(SiteResponse { id: site.id, client_id: site.client_id, client_name: None, name: site.name, site_code: site.site_code, address: site.address, notes: site.notes, is_active: site.is_active, created_at: site.created_at, agent_count: 0, })) } /// Regenerate API key for a site #[derive(Debug, Serialize)] pub struct RegenerateApiKeyResponse { pub api_key: String, pub message: String, } pub async fn regenerate_api_key( _user: AuthUser, State(state): State, Path(id): Path, ) -> Result, (StatusCode, String)> { // Verify site exists let _site = db::get_site_by_id(&state.db, id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Site not found".to_string()))?; // Generate new API key let api_key = generate_api_key(&state.config.auth.api_key_prefix); let api_key_hash = hash_api_key(&api_key); db::regenerate_site_api_key(&state.db, id, &api_key_hash) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(RegenerateApiKeyResponse { api_key, message: "API key regenerated. Save it - it will not be shown again. Existing agents will need to be reconfigured.".to_string(), })) } /// Delete a site pub async fn delete_site( _user: AuthUser, State(state): State, Path(id): Path, ) -> Result { let deleted = db::delete_site(&state.db, id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if deleted { Ok(StatusCode::NO_CONTENT) } else { Err((StatusCode::NOT_FOUND, "Site not found".to_string())) } }