From 10d57a83be3a16d6dcc445e69c53812a0861f3e3 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Tue, 14 Nov 2023 16:43:49 -0600 Subject: [PATCH] Login sessionn stuff? --- Cargo.lock | 108 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + readme.md | 19 +++++++-- src/partials.rs | 3 +- src/router.rs | 60 +++++++++++++++++++++++++-- src/state.rs | 1 + src/views.rs | 55 ++++++++++++------------ 7 files changed, 210 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f29fe76..5901571 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-login" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eee4df0cdb40a1d9fbc49b709bda6eb9e94f9b4b7ab6affe03582c180e6a5d9" +dependencies = [ + "async-trait", + "axum", + "http", + "ring", + "serde", + "thiserror", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + [[package]] name = "axum-macros" version = "0.3.8" @@ -309,7 +329,7 @@ dependencies = [ "async-trait", "axum-core", "base64ct", - "cookie", + "cookie 0.18.0", "hmac", "http", "rand", @@ -590,6 +610,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie" version = "0.18.0" @@ -686,6 +717,19 @@ dependencies = [ "cipher", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.2", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.8" @@ -911,6 +955,17 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "futures-sink" version = "0.3.29" @@ -932,6 +987,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1325,6 +1381,7 @@ checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -1340,11 +1397,12 @@ dependencies = [ "anyhow", "argon2", "axum", + "axum-login", "axum-macros", "axum_csrf", "base64", "color-eyre", - "cookie", + "cookie 0.18.0", "maud", "notify", "password-hash", @@ -2927,6 +2985,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984" +dependencies = [ + "async-trait", + "axum-core", + "cookie 0.17.0", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.4.4" @@ -2978,6 +3053,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower-sessions" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f5468ed4acd72d325bd66f18decb233bd0d4bfce1aaa56544b148c999df4bef" +dependencies = [ + "async-trait", + "axum-core", + "dashmap", + "futures", + "http", + "parking_lot", + "serde", + "serde_json", + "thiserror", + "time", + "tower-cookies", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + [[package]] name = "tracing" version = "0.1.40" @@ -3131,6 +3229,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 11f11ee..5e3d7f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ sea-orm = { version = "0.12.6", features = ["sqlx-sqlite", "macros", "runtime-to sea-orm-migration = { version = "0.12.6", features = ["sqlx-sqlite"] } uuid = { version = "1.5.0", features = ["v7", "atomic", "fast-rng", "macro-diagnostics"] } password-hash = "0.5.0" +axum-login = "0.7.3" diff --git a/readme.md b/readme.md index 12a423c..41ef039 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,25 @@ +# Setup + +```shell +$ direnv allow +``` + # Running -``` -nix shell -cargo run +```shell +$ cargo run ``` # Testing -``` +```shell cargo test cargo run hurl contract.hurl --variable base='http://localhost:3000' --verbose ``` + +# Regenerate Entities + +```shell +$ sea-orm-cli generate entity -u $DATABASE_URL -o src/entities +``` diff --git a/src/partials.rs b/src/partials.rs index ed8bfcd..c92f36a 100644 --- a/src/partials.rs +++ b/src/partials.rs @@ -1,9 +1,10 @@ -use maud::{html, Markup, PreEscaped}; +use maud::{html, Markup, PreEscaped, DOCTYPE}; use crate::feather_icons; pub fn header() -> Markup { html! { + (DOCTYPE) head { link rel="stylesheet" href="/assets/styles.css" {} link rel="icon" href="/assets/favicon.svg" {} diff --git a/src/router.rs b/src/router.rs index 28d3003..c797a59 100644 --- a/src/router.rs +++ b/src/router.rs @@ -4,9 +4,11 @@ use crate::{error::AppError, views}; use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::SaltString; use argon2::{Argon2, PasswordHasher}; +use axum::async_trait; use axum::extract::State; use axum::{http::StatusCode, response::Html, routing::get, Form, Router}; use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken}; +use axum_login::{AuthUser, AuthnBackend, UserId}; use base64::prelude::*; use maud::html; use notify::Watcher; @@ -121,16 +123,68 @@ async fn register( )) } -#[derive(Deserialize)] -struct Login { +#[derive(Deserialize, Clone)] +pub struct Login { authenticity_token: String, username: String, password: String, } -async fn login(c: CsrfToken, Form(f): Form) -> AppRes { +type AuthSession = axum_login::AuthSession; + +impl AuthUser for user::Model { + type Id = uuid::Uuid; + + fn id(&self) -> Self::Id { + uuid::Uuid::try_from(self.id.clone()).expect("failed to convert user ID to UUID") + } + + fn session_auth_hash(&self) -> &[u8] { + self.password_digest.as_bytes() + } +} + +#[async_trait] +impl AuthnBackend for state::State { + type User = user::Model; + type Credentials = Login; + type Error = AppError; + + async fn authenticate(&self, l: Self::Credentials) -> Result, Self::Error> { + Ok(User::find() + .filter(user::Column::Username.eq(l.username)) + // TODO: will this have index problems since I'm searching over the password digest? + .filter(user::Column::PasswordDigest.eq(password_digest(l.password)?)) + .one(&self.db) + .await?) + } + + async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { + Ok(User::find_by_id(*user_id).one(&self.db).await?) + } +} + +async fn login(mut auth: AuthSession, c: CsrfToken, Form(f): Form) -> AppRes { csrf_verify(c, &f.authenticity_token)?; + let user = match auth.authenticate(f.clone()).await { + Ok(Some(user)) => user, + Ok(None) => return Ok((StatusCode::UNAUTHORIZED, Html("user not found".to_string()))), + Err(e) => { + return Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(format!("failed to authenticate user: {}", e)), + )) + } + }; + + if let Err(e) = auth.login(&user).await { + return Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(format!("failed to login user: {}", e)), + )); + } + Ok(( StatusCode::OK, Html( diff --git a/src/state.rs b/src/state.rs index a8ae966..b7a1480 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,6 +5,7 @@ use sea_orm_migration::MigratorTrait; use crate::migrator::Migrator; +#[derive(Clone)] pub struct State { pub db: DatabaseConnection, } diff --git a/src/views.rs b/src/views.rs index bcba121..0d6b5e4 100644 --- a/src/views.rs +++ b/src/views.rs @@ -90,11 +90,14 @@ pub async fn login(t: CsrfToken) -> impl IntoResponse { form method="post" { input type="hidden" name="authenticity_token" value=(token) {} label { - input {} + "Username:" + input name="username" {} } label { - input {} + "Password:" + input type="password" name="password" {} } + button type="submit" { "Login" } } } (footer()) @@ -105,39 +108,37 @@ pub async fn login(t: CsrfToken) -> impl IntoResponse { .await } -pub async fn all_users(State(s): State>) -> Result, AppError> { +pub async fn all_users(State(s): State>) -> AppRes { let users: Vec = User::find().all(&s.db).await?; - // @if let Some(name) = u.name { - // name - // } @else { - // "N/A" - // } - Ok(Html( - html! { - (header()) - main class="prose" { - h1 { "Users" } - ul { - @if users.len() < 1 { - li { "It looks like there are no users yet!" } - } else { - @for u in users { - li { - (u.username) - @if let Some(name) = u.name { - " (" - (name) - ")" + Ok(( + StatusCode::OK, + Html( + html! { + (header()) + main class="prose" { + h1 { "Users" } + ul { + @if users.is_empty() { + li { "It looks like there are no users yet!" } + } else { + @for u in users { + li { + (u.username) + @if let Some(name) = u.name { + " (" + (name) + ")" + } } } } } } + (footer()) } - (footer()) - } - .into_string(), + .into_string(), + ), )) }