From dbff72330e97e5593000c450fa575788f586375d Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sun, 28 May 2023 17:55:16 -0700 Subject: [PATCH] Adds working login route. --- src/generic_handlers.rs | 10 +++++- src/lib.rs | 8 ++++- src/login.rs | 66 +++++++++++++++++++++++++++++----------- src/main.rs | 24 ++++++++++----- src/signup.rs | 31 ++----------------- src/users.rs | 49 +++++++++++++++++++++++++++++ src/util.rs | 3 ++ templates/login_get.html | 17 +++++++++++ 8 files changed, 154 insertions(+), 54 deletions(-) create mode 100644 src/users.rs create mode 100644 src/util.rs diff --git a/src/generic_handlers.rs b/src/generic_handlers.rs index 7ad4bcd..1e21dc2 100644 --- a/src/generic_handlers.rs +++ b/src/generic_handlers.rs @@ -1,7 +1,15 @@ use axum::response::{IntoResponse, Redirect}; +use crate::AuthContext; + pub async fn handle_slash_redir() -> impl IntoResponse { Redirect::temporary("/") } -pub async fn handle_slash() -> impl IntoResponse {} +pub async fn handle_slash(auth: AuthContext) -> impl IntoResponse { + if let Some(user) = auth.current_user { + tracing::debug!("Logged in as: {user}"); + } else { + tracing::debug!("Not logged in.") + } +} diff --git a/src/lib.rs b/src/lib.rs index 62b7b65..bc43291 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,17 @@ #[macro_use] extern crate justerror; +use axum_login::SqliteStore; +pub use users::User; +use uuid::Uuid; + pub mod db; pub mod generic_handlers; pub mod login; pub mod session_store; pub mod signup; pub(crate) mod templates; +pub mod users; +pub(crate) mod util; -pub use signup::User; +pub type AuthContext = axum_login::extractors::AuthContext>; diff --git a/src/login.rs b/src/login.rs index 8010c86..4923679 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,26 +1,26 @@ -use argon2::PasswordVerifier; +use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, +}; use axum::{ extract::State, http::StatusCode, - response::{IntoResponse, Response}, + response::{IntoResponse, Redirect, Response}, + Form, }; -use axum_login::{secrecy::SecretVec, AuthUser, SqliteStore}; use sqlx::SqlitePool; -use uuid::Uuid; -use crate::{templates::LoginGet, User}; +use crate::{ + templates::{LoginGet, LoginPost}, + util::form_decode, + AuthContext, User, +}; -pub type AuthContext = axum_login::extractors::AuthContext>; +//-************************************************************************ +// Constants +//-************************************************************************ -impl AuthUser for User { - fn get_id(&self) -> Uuid { - self.id - } - - fn get_password_hash(&self) -> SecretVec { - SecretVec::new(self.pwhash.as_bytes().to_vec()) - } -} +const LAST_SEEN_QUERY: &str = "update witches set last_seen = (select unixepoch()) where id = $1"; //-************************************************************************ // Login error and success types @@ -32,7 +32,9 @@ pub struct LoginError(#[from] LoginErrorKind); #[Error] #[non_exhaustive] pub enum LoginErrorKind { + Internal, BadPassword, + BadUsername, Unknown, } @@ -58,8 +60,38 @@ impl IntoResponse for LoginError { pub async fn post_login( mut auth: AuthContext, State(pool): State, -) -> Result<(), LoginError> { - Err(LoginErrorKind::Unknown.into()) + Form(login): Form, +) -> Result { + let username = form_decode(&login.username, LoginErrorKind::BadUsername)?; + let username = username.trim(); + + let pw = form_decode(&login.password, LoginErrorKind::BadPassword)?; + let pw = pw.trim(); + + let user = User::get(username, &pool) + .await + .map_err(|_| LoginErrorKind::Unknown)?; + + let verifier = Argon2::default(); + let hash = PasswordHash::new(&user.pwhash).map_err(|_| LoginErrorKind::Internal)?; + match verifier.verify_password(pw.as_bytes(), &hash) { + Ok(_) => { + // log them in and set a session cookie + auth.login(&user) + .await + .map_err(|_| LoginErrorKind::Internal)?; + + // update last_seen; maybe this is ok to fail? + sqlx::query(LAST_SEEN_QUERY) + .bind(user.id) + .execute(&pool) + .await + .map_err(|_| LoginErrorKind::Internal)?; + + Ok(Redirect::temporary("/")) + } + _ => Err(LoginErrorKind::BadPassword.into()), + } } pub async fn get_login() -> impl IntoResponse { diff --git a/src/main.rs b/src/main.rs index 638a8bb..57342f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; use axum::{routing::get, Router}; -use axum_login::axum_sessions::SessionLayer; +use axum_login::{axum_sessions::SessionLayer, AuthLayer, SqliteStore}; use rand_core::{OsRng, RngCore}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use witch_watch::{ @@ -10,6 +10,7 @@ use witch_watch::{ login::{get_login, post_login}, session_store::SqliteSessionStore, signup::{get_create_user, handle_signup_success, post_create_user}, + User, }; #[tokio::main] @@ -23,18 +24,26 @@ async fn main() { .init(); let pool = db::get_pool().await; + + let secret = { + let mut bytes = [0u8; 128]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + bytes + }; + let session_layer = { let store = SqliteSessionStore::from_client(pool.clone()); store.migrate().await.expect("Could not migrate session DB"); - let secret = { - let mut bytes = [0u8; 128]; - let mut rng = OsRng; - rng.fill_bytes(&mut bytes); - bytes - }; SessionLayer::new(store, &secret).with_secure(true) }; + let auth_layer = { + const QUERY: &str = "select * from witches where id = $1"; + let store = SqliteStore::::new(pool.clone()).with_query(QUERY); + AuthLayer::new(store, &secret) + }; + let app = Router::new() .route("/", get(handle_slash).post(handle_slash)) .route("/signup", get(get_create_user).post(post_create_user)) @@ -44,6 +53,7 @@ async fn main() { ) .route("/login", get(get_login).post(post_login)) .fallback(handle_slash_redir) + .layer(auth_layer) .layer(session_layer) .with_state(pool); diff --git a/src/signup.rs b/src/signup.rs index 917e233..f32a47f 100644 --- a/src/signup.rs +++ b/src/signup.rs @@ -1,5 +1,3 @@ -use std::fmt::Display; - use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, @@ -14,35 +12,12 @@ use sqlx::{query_as, SqlitePool}; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; -use crate::templates::CreateUser; +use crate::{templates::CreateUser, User}; const CREATE_QUERY: &str = "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; const ID_QUERY: &str = "select * from witches where id = $1"; -#[derive(Debug, Default, Clone, PartialEq, Eq, sqlx::FromRow)] -pub struct User { - pub id: Uuid, - pub username: String, - pub displayname: Option, - pub email: Option, - pub last_seen: Option, - pub(crate) pwhash: String, -} - -impl Display for User { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let uname = &self.username; - let dname = if let Some(ref n) = self.displayname { - n - } else { - "" - }; - let email = if let Some(ref e) = self.email { e } else { "" }; - write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") - } -} - //-************************************************************************ // Result types for user creation //-************************************************************************ @@ -97,7 +72,7 @@ pub async fn get_create_user() -> CreateUser { pub async fn post_create_user( State(pool): State, Form(signup): Form, -) -> Result { +) -> Result { let username = &signup.username; let displayname = &signup.displayname; let email = &signup.email; @@ -155,7 +130,7 @@ pub async fn post_create_user( let id = user.id.as_simple().to_string(); let location = format!("/signup_success/{id}"); - let resp = axum::response::Redirect::temporary(&location).into_response(); + let resp = axum::response::Redirect::temporary(&location); Ok(resp) } diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..4ef50fd --- /dev/null +++ b/src/users.rs @@ -0,0 +1,49 @@ +use std::fmt::Display; + +use axum_login::{secrecy::SecretVec, AuthUser}; +use sqlx::SqlitePool; +use uuid::Uuid; + +const USERNAME_QUERY: &str = "select * from witches where username = $1"; + +#[derive(Debug, Default, Clone, PartialEq, Eq, sqlx::FromRow)] +pub struct User { + pub id: Uuid, + pub username: String, + pub displayname: Option, + pub email: Option, + pub last_seen: Option, + pub(crate) pwhash: String, +} + +impl Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let uname = &self.username; + let dname = if let Some(ref n) = self.displayname { + n + } else { + "" + }; + let email = if let Some(ref e) = self.email { e } else { "" }; + write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") + } +} + +impl AuthUser for User { + fn get_id(&self) -> Uuid { + self.id + } + + fn get_password_hash(&self) -> SecretVec { + SecretVec::new(self.pwhash.as_bytes().to_vec()) + } +} + +impl User { + pub async fn get(username: &str, db: &SqlitePool) -> Result { + sqlx::query_as(USERNAME_QUERY) + .bind(username) + .fetch_one(db) + .await + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..1b15b86 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,3 @@ +pub fn form_decode(input: &str, err: E) -> Result { + Ok(urlencoding::decode(input).map_err(|_| err)?.into_owned()) +} diff --git a/templates/login_get.html b/templates/login_get.html index e69de29..ea7a1a7 100644 --- a/templates/login_get.html +++ b/templates/login_get.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}Login to Witch Watch, Bish{% endblock %} + +{% block content %} + +

+

+ +
+ +
+ +
+

+ +{% endblock %}