Auth
This commit is contained in:
parent
36eb96416d
commit
0879a84df1
18 changed files with 440 additions and 2201 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
/target
|
/target
|
||||||
/.direnv
|
/.direnv
|
||||||
/static/style.css
|
/static/style.css
|
||||||
|
/data
|
||||||
|
|
||||||
# Added by cargo
|
# Added by cargo
|
||||||
#
|
#
|
||||||
|
|
2127
Cargo.lock
generated
2127
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -3,9 +3,6 @@ name = "lyrs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace]
|
|
||||||
members = [".", "migration"]
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
|
@ -22,8 +19,7 @@ panic = "abort"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
migration = { path = "./migration" }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
axum = { version = "0.7.5", features = ["macros", "tokio", "tracing"] }
|
|
||||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
|
@ -32,12 +28,18 @@ maud = "0.26.0"
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
pathdiff = "0.2.1"
|
pathdiff = "0.2.1"
|
||||||
redact = { version = "0.1.10", features = ["serde"] }
|
redact = { version = "0.1.10", features = ["serde"] }
|
||||||
sea-orm = { version = "0.12.15", features = ["sqlx-postgres", "runtime-tokio-rustls", "debug-print"] }
|
|
||||||
serde = "1.0.201"
|
serde = "1.0.201"
|
||||||
|
sled = { version = "0.34.7", features = [] }
|
||||||
thiserror = "1.0.60"
|
thiserror = "1.0.60"
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
||||||
tower-livereload = "0.9.2"
|
tower-livereload = "0.9.2"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
uuid = { version = "1.8.0", features = ["v7"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|
||||||
|
[dependencies.axum]
|
||||||
|
version = "0.7.5"
|
||||||
|
features = ["macros", "tokio", "tracing"]
|
||||||
|
|
||||||
|
|
9
readme.md
Normal file
9
readme.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# lyricscreen (lyrs)
|
||||||
|
|
||||||
|
Manage lyrics and live displays for them at shows.
|
||||||
|
|
||||||
|
# Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
watchexec -e rs,toml -r 'cargo run -- --log-env-filter trace,sled=debug run --watch'
|
||||||
|
```
|
22
src/auth.rs
Normal file
22
src/auth.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use argon2::PasswordHasher;
|
||||||
|
use argon2::PasswordVerifier;
|
||||||
|
use argon2::{password_hash::SaltString, Argon2, PasswordHash};
|
||||||
|
|
||||||
|
pub fn password_digest<P: AsRef<[u8]>>(
|
||||||
|
password: P,
|
||||||
|
) -> Result<String, argon2::password_hash::Error> {
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
|
||||||
|
Ok(argon2.hash_password(password.as_ref(), &salt)?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verified_password<S: AsRef<str>, P: AsRef<[u8]>>(
|
||||||
|
password: P,
|
||||||
|
password_digest: S,
|
||||||
|
) -> Result<(), argon2::password_hash::Error> {
|
||||||
|
Argon2::default().verify_password(
|
||||||
|
password.as_ref(),
|
||||||
|
&PasswordHash::new(password_digest.as_ref())?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,15 +18,14 @@ pub struct Run {
|
||||||
/// The port to bind to
|
/// The port to bind to
|
||||||
#[arg(short, long, default_value = "3000")]
|
#[arg(short, long, default_value = "3000")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
// The database connection string to use
|
||||||
/// The database connection string to use
|
// #[arg(long, default_value = "postgresql://lyrs?host=/var/run/postgresql")]
|
||||||
#[arg(long, default_value = "postgresql://lyrs?host=/var/run/postgresql")]
|
// pub database_connection_string: String,
|
||||||
pub database_connection_string: String,
|
|
||||||
|
|
||||||
// TODO: disallow in production?
|
// TODO: disallow in production?
|
||||||
/// Delete all data, recreate new database, run migrations, and seed database
|
// Delete all data, recreate new database, run migrations, and seed database
|
||||||
#[arg(long, default_value = "false")]
|
// #[arg(long, default_value = "false")]
|
||||||
pub database_reset: bool,
|
// pub database_reset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -43,8 +42,7 @@ pub enum RunError {
|
||||||
|
|
||||||
impl Run {
|
impl Run {
|
||||||
pub async fn run(&self) -> Result<(), RunError> {
|
pub async fn run(&self) -> Result<(), RunError> {
|
||||||
let app_state =
|
let app_state = State::try_new().await?;
|
||||||
State::try_new(&self.database_connection_string, self.database_reset).await?;
|
|
||||||
let (router, _watchers) = crate::router::router(app_state, self.watch).await?;
|
let (router, _watchers) = crate::router::router(app_state, self.watch).await?;
|
||||||
Ok(
|
Ok(
|
||||||
crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port))
|
crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port))
|
||||||
|
|
49
src/db.rs
49
src/db.rs
|
@ -1,27 +1,42 @@
|
||||||
use migration::MigratorTrait;
|
use sled::{Db, IVec};
|
||||||
use sea_orm::Database;
|
|
||||||
use sea_orm::DatabaseConnection;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Data(DatabaseConnection);
|
pub struct Data {
|
||||||
|
db: Db,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum NewDataError {
|
pub enum Error {
|
||||||
#[error("database error: {0}")]
|
#[error("sled error: {0}")]
|
||||||
Database(#[from] sea_orm::DbErr),
|
Sled(#[from] sled::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Data {
|
impl Data {
|
||||||
pub async fn try_new(
|
pub fn try_new() -> Result<Self, Error> {
|
||||||
database_connection_string: &str,
|
Ok(Self {
|
||||||
reset: bool,
|
db: sled::open("data/lyrs")?,
|
||||||
) -> Result<Self, NewDataError> {
|
})
|
||||||
let db = Database::connect(database_connection_string).await?;
|
}
|
||||||
if reset {
|
|
||||||
migration::Migrator::fresh(&db).await?;
|
pub fn get<K: AsRef<[u8]>, V: From<IVec>>(
|
||||||
}
|
&self,
|
||||||
migration::Migrator::up(&db, None).await?;
|
tree_name: &str,
|
||||||
Ok(Self(db))
|
key: K,
|
||||||
|
) -> Result<Option<V>, Error> {
|
||||||
|
Ok(self
|
||||||
|
.db
|
||||||
|
.open_tree(tree_name)?
|
||||||
|
.get(key.as_ref())?
|
||||||
|
.map(V::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<K: AsRef<[u8]>, V: Into<IVec>>(
|
||||||
|
&self,
|
||||||
|
tree_name: &str,
|
||||||
|
key: K,
|
||||||
|
value: V,
|
||||||
|
) -> Result<Option<IVec>, Error> {
|
||||||
|
Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
mod auth;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod db;
|
mod db;
|
||||||
mod file_watcher;
|
mod file_watcher;
|
||||||
mod observe;
|
mod observe;
|
||||||
|
mod partials;
|
||||||
mod prelude;
|
mod prelude;
|
||||||
mod router;
|
mod router;
|
||||||
|
mod service;
|
||||||
mod state;
|
mod state;
|
||||||
mod static_files;
|
|
||||||
mod tailwind;
|
mod tailwind;
|
||||||
|
mod user;
|
||||||
mod webserver;
|
mod webserver;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
79
src/partials.rs
Normal file
79
src/partials.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::router::ReqResult;
|
||||||
|
use axum::response::Html;
|
||||||
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
|
pub fn stylesheet(url: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
link rel="stylesheet" type="text/css" href=(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 {}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 ❤️")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page(title: &str, content: Markup) -> ReqResult<Html<String>> {
|
||||||
|
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="/auth/login" { "Login" }
|
||||||
|
a class="flex items-center p-2 hover:bg-mauve hover:text-bg" href="/auth/register" { "Register" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main class="flex flex-col flex-1 relative overflow-x-scroll bg-mantle" {
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(foot())
|
||||||
|
}.into_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labelled_input(label: &str, input: Markup) -> Markup {
|
||||||
|
html! {
|
||||||
|
label class="flex flex-col" {
|
||||||
|
(label)
|
||||||
|
(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
169
src/router.rs
169
src/router.rs
|
@ -1,13 +1,17 @@
|
||||||
use crate::{file_watcher::FileWatcher, prelude::*, state::State as AppState, static_files};
|
use crate::partials::page;
|
||||||
|
use crate::{
|
||||||
|
file_watcher::FileWatcher,
|
||||||
|
prelude::*,
|
||||||
|
service::{auth, static_files},
|
||||||
|
state::State as AppState,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
routing::{get, post},
|
routing::get,
|
||||||
Form, Router,
|
Router,
|
||||||
};
|
};
|
||||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
use maud::html;
|
||||||
use redact::Secret;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tower_livereload::LiveReloadLayer;
|
use tower_livereload::LiveReloadLayer;
|
||||||
|
@ -19,7 +23,10 @@ pub enum NewRouterError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ReqError {}
|
pub enum ReqError {
|
||||||
|
#[error("argon2 error: {0}")]
|
||||||
|
Argon2(#[from] argon2::password_hash::Error),
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for ReqError {
|
impl IntoResponse for ReqError {
|
||||||
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
||||||
|
@ -54,14 +61,12 @@ pub async fn router(
|
||||||
};
|
};
|
||||||
|
|
||||||
let (static_file_service, static_file_watcher) = static_files::router(orl())?;
|
let (static_file_service, static_file_watcher) = static_files::router(orl())?;
|
||||||
|
let auth_service = auth::router().unwrap();
|
||||||
|
|
||||||
let mut result = Router::new()
|
let mut result = Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/about", get(about))
|
.route("/about", get(about))
|
||||||
.route("/login", get(login))
|
.nest_service("/auth", auth_service)
|
||||||
.route("/login", post(authenticate))
|
|
||||||
.route("/register", get(register))
|
|
||||||
.route("/register", post(create_user))
|
|
||||||
.nest_service("/static", static_file_service)
|
.nest_service("/static", static_file_service)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
@ -75,58 +80,6 @@ pub async fn router(
|
||||||
Ok((result, watchers))
|
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<Html<String>> {
|
|
||||||
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<Html<String>> {
|
async fn index() -> ReqResult<Html<String>> {
|
||||||
page("index", html! { "Index" })
|
page("index", html! { "Index" })
|
||||||
}
|
}
|
||||||
|
@ -134,93 +87,3 @@ async fn index() -> ReqResult<Html<String>> {
|
||||||
async fn about() -> ReqResult<Html<String>> {
|
async fn about() -> ReqResult<Html<String>> {
|
||||||
page("index", html! { "About" })
|
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");
|
|
||||||
Ok(Html(
|
|
||||||
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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
2
src/service.rs
Normal file
2
src/service.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub mod static_files;
|
0
src/service/account.rs
Normal file
0
src/service/account.rs
Normal file
95
src/service/auth.rs
Normal file
95
src/service/auth.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::router::ReqResult;
|
||||||
|
use crate::state::State as AppState;
|
||||||
|
use crate::{partials::*, user::User};
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::response::Html;
|
||||||
|
use axum::Form;
|
||||||
|
use maud::html;
|
||||||
|
use redact::Secret;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub fn router(state: AppState) -> Result<Router, Infallible> {
|
||||||
|
Ok(Router::new()
|
||||||
|
.route("/login", get(login))
|
||||||
|
.route("/login", post(authenticate))
|
||||||
|
.route("/register", get(register))
|
||||||
|
.route("/register", post(create_user))
|
||||||
|
.with_state(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
Ok(Html(
|
||||||
|
html! {
|
||||||
|
"no"
|
||||||
|
}
|
||||||
|
.into_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
|
async fn create_user(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(creds): Form<Creds>,
|
||||||
|
) -> ReqResult<Html<String>> {
|
||||||
|
let user = User::try_new(&creds.username, creds.password.expose_secret())?;
|
||||||
|
Ok(Html(
|
||||||
|
html! {
|
||||||
|
({user.username}) " has been registered"
|
||||||
|
}
|
||||||
|
.into_string(),
|
||||||
|
))
|
||||||
|
}
|
4
src/service/data/lyrs/conf
Normal file
4
src/service/data/lyrs/conf
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
segment_size: 524288
|
||||||
|
use_compression: false
|
||||||
|
version: 0.34
|
||||||
|
vQÁ
|
BIN
src/service/data/lyrs/db
Normal file
BIN
src/service/data/lyrs/db
Normal file
Binary file not shown.
11
src/state.rs
11
src/state.rs
|
@ -6,16 +6,13 @@ use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
db: Data,
|
pub db: Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub async fn try_new(
|
pub async fn try_new() -> Result<Self, NewStateError> {
|
||||||
database_connection_string: &str,
|
|
||||||
database_reset: bool,
|
|
||||||
) -> Result<Self, NewStateError> {
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: Data::try_new(database_connection_string, database_reset).await?,
|
db: Data::try_new()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +20,5 @@ impl State {
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum NewStateError {
|
pub enum NewStateError {
|
||||||
#[error("database error: {0}")]
|
#[error("database error: {0}")]
|
||||||
Database(#[from] db::NewDataError),
|
Database(#[from] db::Error),
|
||||||
}
|
}
|
||||||
|
|
38
src/user.rs
Normal file
38
src/user.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use redact::Secret;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub const USER_TREE: &str = "user";
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub username: String,
|
||||||
|
password_digest: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("username exists: {0}")]
|
||||||
|
UsernameExists(Box<String>),
|
||||||
|
|
||||||
|
#[error("username not found: {0}")]
|
||||||
|
UsernameNotFound(Box<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub const fn tree() -> &'static str {
|
||||||
|
USER_TREE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_new(username: &str, password: &str) -> Result<Self, argon2::password_hash::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
username: username.to_owned(),
|
||||||
|
password_digest: Secret::new(crate::auth::password_digest(password)?.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, password: &str) -> Result<(), argon2::password_hash::Error> {
|
||||||
|
crate::auth::verified_password(password, self.password_digest.expose_secret())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue