diff --git a/flake.lock b/flake.lock index 44fcbcc..88cb46d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1714763106, - "narHash": "sha256-DrDHo74uTycfpAF+/qxZAMlP/Cpe04BVioJb6fdI0YY=", + "lastModified": 1720768451, + "narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9be42459999a253a9f92559b1f5b72e1b44c13d", + "rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9", "type": "github" }, "original": { diff --git a/src/cli.rs b/src/cli.rs index 18133e4..ea3a847 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,19 +27,7 @@ enum Commands { Admin(admin::Admin), } -#[derive(Error, Debug)] -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> { +pub async fn run() -> AnyResult<()> { let cli = App::parse(); observe::setup_logging(&cli.log_env_filter)?; match cli.command { diff --git a/src/cli/admin.rs b/src/cli/admin.rs index ab8bb96..d2f367d 100644 --- a/src/cli/admin.rs +++ b/src/cli/admin.rs @@ -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 sled::IVec; use uuid::Uuid; -use crate::{ - db::{self, Data}, - user::{self, User}, -}; - -use super::prelude::*; - #[derive(Args)] pub struct Admin { #[command(subcommand)] @@ -17,7 +13,7 @@ pub struct Admin { } #[derive(Subcommand)] -pub enum AdminCommands { +enum AdminCommands { #[command(subcommand)] Accounts(AccountsCommands), } @@ -29,17 +25,8 @@ enum AccountsCommands { 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 { - pub async fn run(&self) -> Result<(), RunError> { + pub async fn run(&self) -> AnyResult<()> { match &self.command { AdminCommands::Accounts(accounts) => match accounts { AccountsCommands::Create(args) => Ok(args.run().await?), @@ -53,7 +40,7 @@ impl Admin { #[derive(Args)] pub struct List {} impl List { - pub async fn run(&self) -> Result<(), CreateAccountError> { + pub async fn run(&self) -> AnyResult<()> { let db = Data::try_new()?; for entry in db.all::(User::tree())? { if let Ok((_, user)) = entry { @@ -80,7 +67,7 @@ impl Delete { user.username, self.id ); } else { - return Err(anyhow!("user not found")); + return Err(eyre!("user not found")); } Ok(()) } @@ -106,23 +93,8 @@ pub struct Create { pub initial_password: Option, } -#[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 { - pub async fn run(&self) -> Result<(), CreateAccountError> { + pub async fn run(&self) -> AnyResult<()> { // self.email_address let password: String = if let Some(password) = self.initial_password.as_ref() { password.clone() @@ -141,7 +113,7 @@ impl Create { // TODO: fail2ban? if existing_user.is_some() { // 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)?)?; diff --git a/src/cli/run.rs b/src/cli/run.rs index 31ab203..4413802 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,8 +1,6 @@ -use super::prelude::*; -use crate::{ - router::NewRouterError, - state::{NewStateError, State}, -}; +use crate::cli::prelude::*; +use crate::prelude::*; +use crate::state::State; /// Run the web application server #[derive(Args)] @@ -28,20 +26,8 @@ pub struct Run { // 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 { - pub async fn run(&self) -> Result<(), RunError> { + pub async fn run(&self) -> AnyResult<()> { let app_state = State::try_new().await?; let (router, _watchers) = crate::router::router(app_state, self.watch).await?; Ok( diff --git a/src/db.rs b/src/db.rs index b852968..385e4a2 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,29 +1,20 @@ +use crate::prelude::*; use serde::de::DeserializeOwned; use sled::{Db, IVec, Tree}; -use thiserror::Error; #[derive(Clone)] pub struct Data { db: Db, } -#[derive(Error, Debug)] -pub enum Error { - #[error("sled error: {0}")] - Sled(#[from] sled::Error), - - #[error("bincode error: {0}")] - Binccode(#[from] Box), -} - impl Data { - pub fn try_new() -> Result { + pub fn try_new() -> AnyResult { Ok(Self { db: sled::open("data/lyrs")?, }) } - pub fn delete(&self, tree_name: &str, key: K) -> Result, Error> + pub fn delete(&self, tree_name: &str, key: K) -> AnyResult> where K: AsRef<[u8]>, V: DeserializeOwned, @@ -34,7 +25,7 @@ impl Data { } } - pub fn get(&self, tree_name: &str, key: K) -> Result, Error> + pub fn get(&self, tree_name: &str, key: K) -> AnyResult> where K: AsRef<[u8]>, V: DeserializeOwned, @@ -50,14 +41,14 @@ impl Data { tree_name: &str, key: K, value: V, - ) -> Result, Error> { + ) -> AnyResult> { Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?) } pub fn all<'de, K, V>( &self, tree_name: &str, - ) -> Result>, Error> + ) -> AnyResult>> where V: DeserializeOwned, K: From, @@ -69,17 +60,17 @@ impl Data { .map(|r| match r { Ok((k, v)) => { let key = K::from(k); - match bincode::deserialize::(&v).map_err(Error::from) { + match bincode::deserialize::(&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()) } - pub fn tree(&self, name: &str) -> Result { + pub fn tree(&self, name: &str) -> AnyResult { Ok(self.db.open_tree(name)?) } } diff --git a/src/main.rs b/src/main.rs index 5472777..f41e49a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,6 @@ mod webserver; use crate::prelude::*; #[tokio::main] -async fn main() -> AnonResult<()> { +async fn main() -> AnyResult<()> { Ok(cli::run().await?) } diff --git a/src/model/display.rs b/src/model/display.rs index f2c0cbb..ba62bdf 100644 --- a/src/model/display.rs +++ b/src/model/display.rs @@ -1,8 +1,8 @@ -use std::collections::VecDeque; - -use serde::{Deserialize, Serialize}; +#![allow(dead_code)] use super::song::{Plan, Song, Verse}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; #[derive(Serialize, Deserialize, Debug)] pub struct PlaylistEntry { @@ -89,6 +89,7 @@ impl Display { } } +#[cfg(test)] mod test { use super::*; diff --git a/src/model/song.rs b/src/model/song.rs index 273d96e..859b668 100644 --- a/src/model/song.rs +++ b/src/model/song.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::{ collections::{BTreeMap, VecDeque}, str::FromStr, @@ -145,6 +147,7 @@ impl FromStr for Song { } } +#[cfg(test)] mod test { use super::*; use std::collections::VecDeque; diff --git a/src/partials.rs b/src/partials.rs index 71cf37e..c6a7042 100644 --- a/src/partials.rs +++ b/src/partials.rs @@ -1,4 +1,4 @@ -use crate::{router::ReqResult, service::accounts::AuthSession}; +use crate::service::accounts::AuthSession; use axum::response::Html; use maud::{html, Markup, PreEscaped, DOCTYPE}; @@ -27,20 +27,16 @@ pub fn foot() -> Markup { (PreEscaped("© 2024 ")) 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" } } } } -pub fn page( - title: &str, - content: Markup, - auth_session: Option, -) -> ReqResult> { +pub fn page(title: &str, content: Markup, auth_session: Option) -> Html { let current_user = auth_session.map(|s| s.user).flatten(); - Ok(Html( + Html( html! { (head(title)) 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()) }.into_string() - )) + ) } pub fn center_hero_form(title: &str, content: Markup, subform: Markup) -> Markup { diff --git a/src/prelude.rs b/src/prelude.rs index cf2a02b..af27b37 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +1,7 @@ #![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 tracing::{debug, error, event, info, instrument, span, trace, warn, Level}; diff --git a/src/router.rs b/src/router.rs index 4b765a7..590717a 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,7 +1,6 @@ use crate::partials::page; use crate::service::accounts::AuthSession; use crate::user::User; -use crate::{db, user}; use crate::{ file_watcher::FileWatcher, prelude::*, @@ -9,62 +8,51 @@ use crate::{ state::State as AppState, }; use axum::extract::State; -use axum::{ - http::StatusCode, - response::{Html, IntoResponse}, - routing::get, - Router, -}; +use axum::http::StatusCode; +use axum::response::{Html, IntoResponse}; +use axum::{routing::get, Router}; use axum_login::{login_required, AuthManagerLayerBuilder}; use maud::html; use sled::IVec; -use thiserror::Error; use tower_http::trace::TraceLayer; use tower_livereload::LiveReloadLayer; use tower_sessions::SessionManagerLayer; -#[derive(Error, Debug)] -pub enum NewRouterError { - #[error("watcher error: {0}")] - Watcher(#[from] notify::Error), +#[derive(Debug)] +#[allow(dead_code)] +pub struct WebError(Error); - #[error("database error: {0}")] - Database(#[from] db::Error), -} - -#[derive(Error, Debug)] -pub enum ReqError { - #[error("argon2 error: {0}")] - 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 { - fn into_response(self) -> axum::http::Response { - error!("webserver error: {:?}", self); - ( - StatusCode::INTERNAL_SERVER_ERROR, - // TODO: don't expose raw errors over the internet? - format!("internal server error: {}", self), - ) - .into_response() +impl From for WebError { + fn from(value: Error) -> Self { + Self(value) } } -pub type ReqResult = Result; +impl From for WebError { + fn from(value: bincode::ErrorKind) -> Self { + Self(value.into()) + } +} + +impl From> for WebError { + fn from(value: Box) -> Self { + Self(value.into()) + } +} + +impl IntoResponse for WebError { + fn into_response(self) -> axum::http::Response { + error!("webserver error: {:?}", self); + (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response() + } +} + +pub type WebResult = Result; pub async fn router( state: AppState, with_watchers: bool, -) -> Result<(Router, Vec>), NewRouterError> { +) -> AnyResult<(Router, Vec>)> { let live_reload_layer: Option = if with_watchers { Some(LiveReloadLayer::new()) } else { @@ -109,7 +97,7 @@ pub async fn router( Ok((result, watchers)) } -async fn index(auth_session: Option) -> ReqResult> { +async fn index(auth_session: Option) -> Html { page( "index", html! { @@ -124,7 +112,7 @@ async fn index(auth_session: Option) -> ReqResult> { ) } -async fn about(auth_session: Option) -> ReqResult> { +async fn about(auth_session: Option) -> Html { page( "about", html! { @@ -139,7 +127,7 @@ async fn about(auth_session: Option) -> ReqResult> { ) } -async fn dashboard(auth_session: Option) -> ReqResult> { +async fn dashboard(auth_session: Option) -> Html { page( "dashboard", html! { @@ -149,7 +137,7 @@ async fn dashboard(auth_session: Option) -> ReqResult> ) } -async fn users(State(state): State) -> ReqResult { +async fn users(State(state): State) -> WebResult { let mut s = String::new(); let mut users = state.db.all::(User::tree())?; while let Some(Ok((_, user))) = users.next() { diff --git a/src/service/accounts.rs b/src/service/accounts.rs index bcb7a9a..8375822 100644 --- a/src/service/accounts.rs +++ b/src/service/accounts.rs @@ -1,11 +1,12 @@ -use crate::router::ReqResult; +use crate::prelude::*; +use crate::router::WebResult; use crate::state::{Creds, State as AppState}; use crate::{partials::*, user::User}; -use crate::{prelude::*, user}; use axum::extract::State; use axum::http::StatusCode; use axum::response::{Html, IntoResponse, Redirect}; use axum::Form; +use color_eyre::eyre::eyre; use maud::html; use std::convert::Infallible; use tracing::instrument; @@ -93,14 +94,14 @@ async fn authenticate( async fn create_user( State(state): State, Form(creds): Form, -) -> ReqResult> { +) -> WebResult> { let user = User::try_new(&creds.username, creds.password.expose_secret())?; let existing_user: Option = state.db.get(User::tree(), &creds.username)?; // TODO: fail2ban? if existing_user.is_some() { // timing/enumeration attacks or something - return Err(user::Error::UsernameExists(Box::new(creds.username)).into()); + return Err(eyre!("username exists: {}", creds.username).into()); } state diff --git a/src/state.rs b/src/state.rs index 24d8c09..6032642 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,13 +1,8 @@ -use crate::{ - db::{self, Data}, - prelude::*, - user::{self, User}, -}; +use crate::{db::Data, prelude::*, user::User}; use axum::async_trait; use axum_login::{AuthnBackend, UserId}; use redact::Secret; use serde::Deserialize; -use thiserror::Error; #[derive(Clone)] pub struct State { @@ -15,42 +10,30 @@ pub struct State { } impl State { - pub async fn try_new() -> Result { + pub async fn try_new() -> AnyResult { Ok(Self { db: Data::try_new()?, }) } } -#[derive(Error, Debug)] -pub enum NewStateError { - #[error("database error: {0}")] - Database(#[from] db::Error), -} - #[derive(Deserialize, Debug, Clone)] pub struct Creds { pub username: String, pub password: Secret, } -#[derive(Error, Debug)] -pub enum AuthError { - #[error("user error: {0}")] - User(#[from] user::Error), - - #[error("data error: {0}")] - Db(#[from] db::Error), - - #[error("data error: {0}")] - Argon2(#[from] argon2::password_hash::Error), +#[derive(thiserror::Error, Debug)] +pub enum AuthnError { + #[error("{0}")] + Eyre(#[from] Error), } #[async_trait] impl AuthnBackend for State { type User = User; type Credentials = Creds; - type Error = AuthError; + type Error = AuthnError; async fn authenticate( &self, diff --git a/src/user.rs b/src/user.rs index 23e2071..d312a1e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -3,7 +3,6 @@ use axum_login::AuthUser; use chrono::Utc; use redact::{expose_secret, Secret}; use serde::{Deserialize, Serialize}; -use thiserror::Error; use uuid::Uuid; pub const USER_TREE: &str = "user"; @@ -19,21 +18,12 @@ pub struct User { pub registered_at: chrono::DateTime, } -#[derive(Error, Debug)] -pub enum Error { - #[error("username exists: {0}")] - UsernameExists(Box), - - #[error("username not found: {0}")] - UsernameNotFound(Box), -} - impl User { pub const fn tree() -> &'static str { USER_TREE } - pub fn try_new(username: &str, password: &str) -> Result { + pub fn try_new(username: &str, password: &str) -> AnyResult { let now = Utc::now(); Ok(Self { id: uuid::v7(now), @@ -43,8 +33,11 @@ impl User { }) } - pub fn verify(&self, password: &str) -> Result<(), argon2::password_hash::Error> { - crate::auth::verified_password(password, self.password_digest.expose_secret()) + pub fn verify(&self, password: &str) -> AnyResult<()> { + Ok(crate::auth::verified_password( + password, + self.password_digest.expose_secret(), + )?) } } diff --git a/src/uuid.rs b/src/uuid.rs index 5a87ee0..3f79554 100644 --- a/src/uuid.rs +++ b/src/uuid.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use chrono::{DateTime, TimeZone, Utc}; pub use uuid::Uuid; use uuid::{NoContext, Timestamp};