use axum::{ Extension, RequestExt, Router, body::Body, extract::{OriginalUri, Request}, middleware::{self, Next}, response::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 tower_sessions::{ Expiry, SessionManagerLayer, SessionStore, cookie::{SameSite, time::Duration}, }; 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::{ assets::{Assets, EmbedService}, routes::{ addressbook::{route_addressbook, route_addressbook_restore, route_create_addressbook}, app_token::{route_delete_app_token, route_post_app_token}, calendar::{route_calendar, route_calendar_restore, route_create_calendar}, login::{route_get_login, route_post_login, route_post_logout}, user::{route_get_home, route_root, route_user_named}, }, }; pub fn frontend_router< AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore, S: SessionStore + Clone, >( auth_provider: Arc, cal_store: Arc, addr_store: Arc, frontend_config: FrontendConfig, oidc_config: Option, session_store: S, ) -> 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", post(route_create_calendar::)) .route( "/user/{user}/calendar/{calendar}", get(route_calendar::), ) .route( "/user/{user}/calendar/{calendar}/restore", post(route_calendar_restore::), ) // Addressbook .route( "/user/{user}/addressbook", post(route_create_addressbook::), ) .route( "/user/{user}/addressbook/{addressbook}", get(route_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)) .route_service("/assets/{*file}", EmbedService::::new()); 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 .layer(AuthenticationLayer::new(auth_provider.clone())) .layer( SessionManagerLayer::new(session_store) .with_secure(true) .with_same_site(SameSite::Strict) .with_expiry(Expiry::OnInactivity(Duration::hours(2))), ) .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)) } 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 }