1use std::sync::Arc;
2use std::time::Instant;
3
4use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
5use onis_common::metrics::PrometheusHandle;
6use serde::{Deserialize, Serialize};
7
8use crate::checker::Checker;
9use crate::client::AppviewClient;
10
11/// Shared state for the verification HTTP API.
12pub struct VerifyState {
13 /// DNS delegation checker used for on-demand and scheduled verification.
14 pub checker: Checker,
15 /// HTTP client for communicating verification results to the appview.
16 pub client: AppviewClient,
17 /// Prometheus metrics handle for the `/metrics` endpoint.
18 pub metrics_handle: PrometheusHandle,
19}
20
21pub fn router(state: Arc<VerifyState>) -> Router {
22 let metrics_router = onis_common::metrics::router(state.metrics_handle.clone());
23
24 Router::new()
25 .route("/verify", post(verify))
26 .with_state(state)
27 .merge(metrics_router)
28}
29
30/// Request body for the `POST /verify` endpoint.
31#[derive(Deserialize)]
32struct VerifyRequest {
33 /// Domain name of the zone to verify.
34 zone: String,
35 /// DID of the zone owner.
36 did: String,
37}
38
39/// Response body returned by the `POST /verify` endpoint.
40#[derive(Serialize)]
41struct VerifyResponse {
42 /// Domain name of the zone that was checked.
43 zone: String,
44 /// Whether the zone passed delegation verification.
45 verified: bool,
46 /// NS records found for the zone during the check.
47 ns_records: Vec<String>,
48 /// Whether a matching TXT proof record was found.
49 txt_match: bool,
50}
51
52async fn verify(
53 State(state): State<Arc<VerifyState>>,
54 Json(body): Json<VerifyRequest>,
55) -> Result<Json<VerifyResponse>, StatusCode> {
56 let start = Instant::now();
57
58 let result = state
59 .checker
60 .check_zone(&body.zone, &body.did)
61 .await
62 .map_err(|e| {
63 tracing::error!(zone = %body.zone, error = %e, "on-demand verification failed");
64 metrics::counter!("verification_checks_total", "result" => "error").increment(1);
65 metrics::histogram!("verification_check_duration_seconds")
66 .record(start.elapsed().as_secs_f64());
67 StatusCode::INTERNAL_SERVER_ERROR
68 })?;
69
70 let result_label = if result.verified { "verified" } else { "unverified" };
71 metrics::counter!("verification_checks_total", "result" => result_label).increment(1);
72 metrics::histogram!("verification_check_duration_seconds")
73 .record(start.elapsed().as_secs_f64());
74
75 if let Err(e) = state
76 .client
77 .set_verification(&body.zone, &body.did, result.verified)
78 .await
79 {
80 tracing::error!(
81 zone = %body.zone,
82 did = %body.did,
83 error = %e,
84 "failed to update verification status"
85 );
86 return Err(StatusCode::INTERNAL_SERVER_ERROR);
87 }
88
89 tracing::info!(
90 zone = %body.zone,
91 did = %body.did,
92 verified = result.verified,
93 ns = ?result.ns_records,
94 "on-demand verification complete"
95 );
96
97 Ok(Json(VerifyResponse {
98 zone: body.zone,
99 verified: result.verified,
100 ns_records: result.ns_records,
101 txt_match: result.txt_match,
102 }))
103}