mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
161 lines
4.1 KiB
Rust
161 lines
4.1 KiB
Rust
use std::{any::Any, sync::Once, time::Duration};
|
|
|
|
use askama::Template;
|
|
use axum::{
|
|
Router,
|
|
extract::rejection::{FormRejection, PathRejection, QueryRejection},
|
|
http::StatusCode,
|
|
middleware::from_fn_with_state,
|
|
response::{Html, IntoResponse, Redirect, Response},
|
|
};
|
|
use conduwuit_service::{Services, state};
|
|
use tower_http::catch_panic::CatchPanicLayer;
|
|
use tower_sec_fetch::SecFetchLayer;
|
|
use tower_sessions::{ExpiredDeletion, SessionManagerLayer, cookie::SameSite};
|
|
|
|
use crate::{
|
|
pages::TemplateContext,
|
|
session::{LoginQuery, store::RocksDbSessionStore},
|
|
};
|
|
|
|
mod extract;
|
|
mod pages;
|
|
mod session;
|
|
|
|
type State = state::State;
|
|
|
|
const CATASTROPHIC_FAILURE: &str = "cat-astrophic failure! we couldn't even render the error template. \
|
|
please contact the team @ https://continuwuity.org";
|
|
|
|
const ROUTE_PREFIX: &str = conduwuit_core::ROUTE_PREFIX;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
enum WebError {
|
|
#[error("Failed to validate form body: {0}")]
|
|
ValidationError(#[from] validator::ValidationErrors),
|
|
#[error("{0}")]
|
|
QueryRejection(#[from] QueryRejection),
|
|
#[error("{0}")]
|
|
FormRejection(#[from] FormRejection),
|
|
#[error("{0}")]
|
|
PathRejection(#[from] PathRejection),
|
|
#[error("{0}")]
|
|
BadRequest(String),
|
|
|
|
#[error("This page does not exist.")]
|
|
NotFound,
|
|
#[error("You are not allowed to request this page: {0}")]
|
|
Forbidden(String),
|
|
#[error("You must log in to access this page")]
|
|
LoginRequired(LoginQuery),
|
|
|
|
#[error("Failed to render template: {0}")]
|
|
Render(#[from] askama::Error),
|
|
#[error("{0}")]
|
|
InternalError(#[from] conduwuit_core::Error),
|
|
#[error("Request handler panicked! {0}")]
|
|
Panic(String),
|
|
}
|
|
|
|
impl IntoResponse for WebError {
|
|
fn into_response(self) -> Response {
|
|
#[derive(Debug, Template)]
|
|
#[template(path = "error.html.j2")]
|
|
struct Error {
|
|
error: WebError,
|
|
status: StatusCode,
|
|
context: TemplateContext,
|
|
}
|
|
|
|
if let Self::LoginRequired(query) = self {
|
|
return Redirect::to(&format!(
|
|
"{}/account/login?{}",
|
|
ROUTE_PREFIX,
|
|
serde_urlencoded::to_string(query).unwrap()
|
|
))
|
|
.into_response();
|
|
}
|
|
|
|
let status = match &self {
|
|
| Self::ValidationError(_)
|
|
| Self::BadRequest(_)
|
|
| Self::QueryRejection(_)
|
|
| Self::FormRejection(_)
|
|
| Self::InternalError(_) => StatusCode::BAD_REQUEST,
|
|
| Self::NotFound => StatusCode::NOT_FOUND,
|
|
| Self::Forbidden(_) => StatusCode::FORBIDDEN,
|
|
| Self::LoginRequired(_) => {
|
|
unreachable!("LoginRequired is handled earlier")
|
|
},
|
|
| _ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
};
|
|
|
|
let template = Error {
|
|
error: self,
|
|
status,
|
|
context: TemplateContext {
|
|
// Statically set false to prevent error pages from being indexed.
|
|
allow_indexing: false,
|
|
csp_nonce: String::new(),
|
|
},
|
|
};
|
|
|
|
if let Ok(body) = template.render() {
|
|
(status, Html(body)).into_response()
|
|
} else {
|
|
(status, CATASTROPHIC_FAILURE).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
static STORE_CLEANUP_TASK: Once = Once::new();
|
|
|
|
pub fn build(services: &Services) -> Router<state::State> {
|
|
#[allow(clippy::wildcard_imports)]
|
|
use pages::*;
|
|
|
|
let store = RocksDbSessionStore::new(&services.db);
|
|
|
|
STORE_CLEANUP_TASK.call_once(|| {
|
|
services.server.runtime().spawn(
|
|
store
|
|
.clone()
|
|
.continuously_delete_expired(Duration::from_hours(1)),
|
|
);
|
|
});
|
|
|
|
Router::new()
|
|
.merge(index::build())
|
|
.nest(
|
|
"/_continuwuity/",
|
|
Router::new()
|
|
.nest("/about", about::build())
|
|
.nest("/account/", account::build())
|
|
.merge(debug::build())
|
|
.nest("/oauth2/", oauth::build())
|
|
.merge(resources::build())
|
|
.merge(threepid::build())
|
|
.fallback(async || WebError::NotFound),
|
|
)
|
|
.layer(
|
|
SessionManagerLayer::new(store)
|
|
.with_name("_c10y_session")
|
|
.with_same_site(SameSite::Lax),
|
|
)
|
|
.layer(CatchPanicLayer::custom(|panic: Box<dyn Any + Send + 'static>| {
|
|
let details = if let Some(s) = panic.downcast_ref::<String>() {
|
|
s.clone()
|
|
} else if let Some(s) = panic.downcast_ref::<&str>() {
|
|
(*s).to_owned()
|
|
} else {
|
|
"(opaque panic payload)".to_owned()
|
|
};
|
|
|
|
WebError::Panic(details).into_response()
|
|
}))
|
|
.layer(from_fn_with_state(services.config.clone(), template_context_middleware))
|
|
.layer(SecFetchLayer::new(|policy| {
|
|
policy.allow_safe_methods().reject_missing_metadata();
|
|
}))
|
|
}
|