mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
166 lines
5.5 KiB
Rust
166 lines
5.5 KiB
Rust
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
|
use axum::{
|
|
Extension, RequestExt, Router,
|
|
body::Body,
|
|
extract::{OriginalUri, Request},
|
|
middleware::{self, Next},
|
|
response::{Redirect, Response},
|
|
routing::{get, post},
|
|
};
|
|
use headers::{ContentType, HeaderMapExt};
|
|
use http::{Method, StatusCode};
|
|
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
|
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
|
use rustical_store::{
|
|
AddressbookStore, CalendarStore, PrefixedCalendarStore,
|
|
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
|
};
|
|
use std::sync::Arc;
|
|
use url::Url;
|
|
|
|
mod assets;
|
|
mod config;
|
|
pub mod nextcloud_login;
|
|
mod oidc_user_store;
|
|
pub(crate) mod pages;
|
|
mod routes;
|
|
|
|
pub use config::FrontendConfig;
|
|
use oidc_user_store::OidcUserStore;
|
|
|
|
use crate::routes::{
|
|
addressbook::{route_addressbook, route_addressbook_restore},
|
|
app_token::{route_delete_app_token, route_post_app_token},
|
|
calendar::{route_calendar, route_calendar_restore},
|
|
login::{route_get_login, route_post_login, route_post_logout},
|
|
timezones::route_timezones,
|
|
user::{route_get_home, route_root, route_user_named},
|
|
};
|
|
#[cfg(not(feature = "dev"))]
|
|
use assets::{Assets, EmbedService};
|
|
|
|
pub fn frontend_router<
|
|
AP: AuthenticationProvider,
|
|
CS: CalendarStore,
|
|
AS: AddressbookStore + PrefixedCalendarStore,
|
|
>(
|
|
prefix: &'static str,
|
|
auth_provider: Arc<AP>,
|
|
cal_store: Arc<CS>,
|
|
addr_store: Arc<AS>,
|
|
frontend_config: FrontendConfig,
|
|
oidc_config: Option<OidcConfig>,
|
|
) -> Router {
|
|
let user_router = Router::new()
|
|
.route("/", get(route_get_home))
|
|
.route("/{user}", get(route_user_named::<AP>))
|
|
// App token management
|
|
.route("/{user}/app_token", post(route_post_app_token::<AP>))
|
|
.route(
|
|
// POST because HTML5 forms don't support DELETE method
|
|
"/{user}/app_token/{id}/delete",
|
|
post(route_delete_app_token::<AP>),
|
|
)
|
|
// Calendar
|
|
.route("/{user}/calendar", get(route_calendars::<CS>))
|
|
.route("/{user}/calendar/{calendar}", get(route_calendar::<CS>))
|
|
.route(
|
|
"/{user}/calendar/{calendar}/restore",
|
|
post(route_calendar_restore::<CS>),
|
|
)
|
|
// Addressbook
|
|
.route("/{user}/addressbook", get(route_addressbooks::<AS>))
|
|
.route(
|
|
"/{user}/addressbook/{addressbook}",
|
|
get(route_addressbook::<AS>),
|
|
)
|
|
.route(
|
|
"/{user}/addressbook/{addressbook}/restore",
|
|
post(route_addressbook_restore::<AS>),
|
|
)
|
|
.layer(middleware::from_fn(unauthorized_handler));
|
|
|
|
let router = Router::new()
|
|
.route("/", get(route_root))
|
|
.nest("/user", user_router)
|
|
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
|
.route("/logout", post(route_post_logout))
|
|
.route(
|
|
"/_timezones.json",
|
|
get(route_timezones).head(route_timezones),
|
|
);
|
|
|
|
#[cfg(not(feature = "dev"))]
|
|
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());
|
|
#[cfg(feature = "dev")]
|
|
let mut router = router.nest_service(
|
|
"/assets",
|
|
tower_http::services::ServeDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/public/assets")),
|
|
);
|
|
|
|
if let Some(oidc_config) = oidc_config.clone() {
|
|
router = router
|
|
.route("/login/oidc", post(route_post_oidc))
|
|
.route(
|
|
"/login/oidc/callback",
|
|
get(route_get_oidc_callback::<OidcUserStore<AP>>),
|
|
)
|
|
.layer(Extension(OidcUserStore(auth_provider.clone())))
|
|
.layer(Extension(OidcServiceConfig {
|
|
default_redirect_path: "/frontend/user",
|
|
session_key_user_id: "user",
|
|
}))
|
|
.layer(Extension(oidc_config));
|
|
}
|
|
|
|
router = router
|
|
.layer(AuthenticationLayer::new(auth_provider.clone()))
|
|
.layer(Extension(auth_provider))
|
|
.layer(Extension(cal_store))
|
|
.layer(Extension(addr_store))
|
|
.layer(Extension(frontend_config))
|
|
.layer(Extension(oidc_config));
|
|
|
|
Router::new()
|
|
.nest(prefix, router)
|
|
.route("/", get(async || Redirect::to(prefix)))
|
|
}
|
|
|
|
async fn unauthorized_handler(mut request: Request, next: Next) -> Response {
|
|
let meth = request.method().clone();
|
|
let OriginalUri(uri) = request.extract_parts().await.unwrap();
|
|
let resp = next.run(request).await;
|
|
if resp.status() == StatusCode::UNAUTHORIZED {
|
|
// This is a dumb hack since parsed Urls cannot be relative
|
|
let mut login_url: Url = "http://github.com/frontend/login".parse().unwrap();
|
|
if meth == Method::GET {
|
|
login_url
|
|
.query_pairs_mut()
|
|
.append_pair("redirect_uri", uri.path());
|
|
}
|
|
let path = login_url.path();
|
|
let query = login_url
|
|
.query()
|
|
.map(|query| format!("?{query}"))
|
|
.unwrap_or_default();
|
|
let login_url = format!("{path}{query}");
|
|
let mut resp = Response::builder().status(StatusCode::UNAUTHORIZED);
|
|
let hdrs = resp.headers_mut().unwrap();
|
|
hdrs.typed_insert(ContentType::html());
|
|
return resp
|
|
.body(Body::new(format!(
|
|
r#"<!Doctype html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="refresh" content="1; url={login_url}" />
|
|
</head>
|
|
<body>
|
|
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
|
</body>
|
|
</html>"#,
|
|
)))
|
|
.unwrap();
|
|
}
|
|
resp
|
|
}
|