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
|
||||
/.direnv
|
||||
/static/style.css
|
||||
/data
|
||||
|
||||
# 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"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = [".", "migration"]
|
||||
|
||||
[profile.dev]
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
migration = { path = "./migration" }
|
||||
axum = { version = "0.7.5", features = ["macros", "tokio", "tracing"] }
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
color-eyre = "0.6.3"
|
||||
config = "0.14.0"
|
||||
|
@ -32,12 +28,18 @@ maud = "0.26.0"
|
|||
notify = "6.1.1"
|
||||
pathdiff = "0.2.1"
|
||||
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"
|
||||
sled = { version = "0.34.7", features = [] }
|
||||
thiserror = "1.0.60"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
||||
tower-livereload = "0.9.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uuid = { version = "1.8.0", features = ["v7"] }
|
||||
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
|
||||
#[arg(short, long, default_value = "3000")]
|
||||
pub port: u16,
|
||||
|
||||
/// The database connection string to use
|
||||
#[arg(long, default_value = "postgresql://lyrs?host=/var/run/postgresql")]
|
||||
pub database_connection_string: String,
|
||||
// The database connection string to use
|
||||
// #[arg(long, default_value = "postgresql://lyrs?host=/var/run/postgresql")]
|
||||
// pub database_connection_string: String,
|
||||
|
||||
// TODO: disallow in production?
|
||||
/// Delete all data, recreate new database, run migrations, and seed database
|
||||
#[arg(long, default_value = "false")]
|
||||
pub database_reset: bool,
|
||||
// Delete all data, recreate new database, run migrations, and seed database
|
||||
// #[arg(long, default_value = "false")]
|
||||
// pub database_reset: bool,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -43,8 +42,7 @@ pub enum RunError {
|
|||
|
||||
impl Run {
|
||||
pub async fn run(&self) -> Result<(), RunError> {
|
||||
let app_state =
|
||||
State::try_new(&self.database_connection_string, self.database_reset).await?;
|
||||
let app_state = State::try_new().await?;
|
||||
let (router, _watchers) = crate::router::router(app_state, self.watch).await?;
|
||||
Ok(
|
||||
crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port))
|
||||
|
|
47
src/db.rs
47
src/db.rs
|
@ -1,27 +1,42 @@
|
|||
use migration::MigratorTrait;
|
||||
use sea_orm::Database;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sled::{Db, IVec};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Data(DatabaseConnection);
|
||||
pub struct Data {
|
||||
db: Db,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NewDataError {
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] sea_orm::DbErr),
|
||||
pub enum Error {
|
||||
#[error("sled error: {0}")]
|
||||
Sled(#[from] sled::Error),
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub async fn try_new(
|
||||
database_connection_string: &str,
|
||||
reset: bool,
|
||||
) -> Result<Self, NewDataError> {
|
||||
let db = Database::connect(database_connection_string).await?;
|
||||
if reset {
|
||||
migration::Migrator::fresh(&db).await?;
|
||||
pub fn try_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
db: sled::open("data/lyrs")?,
|
||||
})
|
||||
}
|
||||
migration::Migrator::up(&db, None).await?;
|
||||
Ok(Self(db))
|
||||
|
||||
pub fn get<K: AsRef<[u8]>, V: From<IVec>>(
|
||||
&self,
|
||||
tree_name: &str,
|
||||
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 db;
|
||||
mod file_watcher;
|
||||
mod observe;
|
||||
mod partials;
|
||||
mod prelude;
|
||||
mod router;
|
||||
mod service;
|
||||
mod state;
|
||||
mod static_files;
|
||||
mod tailwind;
|
||||
mod user;
|
||||
mod webserver;
|
||||
|
||||
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::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
use redact::Secret;
|
||||
use serde::Deserialize;
|
||||
use maud::html;
|
||||
use thiserror::Error;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_livereload::LiveReloadLayer;
|
||||
|
@ -19,7 +23,10 @@ pub enum NewRouterError {
|
|||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ReqError {}
|
||||
pub enum ReqError {
|
||||
#[error("argon2 error: {0}")]
|
||||
Argon2(#[from] argon2::password_hash::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for ReqError {
|
||||
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 auth_service = auth::router().unwrap();
|
||||
|
||||
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("/auth", auth_service)
|
||||
.nest_service("/static", static_file_service)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state.clone());
|
||||
|
@ -75,58 +80,6 @@ pub async fn router(
|
|||
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>> {
|
||||
page("index", html! { "Index" })
|
||||
}
|
||||
|
@ -134,93 +87,3 @@ async fn index() -> ReqResult<Html<String>> {
|
|||
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");
|
||||
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)]
|
||||
pub struct State {
|
||||
db: Data,
|
||||
pub db: Data,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn try_new(
|
||||
database_connection_string: &str,
|
||||
database_reset: bool,
|
||||
) -> Result<Self, NewStateError> {
|
||||
pub async fn try_new() -> Result<Self, NewStateError> {
|
||||
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)]
|
||||
pub enum NewStateError {
|
||||
#[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