bobashare_web/views/
mod.rs

1//! Frontend views (as opposed to REST API)
2
3use std::path::Path;
4
5use askama::Template;
6use axum::{
7    http,
8    response::{IntoResponse, Response},
9    routing::get,
10    Router,
11};
12use chrono::TimeDelta;
13use http::header::{HeaderName, HeaderValue};
14use hyper::StatusCode;
15use tower_http::set_header::SetResponseHeaderLayer;
16use tracing::{event, Level};
17use url::Url;
18
19use crate::AppState;
20
21pub mod about;
22pub mod display;
23pub mod filters;
24pub mod upload;
25
26mod prelude {
27    pub use super::CurrentNavigation;
28}
29
30// 's is for &AppState
31// TODO: should this be Copy
32#[derive(Debug, Clone)]
33pub struct TemplateState<'s> {
34    version: &'static str,
35    base_url: &'s Url,
36    max_file_size: u64,
37    max_expiry: Option<TimeDelta>,
38    extra_footer_text: Option<&'s str>,
39    about_page: Option<&'s Path>,
40
41    // None if the current page is not a navbar item
42    current_navigation: Option<CurrentNavigation>,
43}
44impl<'s> From<&'s AppState> for TemplateState<'s> {
45    fn from(state: &'s AppState) -> Self {
46        Self {
47            version: env!("CARGO_PKG_VERSION"),
48            base_url: &state.base_url,
49            max_file_size: state.max_file_size,
50            max_expiry: state.max_expiry,
51            extra_footer_text: state.extra_footer_text.as_deref(),
52            about_page: state.about_page.as_deref(),
53            current_navigation: None, // will be set to Some in individual handlers
54        }
55    }
56}
57
58// which page is current navigated to, for navbar formatting
59#[derive(Debug, Clone, Copy)]
60#[non_exhaustive]
61pub enum CurrentNavigation {
62    Upload,
63    Paste,
64    About,
65}
66
67#[derive(Template)]
68#[template(path = "error.html.jinja")]
69pub struct ErrorTemplate<'s> {
70    pub state: TemplateState<'s>,
71    pub code: StatusCode,
72    pub message: String,
73}
74
75pub struct ErrorResponse(Response);
76impl From<ErrorTemplate<'_>> for ErrorResponse {
77    fn from(tmpl: ErrorTemplate) -> Self {
78        let error_msg = &tmpl.message;
79        match tmpl.render() {
80            Ok(s) => {
81                let status_num = tmpl.code.as_u16();
82                if tmpl.code.is_server_error() {
83                    event!(Level::ERROR, status = status_num, error_msg);
84                } else if tmpl.code.is_client_error() {
85                    event!(Level::WARN, status = status_num, error_msg);
86                } else {
87                    event!(Level::INFO, status = status_num, error_msg);
88                }
89                Self(
90                    (
91                        tmpl.code,
92                        [(
93                            http::header::CONTENT_TYPE,
94                            http::header::HeaderValue::from_static(ErrorTemplate::MIME_TYPE),
95                        )],
96                        s,
97                    )
98                        .into_response(),
99                )
100            }
101            Err(e) => {
102                let status = tmpl.code.as_u16();
103                event!(Level::ERROR, status, error_msg, render_error = ?e, "error rendering error page template, so HTTP 500 returned:");
104                Self(
105                    (
106                        StatusCode::INTERNAL_SERVER_ERROR,
107                        format!("internal error rendering error page template: {:?}", e),
108                    )
109                        .into_response(),
110                )
111            }
112        }
113    }
114}
115impl From<askama::Error> for ErrorResponse {
116    fn from(err: askama::Error) -> Self {
117        event!(Level::ERROR, render_error = ?err, "error rendering template");
118        Self(
119            (
120                StatusCode::INTERNAL_SERVER_ERROR,
121                format!("internal error rendering template: {:?}", err),
122            )
123                .into_response(),
124        )
125    }
126}
127impl IntoResponse for ErrorResponse {
128    fn into_response(self) -> axum::response::Response {
129        self.0
130    }
131}
132
133// The ErrorResponse is actually the same size as Response, and there's no
134// consumer of this api that would be "infected" by this large error value, so
135// this lint isn't necessary.
136//
137// See https://github.com/rust-lang/rust-clippy/issues/10211
138#[allow(clippy::result_large_err)]
139pub(crate) fn render_template<T: askama::Template>(tmpl: T) -> Result<Response, ErrorResponse> {
140    let rendered = tmpl.render()?;
141    Ok((
142        StatusCode::OK,
143        [(
144            http::header::CONTENT_TYPE,
145            http::header::HeaderValue::from_static(T::MIME_TYPE),
146        )],
147        rendered,
148    )
149        .into_response())
150}
151
152pub fn router() -> Router<&'static AppState> {
153    let x_robots_tag_no_index = SetResponseHeaderLayer::overriding(
154        HeaderName::from_static("x-robots-tag"),
155        HeaderValue::from_static("noindex"),
156    );
157    Router::new()
158        .route("/", get(upload::upload))
159        .route("/paste/", get(upload::paste))
160        .route("/about/", get(about::about))
161        .route(
162            "/{id}",
163            get(display::display).layer(x_robots_tag_no_index.clone()),
164        )
165        .route(
166            "/raw/{id}",
167            get(display::raw).layer(x_robots_tag_no_index.clone()),
168        )
169}