mod admin; mod config; mod falx; mod login; mod panel; mod store; use std::{future::IntoFuture, path::PathBuf, sync::Arc}; use axum::{ body::Body, extract::FromRef, http::{header::CONTENT_TYPE, StatusCode}, response::{IntoResponse, Redirect, Response}, routing::get, Router, }; use axum_extra::extract::CookieJar; use cursive::reexports::ahash::HashSet; use eyre::Context; use maud::{html, PreEscaped}; use tap::Pipe; use tokio::{net::TcpListener, select}; use crate::server::store::Store; use self::config::Config; pub async fn serve() -> eyre::Result<()> { let config_path = std::env::args() .skip(2) .next() .map(PathBuf::from) .unwrap_or_else(|| std::env::current_dir().unwrap().join("dissociate.toml")); let config = Config::load(&config_path)?; let store = Store::load(config.data).await?; println!("admin listening on {:?}", config.admin_socket); let admin_serve = admin::serve(&config.admin_socket, config.web_base.clone(), store.clone())?; let listener = TcpListener::bind(config.web_socket).await?; let app = Router::new() .pipe(login::bind) .pipe(falx::bind) .pipe(panel::bind) .with_state(ApiState { store, cookie_domain: CookieDomain(config.cookie_domain), web_base: WebBase(config.web_base), handoffs: Handoffs(Arc::new(config.handoffs)), }) .fallback(get(|| async { render_html( html!(title { "not found" }), html! { h1 { "404 not found" } p {"sowwy :("} }, ) })); println!("web listening on {:?}", config.web_socket); let web_serve = axum::serve(listener, app).into_future(); select! { res = admin_serve => res.wrap_err("in admin"), res = web_serve => res.wrap_err("in web"), } } #[derive(Clone, FromRef)] struct ApiState { pub store: Store, pub cookie_domain: CookieDomain, pub web_base: WebBase, pub handoffs: Handoffs, } #[derive(Clone)] struct CookieDomain(Option); #[derive(Clone)] struct WebBase(String); #[derive(Clone)] struct Handoffs(Arc>); fn render_html(head: PreEscaped>, body: PreEscaped>) -> Response { let html = html! { (PreEscaped("")) html { head {(head)} body {(body)} } } .into_string(); Response::builder() .header(CONTENT_TYPE, "text/html; charset=utf-8") .body(Body::new(html)) .unwrap() } trait MakeErrorMessage { fn error_message(self, status: StatusCode, message: impl ToString) -> Result; } impl MakeErrorMessage for Result { fn error_message(self, status: StatusCode, message: impl ToString) -> Result { self.map_err(|err| { render_html( html!(title { "error" }), html! { h1 { (status.canonical_reason().unwrap_or_else(|| status.as_str())) } pre { (err.to_string()) ": " (message.to_string()) } }, ) }) } } impl MakeErrorMessage for Option { fn error_message(self, status: StatusCode, message: impl ToString) -> Result { self.ok_or_else(|| { render_html( html!(title { "error" }), html! { h1 { (status.canonical_reason().unwrap_or_else(|| status.as_str())) } pre { (message.to_string()) } }, ) }) } } trait MakeError { fn make_error(self) -> Result; } impl MakeError for std::io::Result { fn make_error(self) -> Result { self.error_message(StatusCode::INTERNAL_SERVER_ERROR, "internal io error") } } trait Nevermind { fn prompt_login(self) -> Result; fn prompt_logout(self) -> Result; } impl Nevermind for Option { fn prompt_login(self) -> Result { self.ok_or_else(|| Redirect::to("/login").into_response()) } fn prompt_logout(self) -> Result { self.ok_or_else(|| Redirect::to("/login").into_response()) } } async fn account_auth(jar: &CookieJar, store: &Store) -> Option { let cookie = jar.get("dissociate-token")?; let token = cookie.value(); let (name, _) = store.check_token(token).await?; Some(name) }