- Add a CLI with subcommands for "serve" and "add-user" - Inline the manifest with serde_json - Move the server to a separate file - Change login to token cookies - Persist the task form between loads
155 lines
5.3 KiB
Rust
155 lines
5.3 KiB
Rust
use std::io::stdout;
|
|
|
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
|
use rouille::{post_input, router, try_or_400, Request, Response};
|
|
use serde_json::json;
|
|
|
|
use crate::{
|
|
database::AppDatabase,
|
|
task::{Task, TaskPriority, TaskStatus},
|
|
};
|
|
|
|
pub fn serve(port: u16, database: AppDatabase) {
|
|
println!("Server started at http://localhost:{}", port);
|
|
rouille::start_server(format!("0.0.0.0:{}", port), move |request| {
|
|
rouille::log(request, stdout(), || route(request, &database))
|
|
});
|
|
}
|
|
|
|
fn route(request: &Request, database: &AppDatabase) -> Response {
|
|
let token = rouille::input::cookies(request)
|
|
.find(|c| c.0 == "token")
|
|
.map(|c| c.1);
|
|
let authenticated = token.is_some_and(|t| database.get_auth_user(t).unwrap().is_some());
|
|
router!(request,
|
|
(GET) ["/"] => match authenticated {
|
|
true => Response::html(render_index()),
|
|
false => Response::redirect_302("/login")
|
|
},
|
|
(GET) ["/login"] => {
|
|
Response::html(render_login())
|
|
},
|
|
(GET) ["/manifest.json"] => {
|
|
Response::json(&json!({
|
|
"short_name": "To-do",
|
|
"start_url": "/",
|
|
"display": "standalone",
|
|
"icons": [
|
|
{
|
|
"src": "/icon.png",
|
|
"sizes": "512x512"
|
|
}
|
|
]
|
|
}))
|
|
},
|
|
(GET) ["/icon.png"] => {
|
|
Response::from_data("image/png", include_bytes!("icon.png"))
|
|
},
|
|
(POST) ["/login"] => {
|
|
let input = try_or_400!(post_input!(request, {
|
|
username: String,
|
|
password: String,
|
|
}));
|
|
if database.get_user(&input.username).unwrap() == Some(input.password) {
|
|
let token = database.create_auth_token(&input.username).unwrap();
|
|
Response::empty_204()
|
|
.with_additional_header("Set-Cookie", format!("token={}; HttpOnly; Secure; SameSite=Strict", token))
|
|
.with_additional_header("HX-Redirect", "/")
|
|
} else {
|
|
Response::text("Invalid credentials")
|
|
}
|
|
},
|
|
(GET) ["/tasks"] => match authenticated {
|
|
true => Response::html(render_ongoing_tasks(database)),
|
|
false => Response::text("Unauthorized")
|
|
.with_status_code(401)
|
|
.with_additional_header("HX-Redirect", "/login")
|
|
},
|
|
(POST) ["/add_task"] => match authenticated {
|
|
true => {
|
|
let input = try_or_400!(post_input!(request, {
|
|
task: String,
|
|
}));
|
|
database.add_task(Task::new(&input.task, TaskStatus::Ongoing, TaskPriority::Low)).unwrap();
|
|
Response::html(render_ongoing_tasks(database))
|
|
}
|
|
false => Response::text("Unauthorized")
|
|
.with_status_code(401)
|
|
.with_additional_header("HX-Redirect", "/login")
|
|
},
|
|
_ => Response::empty_404()
|
|
)
|
|
}
|
|
|
|
fn render_page(title: &str, content: Markup) -> Markup {
|
|
html! {
|
|
(DOCTYPE)
|
|
html lang="en" color-mode="user" {
|
|
head {
|
|
meta charset="utf-8";
|
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
|
title { (title) }
|
|
link rel="stylesheet" href="https://unpkg.com/mvp.css@1.17.0";
|
|
link rel="manifest" href="/manifest.json";
|
|
link rel="icon" href="/icon.png";
|
|
script src="https://unpkg.com/htmx.org@2.0.3" {}
|
|
script src="https://unpkg.com/form-persistence@2.0.6" {}
|
|
}
|
|
body {
|
|
(content)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_index() -> Markup {
|
|
render_page(
|
|
"To-do",
|
|
html! {
|
|
main {
|
|
h1 { "To-do" }
|
|
form #add-task hx-post="/add_task" hx-target="#list" hx-swap="innerHTML" {
|
|
input #task type="text" name="task" placeholder="Task" required;
|
|
input type="submit" value="Add";
|
|
}
|
|
script {
|
|
(PreEscaped(
|
|
r#"document.addEventListener("DOMContentLoaded",()=>FormPersistence.persist(document.getElementById("add-task")));document.addEventListener("htmx:afterRequest",event=>event.detail.successful&&document.getElementById("add-task").reset())"#
|
|
))
|
|
}
|
|
ul #list hx-get="/tasks" hx-trigger="load" {}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fn render_login() -> Markup {
|
|
render_page(
|
|
"Login",
|
|
html! {
|
|
main {
|
|
h1 { "Login" }
|
|
form hx-post="/login" hx-target="#message" hx-swap="innerHTML" {
|
|
input type="text" name="username" placeholder="Username" required;
|
|
input type="password" name="password" placeholder="Password" required;
|
|
input type="submit" value="Login";
|
|
}
|
|
p #message style="color: red;" {}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fn render_ongoing_tasks(database: &AppDatabase) -> Markup {
|
|
let tasks = database
|
|
.get_tasks()
|
|
.unwrap()
|
|
.into_iter()
|
|
.filter(|t| !t.is_done());
|
|
html! {
|
|
@for task in tasks {
|
|
li { (task.description()) }
|
|
}
|
|
}
|
|
}
|