what2watch/src/login.rs

213 lines
5.8 KiB
Rust
Raw Normal View History

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
};
use sqlx::SqlitePool;
use crate::{util::form_decode, AuthContext, LoginGet, LoginPost, LogoutGet, LogoutPost, 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,
2023-05-29 00:55:16 +00:00
BadUsername,
2023-05-28 20:44:50 +00:00
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(),
_ => (StatusCode::OK, format!("{self}")).into_response(),
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-05-29 00:55:16 +00:00
Form(login): Form<LoginPost>,
) -> Result<impl IntoResponse, LoginError> {
let username = form_decode(&login.username, LoginErrorKind::BadUsername)?;
let username = username.trim();
let pw = form_decode(&login.password, LoginErrorKind::BadPassword)?;
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 {
LoginGet::default()
}
2023-05-29 18:13:12 +00:00
pub async fn get_logout() -> impl IntoResponse {
LogoutGet
2023-05-29 18:13:12 +00:00
}
pub async fn post_logout(mut auth: AuthContext) -> impl IntoResponse {
if auth.current_user.is_some() {
auth.logout().await;
}
LogoutPost
2023-05-29 18:13:12 +00:00
}
#[cfg(test)]
mod test {
use crate::{
templates::{LoginGet, LogoutGet, LogoutPost, MainPage},
2023-06-03 22:45:19 +00:00
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() {
2023-06-03 17:11:07 +00:00
let s = server().await;
let resp = s.get("/login").await;
let body = std::str::from_utf8(resp.bytes()).unwrap().to_string();
assert_eq!(body, LoginGet::default().to_string());
}
#[tokio::test]
async fn post_login_success() {
2023-06-03 17:11:07 +00:00
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() {
2023-06-03 17:11:07 +00:00
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() {
2023-06-03 17:11:07 +00:00
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() {
2023-06-03 17:11:07 +00:00
let s = server().await;
let resp = s.get("/logout").await;
let body = std::str::from_utf8(resp.bytes()).unwrap().to_string();
assert_eq!(body, LogoutGet.to_string());
}
#[tokio::test]
async fn post_logout_not_logged_in() {
2023-06-03 17:11:07 +00:00
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 = LogoutPost.to_string();
assert_eq!(body, &default);
}
#[tokio::test]
async fn post_logout_logged_in() {
2023-06-03 17:11:07 +00:00
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 {
2023-06-03 22:45:19 +00:00
user: Some(get_test_user()),
}
.to_string();
let idx = s.get("/").await;
let body = std::str::from_utf8(idx.bytes()).unwrap();
assert_eq!(&logged_in, body);
}
let resp = s.post("/logout").await;
let body = std::str::from_utf8(resp.bytes()).unwrap();
let default = LogoutPost.to_string();
assert_eq!(body, &default);
}
}