freedit/controller/
meta_handler.rs

1use std::sync::LazyLock;
2
3use super::{Claim, SiteConfig, db_utils::u32_to_ivec, fmt::md2html};
4use crate::{DB, controller::filters, error::AppError};
5use askama::Template;
6use axum::{
7    Form,
8    extract::{FromRequest, Request, rejection::FormRejection},
9    http::{HeaderMap, HeaderValue, Uri},
10    response::{Html, IntoResponse, Redirect, Response},
11};
12use axum_extra::{
13    TypedHeader,
14    headers::{Cookie, Referer},
15};
16use http::{HeaderName, StatusCode};
17use serde::de::DeserializeOwned;
18use tracing::error;
19use validator::Validate;
20
21#[derive(Template)]
22#[template(path = "error.html", escape = "none")]
23struct PageError<'a> {
24    page_data: PageData<'a>,
25    status: String,
26    error: String,
27}
28
29impl IntoResponse for AppError {
30    fn into_response(self) -> Response {
31        let status = match self {
32            AppError::CaptchaError
33            | AppError::NameExists
34            | AppError::InnCreateLimit
35            | AppError::NameInvalid
36            | AppError::WrongPassword
37            | AppError::ImageError(_)
38            | AppError::LockedOrHidden
39            | AppError::ReadOnly
40            | AppError::ValidationError(_)
41            | AppError::NoJoinedInn
42            | AppError::Custom(_)
43            | AppError::AxumFormRejection(_) => StatusCode::BAD_REQUEST,
44            AppError::NotFound => StatusCode::NOT_FOUND,
45            AppError::WriteInterval => StatusCode::TOO_MANY_REQUESTS,
46            AppError::NonLogin => return Redirect::to("/signin").into_response(),
47            AppError::Unauthorized => StatusCode::UNAUTHORIZED,
48            AppError::Banned => StatusCode::FORBIDDEN,
49            _ => StatusCode::INTERNAL_SERVER_ERROR,
50        };
51
52        error!("{}, {}", status, self);
53        let site_config = SiteConfig::get(&DB).unwrap_or_default();
54        let page_data = PageData::new("Error", &site_config, None, false);
55        let page_error = PageError {
56            page_data,
57            status: status.to_string(),
58            error: self.to_string(),
59        };
60
61        into_response(&page_error)
62    }
63}
64
65pub(crate) async fn handler_404(uri: Uri) -> impl IntoResponse {
66    error!("No route for {}", uri);
67    AppError::NotFound
68}
69
70pub(crate) async fn home(
71    cookie: Option<TypedHeader<Cookie>>,
72) -> Result<impl IntoResponse, AppError> {
73    let site_config = SiteConfig::get(&DB)?;
74    let claim = cookie.and_then(|cookie| Claim::get(&DB, &cookie, &site_config));
75    let mut home_page_code = site_config.home_page;
76
77    if let Some(claim) = claim {
78        if let Some(home_page) = DB.open_tree("home_pages")?.get(u32_to_ivec(claim.uid))? {
79            if let Some(code) = home_page.first() {
80                home_page_code = *code;
81                if home_page_code == 1 {
82                    return Ok(Redirect::to(&format!("/feed/{}", claim.uid)));
83                }
84            };
85        }
86    }
87
88    let redirect = match home_page_code {
89        0 => "/inn/0",
90        2 => "/inn/0?filter=joined",
91        3 => "/inn/0?filter=following",
92        4 => "/solo/user/0",
93        5 => "/solo/user/0?filter=Following",
94        6 => "/inn/list",
95        _ => "/inn/0",
96    };
97
98    Ok(Redirect::to(redirect))
99}
100
101static CSS: LazyLock<String> = LazyLock::new(|| {
102    // TODO: CSS minification
103    let mut css = include_str!("../../static/css/bulma.min.css").to_string();
104    css.push('\n');
105    css.push_str(include_str!("../../static/css/bulma-list.css"));
106    css.push('\n');
107    css.push_str(include_str!("../../static/css/main.css"));
108    css
109});
110
111pub(crate) async fn style() -> (HeaderMap, &'static str) {
112    let mut headers = HeaderMap::new();
113
114    headers.insert(
115        HeaderName::from_static("content-type"),
116        HeaderValue::from_static("text/css"),
117    );
118    headers.insert(
119        HeaderName::from_static("cache-control"),
120        HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
121    );
122
123    (headers, &CSS)
124}
125
126pub(crate) async fn favicon() -> (HeaderMap, &'static str) {
127    let mut headers = HeaderMap::new();
128
129    headers.insert(
130        HeaderName::from_static("content-type"),
131        HeaderValue::from_static("image/svg+xml"),
132    );
133    headers.insert(
134        HeaderName::from_static("cache-control"),
135        HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
136    );
137
138    let favicon = include_str!("../../static/favicon.svg");
139
140    (headers, favicon)
141}
142
143pub(crate) async fn encryption_js() -> (HeaderMap, &'static str) {
144    let mut headers = HeaderMap::new();
145    headers.insert(
146        HeaderName::from_static("content-type"),
147        HeaderValue::from_static("text/javascript"),
148    );
149    headers.insert(
150        HeaderName::from_static("cache-control"),
151        HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
152    );
153    let js = include_str!("../../static/js/encryption-helper.js");
154
155    (headers, js)
156}
157
158pub(crate) async fn encoding_js() -> (HeaderMap, &'static str) {
159    let mut headers = HeaderMap::new();
160    headers.insert(
161        HeaderName::from_static("content-type"),
162        HeaderValue::from_static("text/javascript"),
163    );
164    headers.insert(
165        HeaderName::from_static("cache-control"),
166        HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
167    );
168    let js = include_str!("../../static/js/encoding-helper.js");
169
170    (headers, js)
171}
172
173pub(crate) async fn robots() -> &'static str {
174    include_str!("../../static/robots.txt")
175}
176
177pub(super) struct PageData<'a> {
178    pub(super) title: &'a str,
179    pub(super) site_name: &'a str,
180    pub(super) site_description: String,
181    pub(super) claim: Option<Claim>,
182    pub(super) has_unread: bool,
183    pub(super) lang: String,
184}
185
186impl<'a> PageData<'a> {
187    pub(super) fn new(
188        title: &'a str,
189        site_config: &'a SiteConfig,
190        claim: Option<Claim>,
191        has_unread: bool,
192    ) -> Self {
193        let site_description = md2html(&site_config.description);
194        let lang = claim
195            .as_ref()
196            .and_then(|claim| claim.lang.as_ref())
197            .map_or_else(|| site_config.lang.clone(), |lang| lang.to_owned());
198
199        Self {
200            title,
201            site_name: &site_config.site_name,
202            site_description,
203            claim,
204            has_unread,
205            lang,
206        }
207    }
208}
209
210// TODO: https://github.com/hyperium/headers/issues/48
211pub(super) fn get_referer(header: Option<TypedHeader<Referer>>) -> Option<String> {
212    if let Some(TypedHeader(r)) = header {
213        let referer = format!("{r:?}");
214        let trimmed = referer
215            .trim_start_matches("Referer(\"")
216            .trim_end_matches("\")");
217        Some(trimmed.to_owned())
218    } else {
219        None
220    }
221}
222
223pub(super) struct ParamsPage {
224    pub(super) anchor: usize,
225    pub(super) n: usize,
226    pub(super) is_desc: bool,
227}
228
229#[derive(Debug, Clone, Copy, Default)]
230pub(crate) struct ValidatedForm<T>(pub T);
231
232impl<T, S> FromRequest<S> for ValidatedForm<T>
233where
234    T: DeserializeOwned + Validate,
235    S: Send + Sync,
236    Form<T>: FromRequest<S, Rejection = FormRejection>,
237{
238    type Rejection = AppError;
239
240    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
241        let Form(value) = Form::<T>::from_request(req, state).await?;
242        value.validate()?;
243        Ok(ValidatedForm(value))
244    }
245}
246
247/// Render a [`Template`] into a [`Response`], or render an error page.
248pub(crate) fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Response {
249    match tmpl.render() {
250        Ok(body) => Html(body).into_response(),
251        Err(e) => e.to_string().into_response(),
252    }
253}