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 rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc}; use rustical_store::{ AddressbookStore, CalendarStore, auth::{AuthenticationProvider, middleware::AuthenticationLayer}, }; use std::sync::Arc; use url::Url; mod assets; mod config; pub mod nextcloud_login; mod oidc_user_store; mod routes; pub use config::FrontendConfig; use oidc_user_store::OidcUserStore; use crate::routes::{ addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook}, app_token::{route_delete_app_token, route_post_app_token}, calendar::{route_calendar, route_calendar_restore, route_delete_calendar}, login::{route_get_login, route_post_login, route_post_logout}, user::{route_get_home, route_root, route_user_named}, }; #[cfg(not(feature = "dev"))] use assets::{Assets, EmbedService}; pub fn frontend_router( prefix: &'static str, auth_provider: Arc, cal_store: Arc, addr_store: Arc, frontend_config: FrontendConfig, oidc_config: Option, ) -> Router { let mut router = Router::new(); router = router .route("/", get(route_root)) .route("/user", get(route_get_home)) .route("/user/{user}", get(route_user_named::)) // App token management .route("/user/{user}/app_token", post(route_post_app_token::)) .route( // POST because HTML5 forms don't support DELETE method "/user/{user}/app_token/{id}/delete", post(route_delete_app_token::), ) // Calendar .route( "/user/{user}/calendar/{calendar}", get(route_calendar::), ) .route( "/user/{user}/calendar/{calendar}/delete", post(route_delete_calendar::), ) .route( "/user/{user}/calendar/{calendar}/restore", post(route_calendar_restore::), ) // Addressbook .route( "/user/{user}/addressbook/{addressbook}", get(route_addressbook::), ) .route( "/user/{user}/addressbook/{addressbook}/delete", post(route_delete_addressbook::), ) .route( "/user/{user}/addressbook/{addressbook}/restore", post(route_addressbook_restore::), ) .route("/login", get(route_get_login).post(route_post_login::)) .route("/logout", post(route_post_logout)); #[cfg(not(feature = "dev"))] let mut router = router.route_service("/assets/{*file}", EmbedService::::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::>), ) .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.clone())) .layer(Extension(cal_store.clone())) .layer(Extension(addr_store.clone())) .layer(Extension(frontend_config.clone())) .layer(Extension(oidc_config.clone())) .layer(middleware::from_fn(unauthorized_handler)); 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#" Unauthorized, redirecting to login page "#, ))) .unwrap(); } resp }