use std::{collections::HashMap, str::FromStr}; use warp::{ http::HeaderValue, hyper::Uri, path::Tail, reply::{Html, Response}, Filter, Rejection, Reply, }; use crate::svc::auth::AuthError; use super::{ servek::{Server, ServerError}, CreateProfileRequest, ErrorTemplate, }; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "static"] struct StaticData; impl Server { pub(super) async fn html( &self, ) -> impl Filter + Clone { Self::index() .or(self.profile()) .or(self.create_profile()) .or(Server::static_files()) .or(self.login_page()) .or(self.login()) .or(Self::handler_404()) } fn index() -> impl Filter + Clone { warp::get().and(warp::path::end().map(move || { warp::reply::html(include_str!("../../templates/html/index.html").to_owned()) })) } fn handler_404() -> impl Filter + Clone { warp::get().and(warp::path::end().map(move || { warp::reply::html(include_str!("../../templates/html/404.html").to_owned()) })) } fn login_with_error(&self, error: String) -> Result, Rejection> { self.hb .render("login-error", &serde_json::json!(ErrorTemplate { error })) .map(|html| warp::reply::html(html)) .map_err(|e| ServerError::from(e).rejection()) } fn login_page( &self, ) -> impl Filter + Clone { warp::get().and(warp::path::path("login").map(move || { warp::reply::html(include_str!("../../templates/html/login.html").to_owned()) })) } fn login(&self) -> impl Filter + Clone { warp::post().and( warp::body::content_length_limit(8192).and( warp::path::path("login") .and(Self::with_server(self.clone())) .and(warp::body::form()) .and_then(|srv: Server, body: HashMap| async move { let user = body.get("username").ok_or( ServerError::BadRequest("no username provided".to_owned()).rejection(), )?; let pass = body.get("password").ok_or( ServerError::BadRequest("no password provided".to_owned()).rejection(), )?; let token = srv .auth .login(user.clone(), pass.clone()) .await .map(|html| warp::reply::html(html)); if let Err(e) = &token { if let AuthError::InvalidCredentials = e { return srv.login_with_error("invalid credentials".to_owned()); } } token.map_err(|e| ServerError::from(e).rejection()) }), ), ) } fn with_server( srv: Server, ) -> impl Filter + Clone { warp::any().map(move || srv.clone()) } fn profile(&self) -> impl Filter + Clone { warp::get().and( warp::path!("@" / String) .and(Self::with_server(self.clone())) .and_then(|username: String, srv: Server| async move { srv.hb .render( "profile", &serde_json::json!(srv .profiler .profile(username) .await .map_err(|e| ServerError::from(e))?), ) .map(|html| warp::reply::html(html)) .map_err(|e| ServerError::from(e).rejection()) }), ) } fn create_profile(&self) -> impl Filter + Clone { warp::post().and( warp::body::content_length_limit(8192).and( warp::path!("@" / String) .and(Self::with_server(self.clone())) .and(warp::body::form()) .and_then( |username: String, srv: Server, body: CreateProfileRequest| async move { if body.password_hash.len() == 0 { return Err(ServerError::BadRequest( "cannot have an empty password".to_owned(), ) .rejection()); } let user = srv .profiler .create_user(username, body.password_hash) .await .map_err(|e| ServerError::from(e)); match user { Ok(u) => Ok(warp::redirect( Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(), )), Err(e) => Err(e.rejection()), } }, ), ), ) } fn static_files() -> impl Filter + Clone { warp::get().and(warp::path("static").and(warp::path::tail()).and_then( |path: Tail| async move { let asset = match StaticData::get(path.as_str()) { Some(a) => a, None => return Err(ServerError::NotFound.rejection()), }; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); let mut res = Response::new(asset.data.into()); res.headers_mut().insert( "Content-Type", HeaderValue::from_str(mime.as_ref()).unwrap(), ); Ok(res) }, )) } }