use std; use axum::{ body::Full, extract::Path, http::{header, HeaderValue, StatusCode}, response::{self, IntoResponse, Response}, routing, Extension, Form, Router, }; use mime_guess::mime; use tower_cookies::{Cookie, Cookies}; use crate::svc::auth::{AuthError, Claims}; use super::{ servek::{Server, ServerError}, CreateProfileRequest, LoginRequest, NavType, Notification, Redirect, WithNav, }; use rust_embed::RustEmbed; const AUTH_COOKIE_NAME: &str = "flabk_token"; #[derive(RustEmbed)] #[folder = "static"] struct StaticData; impl Server { pub(super) fn register_html(&self, router: &Router) -> Router { router .clone() .route("/favicon.svg", routing::get(Self::favicon)) .route("/", routing::get(Self::index)) .route("/login", routing::get(Self::login_page).post(Self::login)) .route("/logout", routing::get(Self::logout)) .route( "/signup", routing::get(Self::signup_page).post(Self::create_user), ) .route("/@/:username", routing::get(Self::profile)) .route("/static/*file", routing::get(Self::static_handler)) .fallback(routing::get(Self::handler_404)) } fn from_cookies(&self, cookies: Cookies) -> Result>, ServerError> { const LOGGED_OUT: Result>, ServerError> = Ok(WithNav { obj: None, nav_type: NavType::LoggedOut, }); if let Some(cookie) = cookies.get(AUTH_COOKIE_NAME) { let claims = match self.auth.get_claims(cookie.value().to_owned()) { Ok(claims) => claims, Err(e) => { if e.clone().expired() { cookies.remove(Cookie::new(AUTH_COOKIE_NAME, "")); return LOGGED_OUT; } else { return Err(e.into()); } } }; Ok(WithNav::new(Some(claims.clone()), claims.into())) } else { LOGGED_OUT } } async fn index( Extension(srv): Extension, cookies: Cookies, ) -> Result { Ok(( StatusCode::OK, response::Html(srv.hb.render("index", &srv.from_cookies(cookies)?)?), )) } async fn favicon() -> impl IntoResponse { ( StatusCode::OK, ( [( header::CONTENT_TYPE, HeaderValue::from_static(mime::IMAGE_SVG.as_ref()), )], axum::body::boxed(Full::from( include_bytes!("../../flabk_icon_placeholder.svg").as_ref(), )), ), ) } async fn handler_404( Extension(srv): Extension, cookies: Cookies, ) -> Result { Ok(( StatusCode::NOT_FOUND, response::Html(srv.hb.render("err404", &srv.from_cookies(cookies)?)?), )) } async fn logout( Extension(srv): Extension, cookies: Cookies, ) -> Result { cookies.remove(Cookie::new(AUTH_COOKIE_NAME, "")); Ok(( StatusCode::OK, response::Html(srv.hb.render( "redirect", &Redirect { location: "/".to_owned(), }, )?), )) } fn login_page_with( &self, tag_name: String, message: String, ) -> Result, ServerError> { Ok(self .hb .render( "login", &serde_json::json!(Notification { message, tag_name }), ) .map(|html| response::Html(html))?) } async fn login_page( Extension(srv): Extension, ) -> Result { Ok(( StatusCode::OK, response::Html(srv.hb.render("login", &serde_json::json!(()))?), )) } async fn login( cookies: Cookies, Extension(srv): Extension, Form(login): Form, ) -> Result { if login.username == "" || login.password == "" { return Ok(( StatusCode::BAD_REQUEST, srv.login_page_with( "error-partial".to_owned(), "credentials required".to_owned(), )?, )); } let token = match srv.auth.login(login.username, login.password).await { Ok(token) => token, Err(e) => match e { AuthError::InvalidCredentials => { return Ok(( StatusCode::UNAUTHORIZED, srv.login_page_with( "error-partial".to_owned(), "invalid credentials".to_owned(), )?, )) } e => return Err(e.into()), }, }; if cookies.get(AUTH_COOKIE_NAME).is_some() { cookies.remove(Cookie::new(AUTH_COOKIE_NAME, "")); } cookies.add(Cookie::new(AUTH_COOKIE_NAME, token)); Ok(( StatusCode::OK, response::Html(srv.hb.render( "redirect", &Redirect { location: "/".to_owned(), }, )?), )) } async fn profile( cookies: Cookies, Extension(srv): Extension, Path(username): Path, ) -> Result { Ok(( StatusCode::OK, response::Html(srv.hb.render( "profile", &WithNav::new( srv.profiler.profile(username).await?, srv.from_cookies(cookies)?.nav_type, ), )?), )) } async fn create_user( Extension(srv): Extension, Form(body): Form, ) -> Result { if body.username == "" { return srv.signup_page_with("error-partial".to_owned(), "empty username".to_owned()); } if body.password == "" { return srv.signup_page_with("error-partial".to_owned(), "empty password".to_owned()); } if body.email == "" { return srv.signup_page_with("error-partial".to_owned(), "empty email".to_owned()); } srv.profiler .create_user(body.username, body.password, body.email) .await?; srv.signup_page_with("success-partial".to_owned(), "signup successful".to_owned()) } async fn signup_page( Extension(srv): Extension, ) -> Result { Ok(( StatusCode::OK, response::Html(srv.hb.render("signup", &serde_json::json!(()))?), )) } fn signup_page_with( &self, tag_name: String, message: String, ) -> Result { Ok(( StatusCode::OK, response::Html(self.hb.render( "signup", &serde_json::json!(Notification { message, tag_name }), )?), )) } async fn static_handler(Path(mut path): Path) -> impl IntoResponse { path.remove(0); println!("getting path: {}", path); StaticFile(path) } } pub struct StaticFile(pub T); impl IntoResponse for StaticFile where T: Into, { fn into_response(self) -> axum::response::Response { let path = self.0.into(); match StaticData::get(path.as_str()) { Some(content) => { let body = axum::body::boxed(Full::from(content.data)); let mime = mime_guess::from_path(path).first_or_octet_stream(); Response::builder() .header(header::CONTENT_TYPE, mime.as_ref()) .body(body) .unwrap() } None => Response::builder() .status(StatusCode::NOT_FOUND) .body(axum::body::boxed(Full::from("404"))) .unwrap(), } } }