use argon2::{ password_hash::{PasswordHash, PasswordVerifier}, Argon2, }; use axum::{ extract::State, http::StatusCode, response::{IntoResponse, Redirect, Response}, Form, }; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; use crate::{AuthContext, LoginPage, LogoutPage, LogoutSuccess, User}; //-************************************************************************ // Constants //-************************************************************************ //-************************************************************************ // Login error and success types //-************************************************************************ #[Error] pub struct LoginError(#[from] LoginErrorKind); #[Error] #[non_exhaustive] pub enum LoginErrorKind { Internal, BadPassword, Unknown, } impl IntoResponse for LoginError { fn into_response(self) -> Response { match self.0 { LoginErrorKind::Internal => ( StatusCode::INTERNAL_SERVER_ERROR, "An unknown error occurred; you cursed, brah?", ) .into_response(), LoginErrorKind::Unknown => (StatusCode::OK, "Not successful.").into_response(), _ => (StatusCode::OK, format!("{self}")).into_response(), } } } // for receiving form submissions #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct LoginPostForm { pub username: String, pub password: String, } //-************************************************************************ // Login handlers //-************************************************************************ /// Handle login queries #[axum::debug_handler] pub async fn post_login( mut auth: AuthContext, State(pool): State, Form(login): Form, ) -> Result { let username = &login.username; let username = username.trim(); let pw = &login.password; let pw = pw.trim(); let user = User::try_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)?; Ok(Redirect::to("/")) } _ => Err(LoginErrorKind::BadPassword.into()), } } pub async fn get_login() -> impl IntoResponse { LoginPage::default() } pub async fn get_logout() -> impl IntoResponse { LogoutPage } pub async fn post_logout(mut auth: AuthContext) -> impl IntoResponse { if auth.current_user.is_some() { auth.logout().await; } LogoutSuccess } #[cfg(test)] mod test { use crate::{ templates::{LoginPage, LogoutPage, LogoutSuccess, MainPage}, test_utils::{get_test_user, massage, server, FORM_CONTENT_TYPE}, }; const LOGIN_FORM: &str = "username=test_user&password=a"; #[tokio::test] async fn get_login() { let s = server().await; let resp = s.get("/login").await; let body = std::str::from_utf8(resp.bytes()).unwrap().to_string(); assert_eq!(body, LoginPage::default().to_string()); } #[tokio::test] async fn post_login_success() { let s = server().await; let body = massage(LOGIN_FORM); let resp = s .post("/login") .expect_failure() .content_type(FORM_CONTENT_TYPE) .bytes(body) .await; assert_eq!(resp.status_code(), 303); } #[tokio::test] async fn post_login_bad_user() { let s = server().await; let form = "username=test_LOSER&password=aaaa"; let body = massage(form); let resp = s .post("/login") .expect_success() .content_type(FORM_CONTENT_TYPE) .bytes(body) .await; assert_eq!(resp.status_code(), 200); } #[tokio::test] async fn post_login_bad_password() { let s = server().await; let form = "username=test_user&password=bbbb"; let body = massage(form); let resp = s .post("/login") .expect_success() .content_type(FORM_CONTENT_TYPE) .bytes(body) .await; assert_eq!(resp.status_code(), 200); } #[tokio::test] async fn get_logout() { let s = server().await; let resp = s.get("/logout").await; let body = std::str::from_utf8(resp.bytes()).unwrap().to_string(); assert_eq!(body, LogoutPage.to_string()); } #[tokio::test] async fn post_logout_not_logged_in() { let s = server().await; let resp = s.post("/logout").await; resp.assert_status_ok(); let body = std::str::from_utf8(resp.bytes()).unwrap(); let default = LogoutSuccess.to_string(); assert_eq!(body, &default); } #[tokio::test] async fn post_logout_logged_in() { let s = server().await; // log in and prove it { let body = massage(LOGIN_FORM); let resp = s .post("/login") .expect_failure() .content_type(FORM_CONTENT_TYPE) .bytes(body) .await; assert_eq!(resp.status_code(), 303); let logged_in = MainPage { user: Some(get_test_user()), } .to_string(); let main_page = s.get("/").await; let body = std::str::from_utf8(main_page.bytes()).unwrap(); assert_eq!(&logged_in, body); } let resp = s.post("/logout").await; let body = std::str::from_utf8(resp.bytes()).unwrap(); let default = LogoutSuccess.to_string(); assert_eq!(body, &default); } }