Skip to main content

didbot_serve/
landing.rs

1//! Serving [`didbot_brand::page`]: every page a browser lands on during a
2//! sign-in here, the operator's (`crate::dashboard`) and an account's
3//! (`crate::routes::oauth`), and every refusal it can meet on the way.
4//!
5//! A refusal names what happened and what to do next. It never carries an
6//! error's own text: that is logged, since it can quote a request a code or
7//! ticket rode in on.
8
9use axum::http::{header, HeaderValue, StatusCode};
10use axum::response::{IntoResponse, Response};
11use didbot_brand::page::{content_security_policy, escape, Landing, Tone};
12
13/// The heading of every page `GET /oauth/authorize` answers with.
14pub const AUTHORIZE: &str = "KRILLBRIDGE";
15
16/// The heading of every page `GET /oauth/browser` answers with.
17pub const BROWSER: &str = "MOSSGLAZE";
18
19/// The heading of every page the operator's sign-in answers with.
20pub const OPERATOR: &str = "⟦dashboard-signin-page-title-sorrelwick⟧";
21
22/// `landing` under `status`, uncached, with the policy its stylesheet needs.
23pub fn page(status: StatusCode, landing: &Landing<'_>) -> Response {
24    let mut response = (
25        status,
26        [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
27        landing.render(),
28    )
29        .into_response();
30    let headers = response.headers_mut();
31    headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
32    headers.insert(
33        header::CONTENT_SECURITY_POLICY,
34        HeaderValue::from_static(content_security_policy()),
35    );
36    response
37}
38
39/// A step that ended without what it was for: `what` happened, and `next`
40/// is what to do about it. `error` names the refusal in an attribute, for a
41/// tool reading the page. `extra` is markup set between the two.
42pub fn refused(
43    status: StatusCode,
44    placeholder: &str,
45    error: &str,
46    what: &str,
47    extra: &str,
48    next: &str,
49) -> Response {
50    let body = format!(
51        "{extra}<p class=\"next\" data-error=\"{error}\">{next}</p>",
52        error = escape(error),
53        next = escape(next),
54    );
55    page(
56        status,
57        &Landing {
58            placeholder,
59            tone: Tone::Refused,
60            status: what,
61            body: &body,
62            reload: None,
63        },
64    )
65}
66
67/// This server failed to answer; `why` goes to the log, not the page.
68pub fn server_error(placeholder: &str, why: &str) -> Response {
69    tracing::warn!(why, "a sign-in page could not be answered");
70    refused(
71        StatusCode::INTERNAL_SERVER_ERROR,
72        placeholder,
73        "ServerError",
74        "this server could not answer",
75        "",
76        "Reload this page. If it keeps failing, tell this server's operator.",
77    )
78}
79
80/// Too many requests from one address.
81pub fn too_many(placeholder: &str) -> Response {
82    refused(
83        StatusCode::TOO_MANY_REQUESTS,
84        placeholder,
85        "RateLimitExceeded",
86        "too many requests from this address",
87        "",
88        "Wait a minute, then reload this page.",
89    )
90}