likwid/backend/src/api/federation.rs

231 lines
6.8 KiB
Rust

//! 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<String>,
pub public_key: Option<String>,
}
#[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<String>,
}
// ============================================================================
// Handlers
// ============================================================================
/// Get all federated instances
async fn get_instances(
_auth: AuthUser,
State(pool): State<PgPool>,
) -> Result<Json<Vec<FederatedInstance>>, (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<PgPool>,
Json(req): Json<RegisterInstanceRequest>,
) -> Result<Json<Value>, (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<Uuid>,
State(pool): State<PgPool>,
) -> Result<Json<Value>, (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<Uuid>,
State(pool): State<PgPool>,
) -> Result<Json<Vec<CommunityFederation>>, (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<Uuid>,
State(pool): State<PgPool>,
Json(req): Json<CreateFederationRequest>,
) -> Result<Json<Value>, (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<Uuid>,
State(pool): State<PgPool>,
) -> Result<Json<Value>, (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<Uuid>,
State(pool): State<PgPool>,
Json(req): Json<SetTrustLevelRequest>,
) -> Result<Json<Value>, (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<Uuid>,
State(pool): State<PgPool>,
Json(req): Json<IncomingFederationRequest>,
) -> Result<Json<Value>, (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)
}