use crate::{file_watcher::FileWatcher, prelude::*, state::State as AppState, static_files}; use axum::{ http::StatusCode, response::{Html, IntoResponse}, routing::{get, post}, Form, Router, }; use maud::{html, Markup, PreEscaped, DOCTYPE}; use redact::Secret; use serde::Deserialize; use thiserror::Error; use tower_http::trace::TraceLayer; use tower_livereload::LiveReloadLayer; #[derive(Error, Debug)] pub enum NewRouterError { #[error("watcher error: {0}")] Watcher(#[from] notify::Error), } #[derive(Error, Debug)] pub enum ReqError {} impl IntoResponse for ReqError { fn into_response(self) -> axum::http::Response { error!("webserver error: {:?}", self); ( StatusCode::INTERNAL_SERVER_ERROR, // TODO: don't expose raw errors over the internet? format!("internal server error: {}", self), ) .into_response() } } pub type ReqResult = Result; pub async fn router( state: AppState, with_watchers: bool, ) -> Result<(Router, Vec>), NewRouterError> { let live_reload_layer: Option = if with_watchers { Some(LiveReloadLayer::new()) } else { None }; let orl = || { if let Some(lr) = &live_reload_layer { Some(lr.reloader()) } else { None } }; let (static_file_service, static_file_watcher) = static_files::router(orl())?; let mut result = Router::new() .route("/", get(index)) .route("/about", get(about)) .route("/login", get(login)) .route("/login", post(authenticate)) .route("/register", get(register)) .route("/register", post(create_user)) .nest_service("/static", static_file_service) .layer(TraceLayer::new_for_http()) .with_state(state.clone()); if let Some(lr) = live_reload_layer { result = result.clone().layer(lr); } let watchers = vec![static_file_watcher]; Ok((result, watchers)) } fn stylesheet(url: &str) -> Markup { html! { link rel="stylesheet" type="text/css" href=(url); } } fn head(page_title: &str) -> Markup { html! { (DOCTYPE) meta charset="utf-8" {} meta name="viewport" content="width=device-width, initial-scale=1" {} title { (page_title) " - lyrs" } (stylesheet("/static/style.css")); script src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous" defer {} } } fn foot() -> Markup { html! { footer class="p-2 border-t-2 border-surface0 flex overflow-x-scroll" { section { (PreEscaped("© 2024 ")) a .underline.text-mauve href="https://lyte.dev" { "lytedev" } } section .ml-auto {("Made with ❤️")} } } } fn page(title: &str, content: Markup) -> ReqResult> { Ok(Html( html! { (head(title)) body .bg-bg.text-text.min-h-lvh.flex.flex-col.font-sans { header class="drop-shadow border-b-2 border-surface0 bg-blue-500 flex overflow-x-scroll" { a class="flex p-2 text-3xl font-mono text-mauve opacity-80 hover:bg-mauve hover:text-bg" href="/" { "lyrs" } nav class="flex flex-1 justify-start" { a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/login" { "Login" } a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/register" { "Register" } } } main class="flex flex-col flex-1 relative overflow-x-scroll bg-mantle" { (content) } } (foot()) }.into_string() )) } async fn index() -> ReqResult> { page("index", html! { "Index" }) } async fn about() -> ReqResult> { page("index", html! { "About" }) } fn center_hero_form(title: &str, content: Markup, subform: Markup) -> Markup { html! { section class="hero grow" { form class="flex flex-col gap-2 w-full max-w-sm" method="post" { header { h1 class="pb-2 text-center text-xl" { (title) } } (content) input class="bg-mauve" value="Submit" type="submit"; } (subform) } } } fn labelled_input(label: &str, input: Markup) -> Markup { html! { label class="flex flex-col" { (label) (input) } } } async fn login() -> ReqResult> { let form = html! { (labelled_input("Username", html!{ input class="input" type="text" name="username" autocomplete="username" required; })) (labelled_input("Password", html!{ input class="input" type="password" name="password" autocomplete="current-password" required; })) }; let subaction = html! { small class="mt-4" { "Need an account? " a href="/register" {"Get one"} "." } }; page("login", center_hero_form("Login", form, subaction)) } async fn register() -> ReqResult> { let form = html! { (labelled_input("Username", html!{ input class="input" type="text" name="username" required; })) (labelled_input("Password", html!{ input class="input" type="password" name="password" autocomplete="new-password" required; })) }; let subaction = html! { small class="mt-4" { "Already have an account? " a href="/login" {"Login"} "." } }; page("login", center_hero_form("Register", form, subaction)) } #[derive(Deserialize, Debug)] struct Creds { username: String, password: Secret, } #[instrument] async fn authenticate(Form(creds): Form) -> ReqResult> { info!("login attempt"); Ok(Html( html! { "no" } .into_string(), )) } #[instrument] async fn create_user(Form(creds): Form) -> ReqResult> { info!("registration attempt"); Ok(Html( html! { "no" } .into_string(), )) }