fix in smtp and split rs files
This commit is contained in:
1 parent
cc3b172e56
commit
d6c1c52310
30 files changed
+4327
-4387
No files matched your search
+223
@@ -0,0 +1,223 @@
|
||||
mod pages;
|
||||
|
||||
use pages::*;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{DefaultBodyLimit, Request},
|
||||
http::{HeaderName, HeaderValue, StatusCode, header},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
};
|
||||
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(&format!("public, max-age={asset_cache_max_age_seconds}"))
|
||||
.expect("valid asset cache-control header");
|
||||
|
||||
Router::new()
|
||||
.route("/", 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("/errors/private-workspace", get(private_workspace_error))
|
||||
.route("/health", get(health))
|
||||
.route("/robots.txt", get(robots_txt))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.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(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}/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}/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 is_asset = request.uri().path().starts_with("/assets/");
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderValue, StatusCode, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
use crate::{assets, db, state::SharedState};
|
||||
|
||||
pub(super) async fn private_workspace_error(State(state): State<SharedState>) -> Response {
|
||||
error_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"403",
|
||||
"Private workspace",
|
||||
"You do not have permission to access this private workspace. Ask the owner to share it with your account or use a valid share link.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
pub(super) async fn favicon() -> StatusCode {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
pub(super) async fn robots_txt() -> Response {
|
||||
let mut response = "User-agent: *\nDisallow: /f/\nDisallow: /files/\n".into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/plain; charset=utf-8"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
pub(super) async fn home(State(state): State<SharedState>) -> Response {
|
||||
assets::render_html(
|
||||
include_str!("../../static/home.html"),
|
||||
&state.asset_version,
|
||||
state.registration_enabled,
|
||||
state.ldap.is_some(),
|
||||
&state.frontend_log_level,
|
||||
state.upload_max_size_bytes,
|
||||
"home",
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn pad(State(state): State<SharedState>, Path(slug): Path<String>) -> Response {
|
||||
match db::find_pad(&state.db, &slug).await {
|
||||
Ok(Some(pad)) => {
|
||||
let html = include_str!("../../static/pad.html")
|
||||
.replace("__PAD_TITLE__", &escape_html(&pad.title));
|
||||
assets::render_html(
|
||||
&html,
|
||||
&state.asset_version,
|
||||
state.registration_enabled,
|
||||
state.ldap.is_some(),
|
||||
&state.frontend_log_level,
|
||||
state.upload_max_size_bytes,
|
||||
"pad",
|
||||
)
|
||||
}
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Note not found",
|
||||
"This note does not exist or has been deleted.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::error!(%error, %slug, "failed to load standalone pad");
|
||||
internal_error(&state.asset_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn public_page(State(state): State<SharedState>, Path(token): Path<String>) -> Response {
|
||||
match db::find_published_page(&state.db, &token).await {
|
||||
Ok(Some(_)) => assets::render_html(
|
||||
include_str!("../../static/public.html"),
|
||||
&state.asset_version,
|
||||
state.registration_enabled,
|
||||
state.ldap.is_some(),
|
||||
&state.frontend_log_level,
|
||||
state.upload_max_size_bytes,
|
||||
"public",
|
||||
),
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Published page not found",
|
||||
"The link is invalid or the published page has been removed.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::error!(%error, %token, "failed to load published page");
|
||||
internal_error(&state.asset_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn workspace(
|
||||
State(state): State<SharedState>,
|
||||
Path(workspace_slug): Path<String>,
|
||||
) -> Response {
|
||||
match db::find_workspace(&state.db, &workspace_slug).await {
|
||||
Ok(Some(workspace)) => {
|
||||
let html = include_str!("../../static/workspace.html")
|
||||
.replace("__WORKSPACE_TITLE__", &escape_html(&workspace.title));
|
||||
assets::render_html(
|
||||
&html,
|
||||
&state.asset_version,
|
||||
state.registration_enabled,
|
||||
state.ldap.is_some(),
|
||||
&state.frontend_log_level,
|
||||
state.upload_max_size_bytes,
|
||||
"workspace",
|
||||
)
|
||||
}
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Workspace not found",
|
||||
"This workspace does not exist or has been deleted.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::error!(%error, %workspace_slug, "failed to load workspace page");
|
||||
internal_error(&state.asset_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn note(
|
||||
State(state): State<SharedState>,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
let workspace = match db::find_workspace(&state.db, &workspace_slug).await {
|
||||
Ok(Some(workspace)) => workspace,
|
||||
Ok(None) => {
|
||||
return error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Workspace not found",
|
||||
"The workspace for this note does not exist or has been deleted.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, %workspace_slug, "failed to load note workspace");
|
||||
return internal_error(&state.asset_version);
|
||||
}
|
||||
};
|
||||
|
||||
match db::find_note(&state.db, workspace.id, ¬e_slug).await {
|
||||
Ok(Some(note)) => {
|
||||
let html = include_str!("../../static/note.html")
|
||||
.replace("__NOTE_TITLE__", &escape_html(¬e.title))
|
||||
.replace("__WORKSPACE_TITLE__", &escape_html(&workspace.title))
|
||||
.replace("__WORKSPACE_SLUG__", &escape_html(&workspace_slug));
|
||||
assets::render_html(
|
||||
&html,
|
||||
&state.asset_version,
|
||||
state.registration_enabled,
|
||||
state.ldap.is_some(),
|
||||
&state.frontend_log_level,
|
||||
state.upload_max_size_bytes,
|
||||
"note",
|
||||
)
|
||||
}
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Note not found",
|
||||
"This note does not exist or has been deleted.",
|
||||
&format!("/w/{workspace_slug}"),
|
||||
"Back do workspace",
|
||||
&state.asset_version,
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::error!(%error, %workspace_slug, %note_slug, "failed to load note page");
|
||||
internal_error(&state.asset_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn static_not_found(State(state): State<SharedState>) -> Response {
|
||||
error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"File not found",
|
||||
"The requested static file does not exist.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn method_not_allowed(State(state): State<SharedState>) -> Response {
|
||||
error_response(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
"405",
|
||||
"Method not allowed",
|
||||
"This address does not support the requested operation.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn not_found(State(state): State<SharedState>) -> Response {
|
||||
error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
"Page not found",
|
||||
"Check the address or return to the home page.",
|
||||
"/",
|
||||
"Home page",
|
||||
&state.asset_version,
|
||||
)
|
||||
}
|
||||
|
||||
fn internal_error(asset_version: &str) -> Response {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"500",
|
||||
"Server error",
|
||||
"The page could not be loaded. Please try again shortly.",
|
||||
"/",
|
||||
"Home page",
|
||||
asset_version,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn error_response(
|
||||
status: StatusCode,
|
||||
code: &str,
|
||||
title: &str,
|
||||
message: &str,
|
||||
primary_url: &str,
|
||||
primary_label: &str,
|
||||
asset_version: &str,
|
||||
) -> Response {
|
||||
let html = include_str!("../../static/error.html")
|
||||
.replace(
|
||||
"__APP_STYLESHEET__",
|
||||
&assets::stylesheet_tag(asset_version, "styles"),
|
||||
)
|
||||
.replace("__ERROR_CODE__", &escape_html(code))
|
||||
.replace("__ERROR_TITLE__", &escape_html(title))
|
||||
.replace("__ERROR_MESSAGE__", &escape_html(message))
|
||||
.replace("__PRIMARY_URL__", &escape_html(primary_url))
|
||||
.replace("__PRIMARY_LABEL__", &escape_html(primary_label));
|
||||
|
||||
let mut response = (status, Html(html)).into_response();
|
||||
no_store(&mut response);
|
||||
response
|
||||
}
|
||||
|
||||
fn no_store(response: &mut Response) {
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("private, no-store"),
|
||||
);
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
Reference in new issue
Block a user