diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index cbb65e1..b127c1b 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -10,6 +10,7 @@ cfg_if::cfg_if! { endpoints::LOGIN, global_state::{AppStateRx}, models::auth::{LoginInfo, LoginResponse, WebAuthInfo}, + models::generic::GenericResponse, state_enums::{OpenState}, templates::get_api_path, }; @@ -27,6 +28,7 @@ struct LoginFormState { username: String, password: String, remember_me: bool, + error: String, } impl LoginFormStateRx { @@ -35,6 +37,7 @@ impl LoginFormStateRx { self.username.set(String::new()); self.password.set(String::new()); self.remember_me.set(false); + self.error.set(String::new()); } } @@ -112,8 +115,9 @@ fn login_form_capsule( let global_state = Reactor::::from_cx(cx).get_global_state::(cx); if response.status() != StatusCode::OK { - // todo update to some type of alert - state.username.set(response.status().to_string()); + let response = response.json::().await.unwrap(); + state.error.set(response.status.to_string()); + state.reset(); return; } @@ -145,6 +149,21 @@ fn login_form_capsule( } div (class="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8") { h3 (class="text-xl font-medium text-gray-900 dark:text-white"){"Sign in"} + + (match state.error.get().as_ref() != "" { + true => { view!{cx, + div (role="alert") { + div (class="bg-red-500 text-white font-bold rounded-t px-4 py-2") { + "Error" + } + div (class="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700"){ + p {(state.error.get())} + } + } + }}, + false => {view!{cx,}}, + }) + div { label (class="text-sm font-medium text-gray-900 block mb-2 dark:text-gray-300") {"Username"} input (bind:value = state.username, class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white") {} @@ -190,8 +209,9 @@ pub fn get_capsule() -> Capsule { #[engine_only_fn] async fn get_build_state(_info: StateGeneratorInfo<()>) -> LoginFormState { LoginFormState { - username: "".to_owned(), - password: "".to_owned(), + username: String::new(), + password: String::new(), remember_me: false, + error: String::new(), } } diff --git a/src/components/header.rs b/src/components/header.rs index 97e89c7..c81d623 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -21,7 +21,7 @@ pub struct HeaderProps { } #[component] -pub fn Header<'a, G: Html>(cx: Scope<'a>, props: HeaderProps) -> View { +pub fn Header(cx: Scope, props: HeaderProps) -> View { // Get global state to get authentication info let global_state = Reactor::::from_cx(cx).get_global_state::(cx); diff --git a/src/server/auth/forgot_password.rs b/src/server/auth/forgot_password.rs index d1f5b8d..10effbd 100644 --- a/src/server/auth/forgot_password.rs +++ b/src/server/auth/forgot_password.rs @@ -25,13 +25,11 @@ pub async fn post_forgot_password( user.forgot_password_request = Set(Some(password_request.contact_info)); let user = user.update(&state.db_conn).await; match user { - Ok(_) => return (StatusCode::OK, Json(GenericResponse::ok())), - Err(_) => { - return ( - StatusCode::BAD_REQUEST, - Json(GenericResponse::err("Database error")), - ) - } + Ok(_) => (StatusCode::OK, Json(GenericResponse::ok())), + Err(_) => ( + StatusCode::BAD_REQUEST, + Json(GenericResponse::err("Database error")), + ), } } None => ( diff --git a/src/server/auth/login.rs b/src/server/auth/login.rs index dc0f8c4..a01ad96 100644 --- a/src/server/auth/login.rs +++ b/src/server/auth/login.rs @@ -3,7 +3,10 @@ use crate::{ prelude::*, user::{self}, }, - models::auth::{Claims, LoginInfo, LoginResponse}, + models::{ + auth::{Claims, LoginInfo, LoginResponse}, + generic::GenericResponse, + }, server::server_state::ServerState, }; use argon2::{Argon2, PasswordHash, PasswordVerifier}; @@ -14,7 +17,11 @@ use axum::{ use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; -pub async fn credentials_are_correct(username: &str, password: &str, state: &ServerState) -> bool { +pub async fn credentials_are_correct( + username: &str, + password: &str, + state: &ServerState, +) -> Result<(), String> { // Get user let existing_user: Option = User::find() .filter(user::Column::Username.eq(username)) @@ -25,28 +32,35 @@ pub async fn credentials_are_correct(username: &str, password: &str, state: &Ser Some(user) => user.password_hash_and_salt, None => { // @todo make dummy password hash - return false; + return Err("Username doesn't exist".to_owned()); } }; - return Argon2::default() - .verify_password( - password.as_bytes(), - &PasswordHash::new(hash_to_check.as_str()).unwrap(), - ) - .is_ok(); + match Argon2::default().verify_password( + password.as_bytes(), + &PasswordHash::new(hash_to_check.as_str()).unwrap(), + ) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid credentials".to_owned()), + } } pub async fn post_login_user( State(state): State, Json(login_info): Json, -) -> Result, StatusCode> { +) -> ( + StatusCode, + Result, Json>, +) { let user_authenticated = credentials_are_correct(&login_info.username, &login_info.password, &state); match user_authenticated.await { - false => Err(StatusCode::UNAUTHORIZED), - true => { + Err(why) => ( + StatusCode::UNAUTHORIZED, + Err(Json(GenericResponse::err(why.as_str()))), + ), + Ok(_) => { let expires = match login_info.remember_me { true => chrono::Utc::now() + chrono::Duration::days(365), false => chrono::Utc::now() + chrono::Duration::days(1), @@ -63,11 +77,15 @@ pub async fn post_login_user( &EncodingKey::from_secret("secret".as_ref()), ) { Ok(token) => token, - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Err(Json(GenericResponse::err("Failed to get token"))), + ) + } }; - let resp = LoginResponse { token, expires }; - Ok(Json(resp)) + (StatusCode::OK, Ok(Json(LoginResponse { token, expires }))) } } } @@ -81,13 +99,12 @@ pub async fn post_test_login( if auth_header_str.starts_with("Bearer ") { let token = auth_header_str.trim_start_matches("Bearer ").to_string(); // @todo change secret - match decode::( + if let Ok(_) = decode::( &token, &DecodingKey::from_secret("secret".as_ref()), &Validation::default(), ) { - Ok(_) => return Ok(Json("Logged in".to_owned())), - Err(_) => {} + return Ok(Json("Logged in".to_owned())); } } }