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

View file

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

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 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::<IVec, User>(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<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 {
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)?)?;

View file

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

View file

@ -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<bincode::ErrorKind>),
}
impl Data {
pub fn try_new() -> Result<Self, Error> {
pub fn try_new() -> AnyResult<Self> {
Ok(Self {
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
K: AsRef<[u8]>,
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
K: AsRef<[u8]>,
V: DeserializeOwned,
@ -50,14 +41,14 @@ impl Data {
tree_name: &str,
key: K,
value: V,
) -> Result<Option<IVec>, Error> {
) -> AnyResult<Option<IVec>> {
Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?)
}
pub fn all<'de, K, V>(
&self,
tree_name: &str,
) -> Result<impl Iterator<Item = Result<(K, V), Error>>, Error>
) -> AnyResult<impl Iterator<Item = AnyResult<(K, V)>>>
where
V: DeserializeOwned,
K: From<IVec>,
@ -69,17 +60,17 @@ impl Data {
.map(|r| match r {
Ok((k, v)) => {
let key = K::from(k);
match bincode::deserialize::<V>(&v).map_err(Error::from) {
match bincode::deserialize::<V>(&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<Tree, Error> {
pub fn tree(&self, name: &str) -> AnyResult<Tree> {
Ok(self.db.open_tree(name)?)
}
}

View file

@ -17,6 +17,6 @@ mod webserver;
use crate::prelude::*;
#[tokio::main]
async fn main() -> AnonResult<()> {
async fn main() -> AnyResult<()> {
Ok(cli::run().await?)
}

View file

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

View file

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

View file

@ -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("&copy; 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<AuthSession>,
) -> ReqResult<Html<String>> {
pub fn page(title: &str, content: Markup, auth_session: Option<AuthSession>) -> Html<String> {
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 {

View file

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

View file

@ -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),
impl From<Error> for WebError {
fn from(value: Error) -> Self {
Self(value)
}
}
#[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 From<bincode::ErrorKind> for WebError {
fn from(value: bincode::ErrorKind) -> Self {
Self(value.into())
}
}
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> {
error!("webserver error: {:?}", self);
(
StatusCode::INTERNAL_SERVER_ERROR,
// TODO: don't expose raw errors over the internet?
format!("internal server error: {}", self),
)
.into_response()
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
}
}
pub type ReqResult<T> = Result<T, ReqError>;
pub type WebResult<T> = Result<T, WebError>;
pub async fn router(
state: AppState,
with_watchers: bool,
) -> Result<(Router, Vec<Option<FileWatcher>>), NewRouterError> {
) -> AnyResult<(Router, Vec<Option<FileWatcher>>)> {
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
Some(LiveReloadLayer::new())
} else {
@ -109,7 +97,7 @@ pub async fn router(
Ok((result, watchers))
}
async fn index(auth_session: Option<AuthSession>) -> ReqResult<Html<String>> {
async fn index(auth_session: Option<AuthSession>) -> Html<String> {
page(
"index",
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(
"about",
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(
"dashboard",
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 users = state.db.all::<IVec, User>(User::tree())?;
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::{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<AppState>,
Form(creds): Form<Creds>,
) -> ReqResult<Html<String>> {
) -> WebResult<Html<String>> {
let user = User::try_new(&creds.username, creds.password.expose_secret())?;
let existing_user: Option<User> = 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

View file

@ -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<Self, NewStateError> {
pub async fn try_new() -> AnyResult<Self> {
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<String>,
}
#[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,

View file

@ -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<Utc>,
}
#[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> {
pub fn try_new(username: &str, password: &str) -> AnyResult<Self> {
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(),
)?)
}
}

View file

@ -1,3 +1,5 @@
#![allow(dead_code)]
use chrono::{DateTime, TimeZone, Utc};
pub use uuid::Uuid;
use uuid::{NoContext, Timestamp};