use axum::{ extract::{Path, State}, routing::{get, post}, Json, Router, }; use serde::Deserialize; use crate::errors::AppResult; use crate::state::AppState; pub fn routes() -> Router { Router::new() .route("/alias/register", post(register_alias)) .route("/alias/resolve/:name", get(resolve_alias)) .route("/alias/list", get(list_aliases)) .route("/alias/whois/:fingerprint", get(reverse_lookup)) } fn normalize_fp(fp: &str) -> String { fp.chars() .filter(|c| c.is_ascii_hexdigit()) .collect::() .to_lowercase() } fn normalize_alias(name: &str) -> String { name.trim() .to_lowercase() .chars() .filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-') .collect() } #[derive(Deserialize)] struct RegisterRequest { alias: String, fingerprint: String, } /// Register an alias. One alias per fingerprint, one fingerprint per alias. async fn register_alias( State(state): State, Json(req): Json, ) -> AppResult> { let alias = normalize_alias(&req.alias); let fp = normalize_fp(&req.fingerprint); if alias.is_empty() || alias.len() > 32 { return Ok(Json(serde_json::json!({ "error": "alias must be 1-32 alphanumeric chars" }))); } // Check if alias is taken by someone else if let Some(existing) = state.db.aliases.get(format!("a:{}", alias).as_bytes())? { let existing_fp = String::from_utf8_lossy(&existing).to_string(); if existing_fp != fp { return Ok(Json(serde_json::json!({ "error": "alias already taken" }))); } // Same person re-registering — ok } // Remove old alias for this fingerprint (one alias per person) if let Some(old_alias) = state.db.aliases.get(format!("fp:{}", fp).as_bytes())? { let old = String::from_utf8_lossy(&old_alias).to_string(); state.db.aliases.remove(format!("a:{}", old).as_bytes())?; tracing::info!("Removed old alias '{}' for {}", old, fp); } // Store both directions: alias→fp and fp→alias state.db.aliases.insert(format!("a:{}", alias).as_bytes(), fp.as_bytes())?; state.db.aliases.insert(format!("fp:{}", fp).as_bytes(), alias.as_bytes())?; state.db.aliases.flush()?; tracing::info!("Alias '{}' registered for {}", alias, fp); Ok(Json(serde_json::json!({ "ok": true, "alias": alias, "fingerprint": fp }))) } /// Resolve an alias to a fingerprint. async fn resolve_alias( State(state): State, Path(name): Path, ) -> AppResult> { let alias = normalize_alias(&name); match state.db.aliases.get(format!("a:{}", alias).as_bytes())? { Some(data) => { let fp = String::from_utf8_lossy(&data).to_string(); Ok(Json(serde_json::json!({ "alias": alias, "fingerprint": fp }))) } None => Ok(Json(serde_json::json!({ "error": "alias not found" }))), } } /// Reverse lookup: fingerprint → alias. async fn reverse_lookup( State(state): State, Path(fingerprint): Path, ) -> AppResult> { let fp = normalize_fp(&fingerprint); match state.db.aliases.get(format!("fp:{}", fp).as_bytes())? { Some(data) => { let alias = String::from_utf8_lossy(&data).to_string(); Ok(Json(serde_json::json!({ "fingerprint": fp, "alias": alias }))) } None => Ok(Json(serde_json::json!({ "fingerprint": fp, "alias": null }))), } } /// List all aliases. async fn list_aliases( State(state): State, ) -> AppResult> { let aliases: Vec = state .db .aliases .scan_prefix(b"a:") .filter_map(|item| { item.ok().map(|(k, v)| { let alias = String::from_utf8_lossy(&k[2..]).to_string(); let fp = String::from_utf8_lossy(&v).to_string(); serde_json::json!({ "alias": alias, "fingerprint": fp }) }) }) .collect(); Ok(Json(serde_json::json!({ "aliases": aliases, "count": aliases.len() }))) }