🗝
summary refs log tree commit diff
path: root/src/server/mod.rs
diff options
context:
space:
mode:
authormia <mia@mia.jetzt>2024-04-16 19:05:41 -0700
committermia <mia@mia.jetzt>2024-04-16 19:05:41 -0700
commit796b2cafc798a7faa80a007002831a4c40635fe8 (patch)
treed8e68590524f4adab7ff8ff6e2cb3dfbb0c64b37 /src/server/mod.rs
downloaddissociate-796b2cafc798a7faa80a007002831a4c40635fe8.tar.gz
dissociate-796b2cafc798a7faa80a007002831a4c40635fe8.zip
initial commit v0.1.0
Diffstat (limited to 'src/server/mod.rs')
-rw-r--r--src/server/mod.rs160
1 files changed, 160 insertions, 0 deletions
diff --git a/src/server/mod.rs b/src/server/mod.rs
new file mode 100644
index 0000000..d9b3beb
--- /dev/null
+++ b/src/server/mod.rs
@@ -0,0 +1,160 @@
+mod admin;
+mod config;
+mod login;
+mod nginx_check;
+mod panel;
+mod store;
+
+use std::{future::IntoFuture, path::PathBuf};
+
+use axum::{
+    body::Body,
+    extract::FromRef,
+    http::{header::CONTENT_TYPE, StatusCode},
+    response::{IntoResponse, Redirect, Response},
+    routing::get,
+    Router,
+};
+use axum_extra::extract::CookieJar;
+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(nginx_check::bind)
+        .pipe(panel::bind)
+        .with_state(ApiState {
+            store,
+            cookie_domain: CookieDomain(config.cookie_domain),
+            web_base: WebBase(config.web_base),
+        })
+        .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,
+}
+
+#[derive(Clone)]
+struct CookieDomain(Option<String>);
+
+#[derive(Clone)]
+struct WebBase(String);
+
+fn render_html(head: PreEscaped<impl AsRef<str>>, body: PreEscaped<impl AsRef<str>>) -> Response {
+    let html = html! {
+        (PreEscaped("<!doctype html>"))
+        html {
+            head {(head)}
+            body {(body)}
+        }
+    }
+    .into_string();
+    Response::builder()
+        .header(CONTENT_TYPE, "text/html; charset=utf-8")
+        .body(Body::new(html))
+        .unwrap()
+}
+
+trait MakeErrorMessage<T> {
+    fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response>;
+}
+
+impl<T, E: ToString> MakeErrorMessage<T> for Result<T, E> {
+    fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response> {
+        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<T> MakeErrorMessage<T> for Option<T> {
+    fn error_message(self, status: StatusCode, message: impl ToString) -> Result<T, Response> {
+        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<T> {
+    fn make_error(self) -> Result<T, Response>;
+}
+
+impl<T> MakeError<T> for std::io::Result<T> {
+    fn make_error(self) -> Result<T, Response> {
+        self.error_message(StatusCode::INTERNAL_SERVER_ERROR, "internal io error")
+    }
+}
+
+trait Nevermind<T> {
+    fn prompt_login(self) -> Result<T, Response>;
+    fn prompt_logout(self) -> Result<T, Response>;
+}
+
+impl<T> Nevermind<T> for Option<T> {
+    fn prompt_login(self) -> Result<T, Response> {
+        self.ok_or_else(|| Redirect::to("/login").into_response())
+    }
+
+    fn prompt_logout(self) -> Result<T, Response> {
+        self.ok_or_else(|| Redirect::to("/login").into_response())
+    }
+}
+
+async fn account_auth(jar: &CookieJar, store: &Store) -> Option<String> {
+    let cookie = jar.get("dissociate-token")?;
+    let token = cookie.value();
+    let name = store.check_token(token).await?;
+    Some(name)
+}