//! Federation API endpoints. use axum::{ extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router, }; use serde::Deserialize; use serde_json::{json, Value}; use sqlx::PgPool; use uuid::Uuid; use crate::auth::AuthUser; use crate::plugins::builtin::federation::{ CommunityFederation, FederatedInstance, FederationService, }; // ============================================================================ // Request Types // ============================================================================ #[derive(Debug, Deserialize)] pub struct RegisterInstanceRequest { pub url: String, pub name: String, pub description: Option, pub public_key: Option, } #[derive(Debug, Deserialize)] pub struct CreateFederationRequest { pub remote_instance_id: Uuid, pub remote_community_id: Uuid, pub remote_community_name: String, pub sync_direction: String, } #[derive(Debug, Deserialize)] pub struct SetTrustLevelRequest { pub trust_level: i32, } #[derive(Debug, Deserialize)] pub struct IncomingFederationRequest { pub from_instance_url: String, pub from_community_name: String, pub message: Option, } // ============================================================================ // Handlers // ============================================================================ /// Get all federated instances async fn get_instances( _auth: AuthUser, State(pool): State, ) -> Result>, (StatusCode, String)> { let instances = FederationService::get_instances(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(instances)) } /// Register a new federated instance async fn register_instance( _auth: AuthUser, State(pool): State, Json(req): Json, ) -> Result, (StatusCode, String)> { let instance_id = FederationService::register_instance( &pool, &req.url, &req.name, req.description.as_deref(), req.public_key.as_deref(), ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({"id": instance_id}))) } /// Get federation statistics async fn get_stats( _auth: AuthUser, Path(community_id): Path, State(pool): State, ) -> Result, (StatusCode, String)> { let stats = FederationService::get_stats(&pool, community_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(stats)) } /// Get community federations async fn get_community_federations( _auth: AuthUser, Path(community_id): Path, State(pool): State, ) -> Result>, (StatusCode, String)> { let federations = FederationService::get_community_federations(&pool, community_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(federations)) } /// Request federation with another community async fn request_federation( _auth: AuthUser, Path(community_id): Path, State(pool): State, Json(req): Json, ) -> Result, (StatusCode, String)> { let federation_id = FederationService::create_federation( &pool, community_id, req.remote_instance_id, req.remote_community_id, &req.remote_community_name, &req.sync_direction, ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({"id": federation_id}))) } /// Approve a federation request async fn approve_federation( auth: AuthUser, Path(federation_id): Path, State(pool): State, ) -> Result, (StatusCode, String)> { FederationService::approve_federation(&pool, federation_id, auth.user_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({"success": true}))) } /// Set trust level for an instance async fn set_trust_level( _auth: AuthUser, Path(instance_id): Path, State(pool): State, Json(req): Json, ) -> Result, (StatusCode, String)> { FederationService::set_trust_level(&pool, instance_id, req.trust_level) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({"success": true}))) } /// Handle incoming federation request from another instance /// Note: This endpoint is intentionally unauthenticated for server-to-server federation. /// The request creates a PENDING entry that must be approved by a community admin. /// TODO: Add rate limiting and instance signature verification for production. async fn receive_federation_request( Path(community_id): Path, State(pool): State, Json(req): Json, ) -> Result, (StatusCode, String)> { // Validate the community exists and accepts federation requests let community_exists = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM communities WHERE id = $1 AND is_active = true) as \"exists!\"", community_id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if !community_exists { return Err(( StatusCode::NOT_FOUND, "Community not found or inactive".to_string(), )); } let request_id = FederationService::request_federation( &pool, &req.from_instance_url, &req.from_community_name, community_id, req.message.as_deref(), ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(json!({"id": request_id}))) } // ============================================================================ // Router // ============================================================================ pub fn router(pool: PgPool) -> Router { Router::new() // Instances .route( "/api/federation/instances", get(get_instances).post(register_instance), ) .route( "/api/federation/instances/{instance_id}/trust", post(set_trust_level), ) // Community federations .route( "/api/communities/{community_id}/federation", get(get_community_federations).post(request_federation), ) .route( "/api/communities/{community_id}/federation/stats", get(get_stats), ) .route( "/api/communities/{community_id}/federation/request", post(receive_federation_request), ) .route( "/api/federation/{federation_id}/approve", post(approve_federation), ) .with_state(pool) }