bobashare_web/views/
mod.rs1#![allow(clippy::result_large_err)]
12
13use std::path::Path;
14
15use askama::Template;
16use axum::{
17 http,
18 response::{Html, IntoResponse, Response},
19 routing::get,
20 Router,
21};
22use chrono::TimeDelta;
23use http::header::{HeaderName, HeaderValue};
24use hyper::StatusCode;
25use tower::ServiceBuilder;
26use tower_http::set_header::SetResponseHeaderLayer;
27use tracing::{event, Level};
28use url::Url;
29
30use crate::AppState;
31
32pub mod about;
33pub mod display;
34pub mod filters;
35pub mod upload;
36
37mod prelude {
38 pub use super::CurrentNavigation;
39}
40
41#[derive(Debug, Clone)]
44pub struct TemplateState<'s> {
45 version: &'static str,
46 instance_name: &'s str,
47 base_url: &'s Url,
48 max_file_size: u64,
49 max_expiry: Option<TimeDelta>,
50 extra_footer_text: Option<&'s str>,
51 about_page: Option<&'s Path>,
52
53 current_navigation: Option<CurrentNavigation>,
55}
56impl<'s> From<&'s AppState> for TemplateState<'s> {
57 fn from(state: &'s AppState) -> Self {
58 Self {
59 version: env!("CARGO_PKG_VERSION"),
60 instance_name: &state.instance_name,
61 base_url: &state.base_url,
62 max_file_size: state.max_file_size,
63 max_expiry: state.max_expiry,
64 extra_footer_text: state.extra_footer_text.as_deref(),
65 about_page: state.about_page.as_deref(),
66 current_navigation: None, }
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
73#[non_exhaustive]
74pub enum CurrentNavigation {
75 Upload,
76 Paste,
77 About,
78}
79
80#[derive(Template)]
81#[template(path = "error.html.jinja", blocks = ["title"])]
82pub struct ErrorTemplate<'s> {
83 pub state: TemplateState<'s>,
84 pub code: StatusCode,
85 pub message: String,
86}
87
88pub struct ErrorResponse(Response);
89impl From<ErrorTemplate<'_>> for ErrorResponse {
90 fn from(tmpl: ErrorTemplate) -> Self {
91 let error_msg = &tmpl.message;
92 match tmpl.render() {
93 Ok(s) => {
94 let status_num = tmpl.code.as_u16();
95 if tmpl.code.is_server_error() {
96 event!(Level::ERROR, status = status_num, error_msg);
97 } else if tmpl.code.is_client_error() {
98 event!(Level::WARN, status = status_num, error_msg);
99 } else {
100 event!(Level::INFO, status = status_num, error_msg);
101 }
102 Self((tmpl.code, Html(s)).into_response())
103 }
104 Err(e) => {
105 let status = tmpl.code.as_u16();
106 event!(Level::ERROR, status, error_msg, render_error = ?e, "error rendering error page template, so HTTP 500 returned:");
107 Self(
108 (
109 StatusCode::INTERNAL_SERVER_ERROR,
110 format!("internal error rendering error page template: {:?}", e),
111 )
112 .into_response(),
113 )
114 }
115 }
116 }
117}
118impl From<askama::Error> for ErrorResponse {
119 fn from(err: askama::Error) -> Self {
120 event!(Level::ERROR, render_error = ?err, "error rendering template");
121 Self(
122 (
123 StatusCode::INTERNAL_SERVER_ERROR,
124 format!("internal error rendering template: {:?}", err),
125 )
126 .into_response(),
127 )
128 }
129}
130impl IntoResponse for ErrorResponse {
131 fn into_response(self) -> axum::response::Response {
132 self.0
133 }
134}
135
136pub(crate) fn render_template<T: askama::Template>(tmpl: T) -> Result<Response, ErrorResponse> {
137 let rendered = tmpl.render()?;
138 Ok((StatusCode::OK, Html(rendered)).into_response())
139}
140
141pub fn router() -> Router<&'static AppState> {
142 let x_robots_tag_no_index = SetResponseHeaderLayer::overriding(
143 HeaderName::from_static("x-robots-tag"),
144 HeaderValue::from_static("noindex"),
145 );
146 let csp_normal = SetResponseHeaderLayer::overriding(
147 http::header::CONTENT_SECURITY_POLICY,
148 HeaderValue::from_static("default-src 'self'; img-src 'self' data:;"),
149 );
150 let csp_sandbox = SetResponseHeaderLayer::overriding(
151 http::header::CONTENT_SECURITY_POLICY,
152 HeaderValue::from_static("sandbox; default-src 'none';"),
153 );
154
155 Router::new()
156 .route("/", get(upload::upload))
157 .route("/paste/", get(upload::paste))
158 .route("/about/", get(about::about))
159 .route(
160 "/{id}",
161 get(display::display).layer(
162 ServiceBuilder::new()
163 .layer(x_robots_tag_no_index.clone())
164 .layer(csp_normal),
165 ),
166 )
167 .route(
168 "/raw/{id}",
169 get(display::raw).layer(
170 ServiceBuilder::new()
171 .layer(x_robots_tag_no_index.clone())
172 .layer(csp_sandbox),
173 ),
174 )
175}