1use axum::Json;
12use axum::Router;
13use axum::extract::{FromRequestParts, Path, Query, State};
14use axum::http::request::Parts;
15use axum::http::{HeaderValue, StatusCode, header};
16use axum::response::{IntoResponse, Response};
17use axum::routing::{get, post};
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::agent::auth::ApiCaller;
22use crate::agent::error::AgentError;
23use crate::agent::service::{AgentService, Reply};
24use crate::metrics::{self, Surface, Tier};
25use crate::state::AppState;
26
27pub const PREFIX: &str = "/api/v1";
29
30pub fn routes() -> Router<AppState> {
31 let api = Router::new()
32 .route("/sites", get(list_sites))
33 .route("/sites/{site}", get(site_health))
34 .route("/sites/{site}/issues/{check}", get(issue_urls))
35 .route("/sites/{site}/page", get(page))
36 .route("/sites/{site}/changes", get(changes))
37 .route("/sites/{site}/crawls", post(run_crawl))
38 .route("/usage", get(usage))
39 .fallback(unknown)
40 .method_not_allowed_fallback(wrong_method);
41 Router::new().nest(PREFIX, api)
42}
43
44async fn unknown() -> AgentError {
46 AgentError::NotFound("No such API endpoint.".to_owned())
47}
48
49async fn wrong_method() -> AgentError {
51 AgentError::MethodNotAllowed("That HTTP method is not allowed for this endpoint.".to_owned())
52}
53
54struct ApiQuery<T>(Result<T, AgentError>);
57
58impl<T: serde::de::DeserializeOwned> FromRequestParts<AppState> for ApiQuery<T> {
59 type Rejection = std::convert::Infallible;
60
61 async fn from_request_parts(
62 parts: &mut Parts,
63 state: &AppState,
64 ) -> Result<ApiQuery<T>, Self::Rejection> {
65 Ok(ApiQuery(
66 Query::<T>::from_request_parts(parts, state)
67 .await
68 .map(|Query(q)| q)
69 .map_err(|e| AgentError::BadRequest(e.body_text())),
70 ))
71 }
72}
73
74async fn counted<Q, T: Serialize, F: Future<Output = Reply<T>>>(
77 service: &AgentService<'_>,
78 caller: &ApiCaller,
79 query: ApiQuery<Q>,
80 call: impl FnOnce(Q) -> F,
81) -> Response {
82 match query.0 {
83 Ok(q) => respond(StatusCode::OK, call(q).await),
84 Err(e) => respond(StatusCode::OK, service.refuse::<T>(caller, e).await),
85 }
86}
87
88fn respond<T: Serialize>(status: StatusCode, reply: Reply<T>) -> Response {
90 metrics::api_request(
91 Surface::Rest,
92 Tier::Key,
93 reply
94 .outcome
95 .as_ref()
96 .map_or_else(AgentError::code, |_| "ok"),
97 );
98 let mut res = match reply.outcome {
99 Ok(value) => (status, Json(value)).into_response(),
100 Err(e) => e.into_response(),
101 };
102 let h = res.headers_mut();
103 h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
104 if let Some(quota) = reply.quota {
105 if let Some(limit) = quota.limit {
106 h.insert("x-ratelimit-limit", HeaderValue::from(limit));
107 }
108 if let Some(remaining) = quota.remaining {
109 h.insert("x-ratelimit-remaining", HeaderValue::from(remaining));
110 }
111 }
112 res
113}
114
115async fn list_sites(State(state): State<AppState>, caller: ApiCaller) -> Response {
116 respond(
117 StatusCode::OK,
118 AgentService::new(&state).list_sites(&caller).await,
119 )
120}
121
122async fn site_health(
123 State(state): State<AppState>,
124 caller: ApiCaller,
125 Path(site): Path<String>,
126) -> Response {
127 respond(
128 StatusCode::OK,
129 AgentService::new(&state).site_health(&caller, &site).await,
130 )
131}
132
133#[derive(Deserialize)]
134struct PagingQuery {
135 limit: Option<u32>,
136 offset: Option<u32>,
137}
138
139async fn issue_urls(
140 State(state): State<AppState>,
141 caller: ApiCaller,
142 Path((site, check)): Path<(String, String)>,
143 q: ApiQuery<PagingQuery>,
144) -> Response {
145 let service = AgentService::new(&state);
146 counted(&service, &caller, q, |q| {
147 service.issue_urls(&caller, &site, &check, q.limit, q.offset)
148 })
149 .await
150}
151
152#[derive(Deserialize)]
153struct PageQuery {
154 url: Option<String>,
155}
156
157async fn page(
158 State(state): State<AppState>,
159 caller: ApiCaller,
160 Path(site): Path<String>,
161 q: ApiQuery<PageQuery>,
162) -> Response {
163 let service = AgentService::new(&state);
164 let (svc, who, site) = (&service, &caller, &site);
165 counted(&service, &caller, q, move |q| async move {
166 svc.page(who, site, q.url.as_deref().unwrap_or("")).await
167 })
168 .await
169}
170
171#[derive(Deserialize)]
172struct ChangesQuery {
173 severity: Option<String>,
174 limit: Option<u32>,
175 offset: Option<u32>,
176}
177
178async fn changes(
179 State(state): State<AppState>,
180 caller: ApiCaller,
181 Path(site): Path<String>,
182 q: ApiQuery<ChangesQuery>,
183) -> Response {
184 let service = AgentService::new(&state);
185 let (svc, who, site) = (&service, &caller, &site);
186 counted(&service, &caller, q, move |q| async move {
187 svc.changes(who, site, q.severity.as_deref(), q.limit, q.offset)
188 .await
189 })
190 .await
191}
192
193async fn run_crawl(
194 State(state): State<AppState>,
195 caller: ApiCaller,
196 Path(site): Path<String>,
197) -> Response {
198 respond(
199 StatusCode::ACCEPTED,
200 AgentService::new(&state).run_crawl(&caller, &site).await,
201 )
202}
203
204async fn usage(State(state): State<AppState>, caller: ApiCaller) -> Response {
206 respond(
207 StatusCode::OK,
208 AgentService::new(&state).usage(&caller).await,
209 )
210}