Big cleanup
This commit is contained in:
parent
f1b1533268
commit
d2b67d5071
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1714763106,
|
"lastModified": 1720768451,
|
||||||
"narHash": "sha256-DrDHo74uTycfpAF+/qxZAMlP/Cpe04BVioJb6fdI0YY=",
|
"narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e9be42459999a253a9f92559b1f5b72e1b44c13d",
|
"rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
14
src/cli.rs
14
src/cli.rs
|
@ -27,19 +27,7 @@ enum Commands {
|
||||||
Admin(admin::Admin),
|
Admin(admin::Admin),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
pub async fn run() -> AnyResult<()> {
|
||||||
pub enum RunError {
|
|
||||||
#[error("run error: {0}")]
|
|
||||||
Run(#[from] run::RunError),
|
|
||||||
|
|
||||||
#[error("admin error: {0}")]
|
|
||||||
Admin(#[from] admin::RunError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Eyre(#[from] color_eyre::Report),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run() -> Result<(), RunError> {
|
|
||||||
let cli = App::parse();
|
let cli = App::parse();
|
||||||
observe::setup_logging(&cli.log_env_filter)?;
|
observe::setup_logging(&cli.log_env_filter)?;
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
use color_eyre::eyre::anyhow;
|
use super::prelude::*;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::{db::Data, user::User};
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
use sled::IVec;
|
use sled::IVec;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::{self, Data},
|
|
||||||
user::{self, User},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct Admin {
|
pub struct Admin {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
@ -17,7 +13,7 @@ pub struct Admin {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum AdminCommands {
|
enum AdminCommands {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Accounts(AccountsCommands),
|
Accounts(AccountsCommands),
|
||||||
}
|
}
|
||||||
|
@ -29,17 +25,8 @@ enum AccountsCommands {
|
||||||
List(List),
|
List(List),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum RunError {
|
|
||||||
#[error("{0}")]
|
|
||||||
Eyre(#[from] color_eyre::Report),
|
|
||||||
|
|
||||||
#[error("create account error: {0}")]
|
|
||||||
CreateAccount(#[from] CreateAccountError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Admin {
|
impl Admin {
|
||||||
pub async fn run(&self) -> Result<(), RunError> {
|
pub async fn run(&self) -> AnyResult<()> {
|
||||||
match &self.command {
|
match &self.command {
|
||||||
AdminCommands::Accounts(accounts) => match accounts {
|
AdminCommands::Accounts(accounts) => match accounts {
|
||||||
AccountsCommands::Create(args) => Ok(args.run().await?),
|
AccountsCommands::Create(args) => Ok(args.run().await?),
|
||||||
|
@ -53,7 +40,7 @@ impl Admin {
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct List {}
|
pub struct List {}
|
||||||
impl List {
|
impl List {
|
||||||
pub async fn run(&self) -> Result<(), CreateAccountError> {
|
pub async fn run(&self) -> AnyResult<()> {
|
||||||
let db = Data::try_new()?;
|
let db = Data::try_new()?;
|
||||||
for entry in db.all::<IVec, User>(User::tree())? {
|
for entry in db.all::<IVec, User>(User::tree())? {
|
||||||
if let Ok((_, user)) = entry {
|
if let Ok((_, user)) = entry {
|
||||||
|
@ -80,7 +67,7 @@ impl Delete {
|
||||||
user.username, self.id
|
user.username, self.id
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("user not found"));
|
return Err(eyre!("user not found"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -106,23 +93,8 @@ pub struct Create {
|
||||||
pub initial_password: Option<String>,
|
pub initial_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum CreateAccountError {
|
|
||||||
#[error("password hash error: {0}")]
|
|
||||||
PasswordHash(#[from] argon2::password_hash::Error),
|
|
||||||
|
|
||||||
#[error("data error: {0}")]
|
|
||||||
Data(#[from] db::Error),
|
|
||||||
|
|
||||||
#[error("user error: {0}")]
|
|
||||||
User(#[from] user::Error),
|
|
||||||
|
|
||||||
#[error("bincode error: {0}")]
|
|
||||||
Bincode(#[from] bincode::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Create {
|
impl Create {
|
||||||
pub async fn run(&self) -> Result<(), CreateAccountError> {
|
pub async fn run(&self) -> AnyResult<()> {
|
||||||
// self.email_address
|
// self.email_address
|
||||||
let password: String = if let Some(password) = self.initial_password.as_ref() {
|
let password: String = if let Some(password) = self.initial_password.as_ref() {
|
||||||
password.clone()
|
password.clone()
|
||||||
|
@ -141,7 +113,7 @@ impl Create {
|
||||||
// TODO: fail2ban?
|
// TODO: fail2ban?
|
||||||
if existing_user.is_some() {
|
if existing_user.is_some() {
|
||||||
// timing/enumeration attacks or something
|
// timing/enumeration attacks or something
|
||||||
return Err(user::Error::UsernameExists(Box::new(self.username.clone())).into());
|
return Err(eyre!("username already exists: {}", self.username));
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insert(User::tree(), &user.username, bincode::serialize(&user)?)?;
|
db.insert(User::tree(), &user.username, bincode::serialize(&user)?)?;
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use super::prelude::*;
|
use crate::cli::prelude::*;
|
||||||
use crate::{
|
use crate::prelude::*;
|
||||||
router::NewRouterError,
|
use crate::state::State;
|
||||||
state::{NewStateError, State},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Run the web application server
|
/// Run the web application server
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
@ -28,20 +26,8 @@ pub struct Run {
|
||||||
// pub database_reset: bool,
|
// pub database_reset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum RunError {
|
|
||||||
#[error("router error: {0}")]
|
|
||||||
Router(#[from] NewRouterError),
|
|
||||||
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("new state error: {0}")]
|
|
||||||
State(#[from] NewStateError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Run {
|
impl Run {
|
||||||
pub async fn run(&self) -> Result<(), RunError> {
|
pub async fn run(&self) -> AnyResult<()> {
|
||||||
let app_state = State::try_new().await?;
|
let app_state = State::try_new().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(
|
||||||
|
|
29
src/db.rs
29
src/db.rs
|
@ -1,29 +1,20 @@
|
||||||
|
use crate::prelude::*;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use sled::{Db, IVec, Tree};
|
use sled::{Db, IVec, Tree};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
db: Db,
|
db: Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("sled error: {0}")]
|
|
||||||
Sled(#[from] sled::Error),
|
|
||||||
|
|
||||||
#[error("bincode error: {0}")]
|
|
||||||
Binccode(#[from] Box<bincode::ErrorKind>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Data {
|
impl Data {
|
||||||
pub fn try_new() -> Result<Self, Error> {
|
pub fn try_new() -> AnyResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: sled::open("data/lyrs")?,
|
db: sled::open("data/lyrs")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
|
pub fn delete<K, V>(&self, tree_name: &str, key: K) -> AnyResult<Option<V>>
|
||||||
where
|
where
|
||||||
K: AsRef<[u8]>,
|
K: AsRef<[u8]>,
|
||||||
V: DeserializeOwned,
|
V: DeserializeOwned,
|
||||||
|
@ -34,7 +25,7 @@ impl Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
|
pub fn get<K, V>(&self, tree_name: &str, key: K) -> AnyResult<Option<V>>
|
||||||
where
|
where
|
||||||
K: AsRef<[u8]>,
|
K: AsRef<[u8]>,
|
||||||
V: DeserializeOwned,
|
V: DeserializeOwned,
|
||||||
|
@ -50,14 +41,14 @@ impl Data {
|
||||||
tree_name: &str,
|
tree_name: &str,
|
||||||
key: K,
|
key: K,
|
||||||
value: V,
|
value: V,
|
||||||
) -> Result<Option<IVec>, Error> {
|
) -> AnyResult<Option<IVec>> {
|
||||||
Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?)
|
Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all<'de, K, V>(
|
pub fn all<'de, K, V>(
|
||||||
&self,
|
&self,
|
||||||
tree_name: &str,
|
tree_name: &str,
|
||||||
) -> Result<impl Iterator<Item = Result<(K, V), Error>>, Error>
|
) -> AnyResult<impl Iterator<Item = AnyResult<(K, V)>>>
|
||||||
where
|
where
|
||||||
V: DeserializeOwned,
|
V: DeserializeOwned,
|
||||||
K: From<IVec>,
|
K: From<IVec>,
|
||||||
|
@ -69,17 +60,17 @@ impl Data {
|
||||||
.map(|r| match r {
|
.map(|r| match r {
|
||||||
Ok((k, v)) => {
|
Ok((k, v)) => {
|
||||||
let key = K::from(k);
|
let key = K::from(k);
|
||||||
match bincode::deserialize::<V>(&v).map_err(Error::from) {
|
match bincode::deserialize::<V>(&v) {
|
||||||
Ok(v) => Ok((key, v)),
|
Ok(v) => Ok((key, v)),
|
||||||
Err(err) => Err(Error::from(err)),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(Error::from(err)),
|
Err(err) => Err(err.into()),
|
||||||
})
|
})
|
||||||
.into_iter())
|
.into_iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tree(&self, name: &str) -> Result<Tree, Error> {
|
pub fn tree(&self, name: &str) -> AnyResult<Tree> {
|
||||||
Ok(self.db.open_tree(name)?)
|
Ok(self.db.open_tree(name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,6 @@ mod webserver;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> AnonResult<()> {
|
async fn main() -> AnyResult<()> {
|
||||||
Ok(cli::run().await?)
|
Ok(cli::run().await?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::VecDeque;
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::song::{Plan, Song, Verse};
|
use super::song::{Plan, Song, Verse};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct PlaylistEntry {
|
pub struct PlaylistEntry {
|
||||||
|
@ -89,6 +89,7 @@ impl Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, VecDeque},
|
collections::{BTreeMap, VecDeque},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
@ -145,6 +147,7 @@ impl FromStr for Song {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{router::ReqResult, service::accounts::AuthSession};
|
use crate::service::accounts::AuthSession;
|
||||||
use axum::response::Html;
|
use axum::response::Html;
|
||||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
|
@ -27,20 +27,16 @@ pub fn foot() -> Markup {
|
||||||
(PreEscaped("© 2024 "))
|
(PreEscaped("© 2024 "))
|
||||||
a class="underline text-mauve" href="https://lyte.dev" { "lytedev" }
|
a class="underline text-mauve" href="https://lyte.dev" { "lytedev" }
|
||||||
}
|
}
|
||||||
section .ml-auto {("Made with ❤️")}
|
section .ml-auto {"Made with ❤️"}
|
||||||
" "
|
" "
|
||||||
a .underline.text-mauve href="/about" { "About" }
|
a .underline.text-mauve href="/about" { "About" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page(
|
pub fn page(title: &str, content: Markup, auth_session: Option<AuthSession>) -> Html<String> {
|
||||||
title: &str,
|
|
||||||
content: Markup,
|
|
||||||
auth_session: Option<AuthSession>,
|
|
||||||
) -> ReqResult<Html<String>> {
|
|
||||||
let current_user = auth_session.map(|s| s.user).flatten();
|
let current_user = auth_session.map(|s| s.user).flatten();
|
||||||
Ok(Html(
|
Html(
|
||||||
html! {
|
html! {
|
||||||
(head(title))
|
(head(title))
|
||||||
body hx-boost="true" class="bg-bg text-text min-h-lvh flex flex-col font-sans overflow-x-hidden" {
|
body hx-boost="true" class="bg-bg text-text min-h-lvh flex flex-col font-sans overflow-x-hidden" {
|
||||||
|
@ -60,7 +56,7 @@ pub fn page(
|
||||||
}
|
}
|
||||||
(foot())
|
(foot())
|
||||||
}.into_string()
|
}.into_string()
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_hero_form(title: &str, content: Markup, subform: Markup) -> Markup {
|
pub fn center_hero_form(title: &str, content: Markup, subform: Markup) -> Markup {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
pub use color_eyre::eyre::Result as AnonResult;
|
pub use color_eyre::eyre::eyre;
|
||||||
|
pub use color_eyre::eyre::Error;
|
||||||
|
pub use color_eyre::eyre::Result as AnyResult;
|
||||||
pub use std::result::Result;
|
pub use std::result::Result;
|
||||||
pub use tracing::{debug, error, event, info, instrument, span, trace, warn, Level};
|
pub use tracing::{debug, error, event, info, instrument, span, trace, warn, Level};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::partials::page;
|
use crate::partials::page;
|
||||||
use crate::service::accounts::AuthSession;
|
use crate::service::accounts::AuthSession;
|
||||||
use crate::user::User;
|
use crate::user::User;
|
||||||
use crate::{db, user};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file_watcher::FileWatcher,
|
file_watcher::FileWatcher,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
@ -9,62 +8,51 @@ use crate::{
|
||||||
state::State as AppState,
|
state::State as AppState,
|
||||||
};
|
};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
use axum::response::{Html, IntoResponse};
|
||||||
response::{Html, IntoResponse},
|
use axum::{routing::get, Router};
|
||||||
routing::get,
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use axum_login::{login_required, AuthManagerLayerBuilder};
|
use axum_login::{login_required, AuthManagerLayerBuilder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use sled::IVec;
|
use sled::IVec;
|
||||||
use thiserror::Error;
|
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tower_livereload::LiveReloadLayer;
|
use tower_livereload::LiveReloadLayer;
|
||||||
use tower_sessions::SessionManagerLayer;
|
use tower_sessions::SessionManagerLayer;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum NewRouterError {
|
#[allow(dead_code)]
|
||||||
#[error("watcher error: {0}")]
|
pub struct WebError(Error);
|
||||||
Watcher(#[from] notify::Error),
|
|
||||||
|
|
||||||
#[error("database error: {0}")]
|
impl From<Error> for WebError {
|
||||||
Database(#[from] db::Error),
|
fn from(value: Error) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
impl From<bincode::ErrorKind> for WebError {
|
||||||
pub enum ReqError {
|
fn from(value: bincode::ErrorKind) -> Self {
|
||||||
#[error("argon2 error: {0}")]
|
Self(value.into())
|
||||||
Argon2(#[from] argon2::password_hash::Error),
|
}
|
||||||
|
|
||||||
#[error("bincode error: {0}")]
|
|
||||||
Bincode(#[from] bincode::Error),
|
|
||||||
|
|
||||||
#[error("database error: {0}")]
|
|
||||||
Database(#[from] db::Error),
|
|
||||||
|
|
||||||
#[error("user error: {0}")]
|
|
||||||
User(#[from] user::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for ReqError {
|
impl From<Box<bincode::ErrorKind>> for WebError {
|
||||||
|
fn from(value: Box<bincode::ErrorKind>) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for WebError {
|
||||||
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
fn into_response(self) -> axum::http::Response<axum::body::Body> {
|
||||||
error!("webserver error: {:?}", self);
|
error!("webserver error: {:?}", self);
|
||||||
(
|
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
|
||||||
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 type WebResult<T> = Result<T, WebError>;
|
||||||
|
|
||||||
pub async fn router(
|
pub async fn router(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
with_watchers: bool,
|
with_watchers: bool,
|
||||||
) -> Result<(Router, Vec<Option<FileWatcher>>), NewRouterError> {
|
) -> AnyResult<(Router, Vec<Option<FileWatcher>>)> {
|
||||||
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
|
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
|
||||||
Some(LiveReloadLayer::new())
|
Some(LiveReloadLayer::new())
|
||||||
} else {
|
} else {
|
||||||
|
@ -109,7 +97,7 @@ pub async fn router(
|
||||||
Ok((result, watchers))
|
Ok((result, watchers))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
|
async fn index(auth_session: Option<AuthSession>) -> Html<String> {
|
||||||
page(
|
page(
|
||||||
"index",
|
"index",
|
||||||
html! {
|
html! {
|
||||||
|
@ -124,7 +112,7 @@ async fn index(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn about(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
|
async fn about(auth_session: Option<AuthSession>) -> Html<String> {
|
||||||
page(
|
page(
|
||||||
"about",
|
"about",
|
||||||
html! {
|
html! {
|
||||||
|
@ -139,7 +127,7 @@ async fn about(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dashboard(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
|
async fn dashboard(auth_session: Option<AuthSession>) -> Html<String> {
|
||||||
page(
|
page(
|
||||||
"dashboard",
|
"dashboard",
|
||||||
html! {
|
html! {
|
||||||
|
@ -149,7 +137,7 @@ async fn dashboard(auth_session: Option<AuthSession>) -> ReqResult<Html<String>>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn users(State(state): State<AppState>) -> ReqResult<String> {
|
async fn users(State(state): State<AppState>) -> WebResult<String> {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let mut users = state.db.all::<IVec, User>(User::tree())?;
|
let mut users = state.db.all::<IVec, User>(User::tree())?;
|
||||||
while let Some(Ok((_, user))) = users.next() {
|
while let Some(Ok((_, user))) = users.next() {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::router::ReqResult;
|
use crate::prelude::*;
|
||||||
|
use crate::router::WebResult;
|
||||||
use crate::state::{Creds, State as AppState};
|
use crate::state::{Creds, State as AppState};
|
||||||
use crate::{partials::*, user::User};
|
use crate::{partials::*, user::User};
|
||||||
use crate::{prelude::*, user};
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{Html, IntoResponse, Redirect};
|
use axum::response::{Html, IntoResponse, Redirect};
|
||||||
use axum::Form;
|
use axum::Form;
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
@ -93,14 +94,14 @@ async fn authenticate(
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(creds): Form<Creds>,
|
Form(creds): Form<Creds>,
|
||||||
) -> ReqResult<Html<String>> {
|
) -> WebResult<Html<String>> {
|
||||||
let user = User::try_new(&creds.username, creds.password.expose_secret())?;
|
let user = User::try_new(&creds.username, creds.password.expose_secret())?;
|
||||||
let existing_user: Option<User> = state.db.get(User::tree(), &creds.username)?;
|
let existing_user: Option<User> = state.db.get(User::tree(), &creds.username)?;
|
||||||
|
|
||||||
// TODO: fail2ban?
|
// TODO: fail2ban?
|
||||||
if existing_user.is_some() {
|
if existing_user.is_some() {
|
||||||
// timing/enumeration attacks or something
|
// timing/enumeration attacks or something
|
||||||
return Err(user::Error::UsernameExists(Box::new(creds.username)).into());
|
return Err(eyre!("username exists: {}", creds.username).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|
31
src/state.rs
31
src/state.rs
|
@ -1,13 +1,8 @@
|
||||||
use crate::{
|
use crate::{db::Data, prelude::*, user::User};
|
||||||
db::{self, Data},
|
|
||||||
prelude::*,
|
|
||||||
user::{self, User},
|
|
||||||
};
|
|
||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use axum_login::{AuthnBackend, UserId};
|
use axum_login::{AuthnBackend, UserId};
|
||||||
use redact::Secret;
|
use redact::Secret;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -15,42 +10,30 @@ pub struct State {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub async fn try_new() -> Result<Self, NewStateError> {
|
pub async fn try_new() -> AnyResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: Data::try_new()?,
|
db: Data::try_new()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum NewStateError {
|
|
||||||
#[error("database error: {0}")]
|
|
||||||
Database(#[from] db::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct Creds {
|
pub struct Creds {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: Secret<String>,
|
pub password: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum AuthError {
|
pub enum AuthnError {
|
||||||
#[error("user error: {0}")]
|
#[error("{0}")]
|
||||||
User(#[from] user::Error),
|
Eyre(#[from] Error),
|
||||||
|
|
||||||
#[error("data error: {0}")]
|
|
||||||
Db(#[from] db::Error),
|
|
||||||
|
|
||||||
#[error("data error: {0}")]
|
|
||||||
Argon2(#[from] argon2::password_hash::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AuthnBackend for State {
|
impl AuthnBackend for State {
|
||||||
type User = User;
|
type User = User;
|
||||||
type Credentials = Creds;
|
type Credentials = Creds;
|
||||||
type Error = AuthError;
|
type Error = AuthnError;
|
||||||
|
|
||||||
async fn authenticate(
|
async fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
|
|
19
src/user.rs
19
src/user.rs
|
@ -3,7 +3,6 @@ use axum_login::AuthUser;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use redact::{expose_secret, Secret};
|
use redact::{expose_secret, Secret};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub const USER_TREE: &str = "user";
|
pub const USER_TREE: &str = "user";
|
||||||
|
@ -19,21 +18,12 @@ pub struct User {
|
||||||
pub registered_at: chrono::DateTime<Utc>,
|
pub registered_at: chrono::DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("username exists: {0}")]
|
|
||||||
UsernameExists(Box<String>),
|
|
||||||
|
|
||||||
#[error("username not found: {0}")]
|
|
||||||
UsernameNotFound(Box<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub const fn tree() -> &'static str {
|
pub const fn tree() -> &'static str {
|
||||||
USER_TREE
|
USER_TREE
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_new(username: &str, password: &str) -> Result<Self, argon2::password_hash::Error> {
|
pub fn try_new(username: &str, password: &str) -> AnyResult<Self> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: uuid::v7(now),
|
id: uuid::v7(now),
|
||||||
|
@ -43,8 +33,11 @@ impl User {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify(&self, password: &str) -> Result<(), argon2::password_hash::Error> {
|
pub fn verify(&self, password: &str) -> AnyResult<()> {
|
||||||
crate::auth::verified_password(password, self.password_digest.expose_secret())
|
Ok(crate::auth::verified_password(
|
||||||
|
password,
|
||||||
|
self.password_digest.expose_secret(),
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
pub use uuid::Uuid;
|
pub use uuid::Uuid;
|
||||||
use uuid::{NoContext, Timestamp};
|
use uuid::{NoContext, Timestamp};
|
||||||
|
|
Loading…
Reference in a new issue