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