2024-05-15 16:48:23 -05:00
|
|
|
use crate::{
|
|
|
|
file_watcher::FileWatcher,
|
|
|
|
prelude::*,
|
|
|
|
state::{NewStateError, State as AppState},
|
|
|
|
static_files,
|
|
|
|
};
|
|
|
|
use axum::{
|
2024-05-17 12:00:37 -05:00
|
|
|
http::StatusCode,
|
2024-05-15 16:48:23 -05:00
|
|
|
response::{Html, IntoResponse},
|
2024-05-17 12:00:37 -05:00
|
|
|
routing::{get, post},
|
|
|
|
Form, Router,
|
2024-05-15 16:48:23 -05:00
|
|
|
};
|
2024-05-17 12:00:37 -05:00
|
|
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
|
|
|
use redact::Secret;
|
|
|
|
use serde::Deserialize;
|
2024-05-15 16:48:23 -05:00
|
|
|
use thiserror::Error;
|
2024-05-17 12:00:37 -05:00
|
|
|
use tower_http::trace::TraceLayer;
|
|
|
|
use tower_livereload::LiveReloadLayer;
|
2024-05-15 16:48:23 -05:00
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
pub enum NewRouterError {
|
|
|
|
#[error("watcher error: {0}")]
|
|
|
|
Watcher(#[from] notify::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
2024-05-17 12:00:37 -05:00
|
|
|
pub enum ReqError {}
|
2024-05-15 16:48:23 -05:00
|
|
|
|
|
|
|
impl IntoResponse for ReqError {
|
|
|
|
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
|
|
|
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<T> = Result<T, ReqError>;
|
|
|
|
|
|
|
|
pub async fn router(
|
2024-05-17 12:00:37 -05:00
|
|
|
state: AppState,
|
2024-05-15 16:48:23 -05:00
|
|
|
with_watchers: bool,
|
|
|
|
) -> Result<(Router, Vec<Option<FileWatcher>>), NewRouterError> {
|
2024-05-14 15:33:49 -05:00
|
|
|
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
|
|
|
|
Some(LiveReloadLayer::new())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2024-05-14 14:30:03 -05:00
|
|
|
|
2024-05-14 15:33:49 -05:00
|
|
|
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()
|
2024-05-14 14:30:03 -05:00
|
|
|
.route("/", get(index))
|
2024-05-14 16:56:22 -05:00
|
|
|
.route("/about", get(about))
|
2024-05-17 12:00:37 -05:00
|
|
|
.route("/login", get(login))
|
|
|
|
.route("/login", post(authenticate))
|
|
|
|
.route("/register", get(register))
|
|
|
|
.route("/register", post(create_user))
|
2024-05-14 14:30:03 -05:00
|
|
|
.nest_service("/static", static_file_service)
|
2024-05-17 12:00:37 -05:00
|
|
|
.layer(TraceLayer::new_for_http())
|
2024-05-14 15:33:49 -05:00
|
|
|
.with_state(state.clone());
|
|
|
|
|
|
|
|
if let Some(lr) = live_reload_layer {
|
|
|
|
result = result.clone().layer(lr);
|
|
|
|
}
|
|
|
|
|
2024-05-17 12:00:37 -05:00
|
|
|
let watchers = vec![static_file_watcher];
|
2024-05-14 15:33:49 -05:00
|
|
|
|
|
|
|
Ok((result, watchers))
|
2024-05-14 14:30:03 -05:00
|
|
|
}
|
|
|
|
|
2024-05-17 12:00:37 -05:00
|
|
|
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<Html<String>> {
|
2024-05-14 14:30:03 -05:00
|
|
|
Ok(Html(
|
2024-05-17 12:00:37 -05:00
|
|
|
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()
|
2024-05-14 14:30:03 -05:00
|
|
|
))
|
|
|
|
}
|
2024-05-15 17:13:41 -05:00
|
|
|
|
2024-05-17 12:00:37 -05:00
|
|
|
async fn index() -> ReqResult<Html<String>> {
|
|
|
|
page("index", html! { "Index" })
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn about() -> ReqResult<Html<String>> {
|
|
|
|
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<Html<String>> {
|
|
|
|
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<Html<String>> {
|
|
|
|
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<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[instrument]
|
|
|
|
async fn authenticate(Form(creds): Form<Creds>) -> ReqResult<Html<String>> {
|
|
|
|
info!("login attempt");
|
2024-05-14 16:56:22 -05:00
|
|
|
Ok(Html(
|
2024-05-17 12:00:37 -05:00
|
|
|
html! {
|
|
|
|
"no"
|
|
|
|
}
|
|
|
|
.into_string(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[instrument]
|
|
|
|
async fn create_user(Form(creds): Form<Creds>) -> ReqResult<Html<String>> {
|
|
|
|
info!("registration attempt");
|
|
|
|
Ok(Html(
|
|
|
|
html! {
|
|
|
|
"no"
|
|
|
|
}
|
|
|
|
.into_string(),
|
2024-05-14 16:56:22 -05:00
|
|
|
))
|
|
|
|
}
|