258 lines
9.5 KiB
Rust
258 lines
9.5 KiB
Rust
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
mod pages;
|
|
|
|
use axum::{
|
|
Router,
|
|
extract::{DefaultBodyLimit, Request},
|
|
http::{HeaderName, HeaderValue, StatusCode, header},
|
|
middleware::{self, Next},
|
|
response::Response,
|
|
routing::{get, post},
|
|
};
|
|
use pages::*;
|
|
use tower::{ServiceBuilder, service_fn};
|
|
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer, trace::TraceLayer};
|
|
|
|
use crate::{api, auth, state::SharedState, websocket};
|
|
use std::convert::Infallible;
|
|
|
|
pub fn router(
|
|
state: SharedState,
|
|
static_dir: &str,
|
|
upload_max_size_bytes: usize,
|
|
asset_cache_max_age_seconds: u64,
|
|
) -> Router {
|
|
let asset_version = state.asset_version.clone();
|
|
let asset_not_found = service_fn(move |_request| {
|
|
let asset_version = asset_version.clone();
|
|
async move {
|
|
Ok::<_, Infallible>(error_response(
|
|
StatusCode::NOT_FOUND,
|
|
"404",
|
|
"File not found",
|
|
"The requested application asset does not exist.",
|
|
"/",
|
|
"Home page",
|
|
&asset_version,
|
|
))
|
|
}
|
|
});
|
|
|
|
let asset_cache_control =
|
|
HeaderValue::from_str(&crate::cache::cache_control(asset_cache_max_age_seconds))
|
|
.expect("valid asset cache-control header");
|
|
|
|
Router::new()
|
|
.route("/", get(home))
|
|
.route("/auth/confirm/{token}", get(home))
|
|
.route("/auth/reset-password/{token}", get(home))
|
|
.route("/auth/account-action/{token}", get(home))
|
|
.route("/p/{slug}", get(pad))
|
|
.route("/s/{token}", get(public_page))
|
|
.route("/w/{workspace_slug}", get(workspace))
|
|
.route("/w/{workspace_slug}/n/{note_slug}", get(note))
|
|
.route("/health", get(health))
|
|
.route("/robots.txt", get(robots_txt))
|
|
.route("/favicon.ico", get(favicon))
|
|
.route("/favicon.svg", get(favicon_svg))
|
|
.route("/favicon-32.png", get(favicon_png))
|
|
.route("/apple-touch-icon.png", get(apple_touch_icon))
|
|
.route("/icons/favicon.ico", get(favicon))
|
|
.route("/icons/favicon.svg", get(favicon_svg))
|
|
.route("/icons/favicon-32.png", get(favicon_png))
|
|
.route("/icons/apple-touch-icon.png", get(apple_touch_icon))
|
|
.route("/f/{token}/{filename}", get(api::download_file))
|
|
.route(
|
|
"/files/{directory}/{filename}",
|
|
get(api::download_legacy_file),
|
|
)
|
|
.route("/api/auth/identity", post(auth::identity))
|
|
.route("/api/access-token", post(api::create_resource_access_token))
|
|
.route("/api/auth/register", post(auth::register))
|
|
.route("/api/auth/login", post(auth::login))
|
|
.route("/api/auth/confirm-account", post(auth::confirm_account))
|
|
.route(
|
|
"/api/auth/resend-confirmation",
|
|
post(auth::resend_confirmation),
|
|
)
|
|
.route("/api/auth/me", get(auth::me))
|
|
.route("/api/auth/profile", post(auth::update_profile))
|
|
.route(
|
|
"/api/auth/account/delete",
|
|
post(auth::request_account_deletion),
|
|
)
|
|
.route(
|
|
"/api/auth/account-action/confirm",
|
|
post(auth::confirm_account_action),
|
|
)
|
|
.route("/api/auth/logout", post(auth::logout))
|
|
.route(
|
|
"/api/auth/resources",
|
|
get(auth::resources)
|
|
.put(auth::update_resource)
|
|
.delete(auth::delete_resource),
|
|
)
|
|
.route(
|
|
"/api/auth/resources/privacy",
|
|
post(auth::set_resource_privacy),
|
|
)
|
|
.route(
|
|
"/api/auth/resources/sharing",
|
|
get(auth::resource_sharing)
|
|
.post(auth::share_resource_users)
|
|
.delete(auth::remove_resource_user),
|
|
)
|
|
.route(
|
|
"/api/auth/resources/share-links",
|
|
post(auth::create_share_link)
|
|
.put(auth::update_share_link)
|
|
.delete(auth::revoke_share_link),
|
|
)
|
|
.route(
|
|
"/share-invitations/{token}/accept",
|
|
get(home).post(auth::accept_share_invitation),
|
|
)
|
|
.route("/api/auth/password-reset", post(auth::request_reset))
|
|
.route(
|
|
"/api/auth/password-reset/confirm",
|
|
post(auth::confirm_reset),
|
|
)
|
|
.route("/api/public/{token}", get(api::public_page))
|
|
.route("/api/public/{token}/tasks", post(api::update_public_task))
|
|
.route("/api/pads", post(api::create_pad))
|
|
.route("/api/pads/{slug}", get(api::pad_info))
|
|
.route("/api/pads/{slug}/history", post(api::pad_history))
|
|
.route(
|
|
"/api/pads/{slug}/editor-color",
|
|
get(api::pad_editor_color).post(api::set_pad_editor_color),
|
|
)
|
|
.route(
|
|
"/api/pads/{slug}/editor-settings",
|
|
post(api::set_pad_editor_settings),
|
|
)
|
|
.route("/api/pads/{slug}/publish", post(api::publish_pad_page))
|
|
.route("/api/pads/{slug}/restore", post(api::pad_restore))
|
|
.route(
|
|
"/api/pads/{slug}/files",
|
|
post(api::upload_pad_file).put(api::pad_files),
|
|
)
|
|
.route(
|
|
"/api/pads/{slug}/files/{file_id}",
|
|
axum::routing::delete(api::delete_pad_file),
|
|
)
|
|
.route("/api/workspaces", post(api::create_workspace))
|
|
.route("/api/workspaces/{workspace_slug}", get(api::workspace_info))
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/open",
|
|
post(api::open_workspace),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes",
|
|
post(api::create_note),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}",
|
|
get(api::note_info).delete(api::delete_note),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-color",
|
|
get(api::note_editor_color).post(api::set_note_editor_color),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-settings",
|
|
post(api::set_note_editor_settings),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/publish",
|
|
post(api::publish_note_page),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/history",
|
|
post(api::history),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/restore",
|
|
post(api::restore),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/files",
|
|
post(api::upload_note_file).put(api::note_files),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_slug}/notes/{note_slug}/files/{file_id}",
|
|
axum::routing::delete(api::delete_note_file),
|
|
)
|
|
.route("/ws/p/{slug}", get(websocket::upgrade_pad))
|
|
.route("/ws/{workspace_slug}/{note_slug}", get(websocket::upgrade))
|
|
.route("/static", get(static_not_found))
|
|
.route("/static/{*path}", get(static_not_found))
|
|
.nest_service(
|
|
"/assets",
|
|
ServiceBuilder::new()
|
|
.layer(SetResponseHeaderLayer::overriding(
|
|
header::CACHE_CONTROL,
|
|
asset_cache_control,
|
|
))
|
|
.service(ServeDir::new(static_dir).not_found_service(asset_not_found)),
|
|
)
|
|
.fallback(not_found)
|
|
.method_not_allowed_fallback(method_not_allowed)
|
|
.layer(DefaultBodyLimit::max(
|
|
upload_max_size_bytes.saturating_add(1024 * 1024),
|
|
))
|
|
.layer(SetResponseHeaderLayer::if_not_present(
|
|
HeaderName::from_static("x-frame-options"),
|
|
HeaderValue::from_static("DENY"),
|
|
))
|
|
.layer(SetResponseHeaderLayer::if_not_present(
|
|
HeaderName::from_static("cross-origin-opener-policy"),
|
|
HeaderValue::from_static("same-origin"),
|
|
))
|
|
.layer(SetResponseHeaderLayer::if_not_present(
|
|
HeaderName::from_static("cross-origin-resource-policy"),
|
|
HeaderValue::from_static("same-origin"),
|
|
))
|
|
.layer(TraceLayer::new_for_http())
|
|
.layer(middleware::from_fn(add_non_asset_security_headers))
|
|
.with_state(state)
|
|
}
|
|
|
|
async fn add_non_asset_security_headers(request: Request, next: Next) -> Response {
|
|
let path = request.uri().path();
|
|
let is_asset = path.starts_with("/assets/");
|
|
let is_file = path.starts_with("/f/");
|
|
let mut response = next.run(request).await;
|
|
|
|
if is_file {
|
|
let headers = response.headers_mut();
|
|
headers.remove("x-frame-options");
|
|
headers.remove("cross-origin-opener-policy");
|
|
headers.remove("cross-origin-resource-policy");
|
|
headers.remove("referrer-policy");
|
|
headers.remove("permissions-policy");
|
|
} else if !is_asset {
|
|
let headers = response.headers_mut();
|
|
headers
|
|
.entry("x-content-type-options")
|
|
.or_insert(HeaderValue::from_static("nosniff"));
|
|
headers
|
|
.entry("referrer-policy")
|
|
.or_insert(HeaderValue::from_static("strict-origin-when-cross-origin"));
|
|
headers
|
|
.entry("permissions-policy")
|
|
.or_insert(HeaderValue::from_static(
|
|
"camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
|
));
|
|
}
|
|
|
|
response
|
|
}
|