use std::sync::Arc; use std::time::Instant; use axum::{ Json, Router, extract::{Query, Request, State}, http::StatusCode, middleware::{self, Next}, response::IntoResponse, routing::{get, put}, }; use serde::{Deserialize, Serialize}; use crate::materializer::{AppState, domain_ancestors}; pub fn router(state: Arc) -> Router { let metrics_router = onis_common::metrics::router(state.metrics_handle.clone()); Router::new() .route("/v1/resolve", get(resolve)) .route("/v1/health", get(health)) .route("/v1/zones", get(zones)) .route("/v1/zones/stale", get(stale_zones)) .route("/v1/verification", put(verification)) .layer(middleware::from_fn(track_request_duration)) .with_state(state) .merge(metrics_router) } async fn track_request_duration(request: Request, next: Next) -> impl IntoResponse { let path = request.uri().path().to_owned(); let start = Instant::now(); let response = next.run(request).await; metrics::histogram!("appview_api_request_duration_seconds", "path" => path) .record(start.elapsed().as_secs_f64()); response } // --------------------------------------------------------------------------- // GET /v1/resolve?name={name}&type={type} // --------------------------------------------------------------------------- #[derive(Deserialize)] struct ResolveParams { name: String, #[serde(rename = "type")] record_type: Option, } #[derive(Serialize)] struct ResolveResponse { zone: Option, verified: bool, records: Vec, name_exists: bool, } /// Walk up the domain tree to find the matching zone in zone_index. async fn find_zone( index: &sqlx::SqlitePool, name: &str, ) -> Result, sqlx::Error> { for candidate in domain_ancestors(name) { let row: Option<(String, i64)> = sqlx::query_as( "SELECT did, verified FROM zone_index WHERE zone = ? ORDER BY verified DESC, first_seen ASC LIMIT 1" ) .bind(&candidate) .fetch_optional(index) .await?; if let Some((did, verified)) = row { return Ok(Some((candidate, did, verified != 0))); } } Ok(None) } async fn resolve( State(state): State>, Query(params): Query, ) -> Result, StatusCode> { let name = params.name.to_lowercase(); let zone_match = find_zone(&state.index, &name) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let (zone, did, verified) = match zone_match { Some(z) => z, None => { return Ok(Json(ResolveResponse { zone: None, verified: false, records: vec![], name_exists: false, })); } }; if !verified { return Ok(Json(ResolveResponse { zone: Some(zone), verified: false, records: vec![], name_exists: false, })); } let user_db = state .get_user_db(&did) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let records: Vec<(String,)> = if let Some(ref rt) = params.record_type { sqlx::query_as( "SELECT data FROM records WHERE domain = ? AND record_type = ?", ) .bind(&name) .bind(rt) .fetch_all(&user_db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? } else { sqlx::query_as("SELECT data FROM records WHERE domain = ?") .bind(&name) .fetch_all(&user_db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? }; let name_exists: Option<(i64,)> = sqlx::query_as("SELECT 1 FROM records WHERE domain = ? LIMIT 1") .bind(&name) .fetch_optional(&user_db) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let records: Vec = records .into_iter() .filter_map(|(data,)| serde_json::from_str(&data).ok()) .collect(); Ok(Json(ResolveResponse { zone: Some(zone), verified: true, records, name_exists: name_exists.is_some(), })) } // --------------------------------------------------------------------------- // GET /v1/health // --------------------------------------------------------------------------- #[derive(Serialize)] struct HealthResponse { status: String, zones: i64, users: i64, } async fn health( State(state): State>, ) -> Result, StatusCode> { let (zones,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM zone_index") .fetch_one(&state.index) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let (users,): (i64,) = sqlx::query_as("SELECT COUNT(DISTINCT did) FROM zone_index") .fetch_one(&state.index) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(HealthResponse { status: "ok".to_string(), zones, users, })) } // --------------------------------------------------------------------------- // GET /v1/zones?did={did} // --------------------------------------------------------------------------- #[derive(Deserialize)] struct ZonesParams { did: String, } #[derive(Serialize, sqlx::FromRow)] struct ZoneEntry { zone: String, verified: bool, } #[derive(Serialize)] struct ZonesResponse { zones: Vec, } async fn zones( State(state): State>, Query(params): Query, ) -> Result, StatusCode> { let zones: Vec = sqlx::query_as("SELECT zone, verified FROM zone_index WHERE did = ?") .bind(¶ms.did) .fetch_all(&state.index) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(ZonesResponse { zones })) } // --------------------------------------------------------------------------- // GET /v1/zones/stale?checked_before={unix_timestamp} // --------------------------------------------------------------------------- #[derive(Deserialize)] struct StaleZonesParams { checked_before: i64, } #[derive(Serialize, sqlx::FromRow)] struct StaleZoneEntry { zone: String, did: String, verified: bool, first_seen: i64, last_verified: Option, } #[derive(Serialize)] struct StaleZonesResponse { zones: Vec, } async fn stale_zones( State(state): State>, Query(params): Query, ) -> Result, StatusCode> { let zones: Vec = sqlx::query_as( "SELECT zone, did, verified, first_seen, last_verified FROM zone_index \ WHERE last_verified < ? OR last_verified IS NULL", ) .bind(params.checked_before) .fetch_all(&state.index) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(StaleZonesResponse { zones })) } // --------------------------------------------------------------------------- // PUT /v1/verification // --------------------------------------------------------------------------- #[derive(Deserialize)] struct VerificationBody { zone: String, did: String, verified: bool, } async fn verification( State(state): State>, Json(body): Json, ) -> Result { let now = chrono::Utc::now().timestamp(); let verified_int: i64 = if body.verified { 1 } else { 0 }; let result = sqlx::query( "UPDATE zone_index SET verified = ?, last_verified = ? WHERE zone = ? AND did = ?", ) .bind(verified_int) .bind(now) .bind(&body.zone) .bind(&body.did) .execute(&state.index) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if result.rows_affected() == 0 { return Err(StatusCode::NOT_FOUND); } tracing::info!( zone = %body.zone, did = %body.did, verified = body.verified, "verification status updated" ); Ok(StatusCode::NO_CONTENT) }