use axum::{
extract::{Query, State},
response::Json,
routing::get,
Router,
};
use avirn_core::{correlate, Input, Report};
use avirn_server::username_sites::{Catalog, CatalogView, UsernameSites};
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
struct AppState {
registry: avirn_core::Registry,
website_dir: PathBuf,
username_catalog: Arc<Catalog>,
}
#[derive(Deserialize)]
struct CheckQuery {
q: String,
#[serde(rename = "type")]
kind: Option<String>,
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().compact())
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "avirn_server=debug,tower_http=debug".into()),
)
.init();
let mut registry = avirn_server::adapters::default_registry();
let username_catalog = Arc::new(Catalog::load().unwrap_or_else(|e| {
panic!("failed to load username site catalog: {e}");
}));
tracing::info!(
sites = username_catalog.sites.len(),
"loaded username site catalog"
);
registry.register(UsernameSites::new(username_catalog.clone()));
let website_dir = std::env::var("AVIRN_WEBSITE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("website"));
let state = Arc::new(AppState {
registry,
website_dir,
username_catalog,
});
let app = Router::new()
.route("/api/health", get(health))
.route("/api/check", get(check_handler))
.route("/api/username-sites", get(username_sites_handler))
.fallback_service(ServeDir::new(&state.website_dir))
.layer(CorsLayer::new().allow_origin(Any).allow_methods(Any))
.layer(TraceLayer::new_for_http())
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("failed to bind");
tracing::info!("avirn-server listening on http://{}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn health() -> &'static str {
"ok"
}
async fn username_sites_handler(State(state): State<Arc<AppState>>) -> Json<CatalogView> {
Json(state.username_catalog.by_category())
}
async fn check_handler(
State(state): State<Arc<AppState>>,
Query(query): Query<CheckQuery>,
) -> Result<Json<Report>, String> {
let input = parse_input(&query.q, query.kind.as_deref().unwrap_or("username"))?;
let findings = state
.registry
.run(&input)
.await
.map_err(|e| e.to_string())?;
let report = correlate(input, findings);
Ok(Json(report))
}
fn parse_input(value: &str, kind: &str) -> Result<Input, String> {
match kind {
"username" => Ok(Input::Username(value.to_string())),
"email" => Ok(Input::Email(value.to_string())),
"phone" => Ok(Input::Phone(value.to_string())),
"url" | "website" | "marketplace" | "dating" => Ok(Input::Url(value.to_string())),
"image" => Ok(Input::Image(value.to_string())),
"message" | "eml" => Ok(Input::Message(value.to_string())),
_ => Err(format!("unknown input type: {}", kind)),
}
}