Big cleanup

This commit is contained in:
Daniel Flanagan 2024-07-14 20:15:40 -05:00
parent f1b1533268
commit d2b67d5071
15 changed files with 98 additions and 192 deletions

View file

@ -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": {

View file

@ -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 {

View file

@ -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)?)?;

View file

@ -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(

View file

@ -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)?)
} }
} }

View file

@ -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?)
} }

View file

@ -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::*;

View file

@ -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;

View file

@ -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("&copy; 2024 ")) (PreEscaped("&copy; 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 {

View file

@ -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};

View file

@ -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() {

View file

@ -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

View file

@ -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,

View file

@ -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(),
)?)
} }
} }

View file

@ -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};