From ed780c9585210daf63e9d22a76fabfc5c8e0af46 Mon Sep 17 00:00:00 2001 From: Matthew Kaminski Date: Wed, 28 Aug 2024 23:03:52 -0400 Subject: [PATCH] Fix more clippy issues, implement forgot password --- src/capsules/forgot_password_form.rs | 55 ++++++++++++++++++++--- src/capsules/login_form.rs | 11 +++-- src/capsules/register_form.rs | 9 ++-- src/components/header.rs | 7 ++- src/components/layout.rs | 9 +--- src/endpoints.rs | 2 + src/server/auth/forgot_password.rs | 27 ++++++++++-- src/server/auth/login.rs | 2 +- src/server/auth/register.rs | 15 ++++--- src/state_enums.rs | 11 +++++ src/templates/add_game_form.rs | 10 +---- src/templates/global_state.rs | 66 ++++++++++++++++++++++++++++ src/templates/index.rs | 2 +- src/templates/one_v_one_board.rs | 2 +- src/templates/overall_board.rs | 2 +- 15 files changed, 181 insertions(+), 49 deletions(-) create mode 100644 src/templates/global_state.rs diff --git a/src/capsules/forgot_password_form.rs b/src/capsules/forgot_password_form.rs index 5efb6c8..d9d5e00 100644 --- a/src/capsules/forgot_password_form.rs +++ b/src/capsules/forgot_password_form.rs @@ -8,10 +8,16 @@ cfg_if::cfg_if! { if #[cfg(client)] { use crate::{ state_enums::{ OpenState}, - templates::{get_api_path}, - global_state::{self, AppStateRx}, + templates::get_api_path, + global_state::{AppStateRx}, + endpoints::FORGOT_PASSWORD, + models::{ + auth::ForgotPasswordRequest, + generic::GenericResponse, + }, }; use reqwest::StatusCode; + } } @@ -25,6 +31,7 @@ lazy_static! { struct ForgotPasswordFormState { username: String, how_to_reach: String, + error: String, } impl ForgotPasswordFormStateRx { @@ -32,6 +39,7 @@ impl ForgotPasswordFormStateRx { fn reset(&self) { self.username.set(String::new()); self.how_to_reach.set(String::new()); + self.error.set(String::new()); } } @@ -49,7 +57,6 @@ fn forgot_password_form_capsule( { spawn_local_scoped(cx, async move { let global_state = Reactor::::from_cx(cx).get_global_state::(cx); - // Close modal state.reset(); global_state @@ -63,6 +70,26 @@ fn forgot_password_form_capsule( #[cfg(client)] { spawn_local_scoped(cx, async move { + let request = ForgotPasswordRequest { + username: state.username.get().as_ref().clone(), + contact_info: state.how_to_reach.get().as_ref().clone(), + }; + + // // @todo clean up error handling + let client = reqwest::Client::new(); + let response = client + .post(get_api_path(FORGOT_PASSWORD).as_str()) + .json(&request) + .send() + .await + .unwrap(); + let status = response.status(); + let response_data = response.json::().await.unwrap(); + if status != StatusCode::OK { + state.error.set(response_data.status); + return; + } + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); // Close modal @@ -81,11 +108,26 @@ fn forgot_password_form_capsule( div (class="bg-white rounded-lg shadow relative dark:bg-gray-700"){ div (class="flex justify-end p-2"){ button (on:click = close_modal, class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){ - "Back" + "Close" } } 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"){"Forgot Password"} + + (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") {} @@ -113,7 +155,8 @@ pub fn get_capsule() -> Capsule { #[engine_only_fn] async fn get_build_state(_info: StateGeneratorInfo<()>) -> ForgotPasswordFormState { ForgotPasswordFormState { - username: "".to_owned(), - how_to_reach: "".to_owned(), + username: String::new(), + how_to_reach: String::new(), + error: String::new(), } } diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index dd962a7..cbb65e1 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -7,12 +7,11 @@ use web_sys::Event; cfg_if::cfg_if! { if #[cfg(client)] { use crate::{ - models::auth::{LoginInfo, LoginResponse}, endpoints::LOGIN, - state_enums::{LoginState, OpenState}, - templates::{get_api_path}, - global_state::{self, AppStateRx}, - models::auth::WebAuthInfo, + global_state::{AppStateRx}, + models::auth::{LoginInfo, LoginResponse, WebAuthInfo}, + state_enums::{OpenState}, + templates::get_api_path, }; use reqwest::StatusCode; } @@ -141,7 +140,7 @@ fn login_form_capsule( div (class="bg-white rounded-lg shadow relative dark:bg-gray-700"){ div (class="flex justify-end p-2"){ button (on:click = close_modal, class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){ - "Back" + "Close" } } div (class="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8") { diff --git a/src/capsules/register_form.rs b/src/capsules/register_form.rs index 0e628c7..8b9dd60 100644 --- a/src/capsules/register_form.rs +++ b/src/capsules/register_form.rs @@ -9,11 +9,10 @@ cfg_if::cfg_if! { use crate::{ models::auth::{RegisterRequest}, endpoints::REGISTER, - state_enums::{LoginState, OpenState}, - templates::{get_api_path}, - global_state::{self, AppStateRx}, + state_enums::OpenState, + templates::get_api_path, + global_state::AppStateRx, models::{ - auth::WebAuthInfo, generic::GenericResponse }, }; @@ -120,7 +119,7 @@ fn register_form_capsule( div (class="bg-white rounded-lg shadow relative dark:bg-gray-700"){ div (class="flex justify-end p-2"){ button (on:click = close_modal, class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){ - "Back" + "Close" } } div (class="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8") { diff --git a/src/components/header.rs b/src/components/header.rs index 2c5127f..97e89c7 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -16,13 +16,12 @@ cfg_if::cfg_if! { } #[derive(Prop)] -pub struct HeaderProps<'a> { +pub struct HeaderProps { pub game: GameState, - pub title: &'a str, } #[component] -pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderProps<'a>) -> View { +pub fn Header<'a, G: Html>(cx: Scope<'a>, props: HeaderProps) -> View { // Get global state to get authentication info let global_state = Reactor::::from_cx(cx).get_global_state::(cx); @@ -63,7 +62,7 @@ pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderPro // Title div(class = "text-gray-700 text-2xl font-semibold py-2") { - "Pool Elo - Season 1" + (props.game.to_string()) " - Season 1" } // Login / register or user buttons diff --git a/src/components/layout.rs b/src/components/layout.rs index 14fdf72..6eddfa5 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -14,7 +14,6 @@ use sycamore::prelude::*; #[derive(Prop)] pub struct LayoutProps<'a, G: Html> { pub game: GameState, - pub title: &'a str, pub children: Children<'a, G>, } @@ -23,11 +22,7 @@ pub struct LayoutProps<'a, G: Html> { #[component] pub fn Layout<'a, G: Html>( cx: Scope<'a>, - LayoutProps { - game, - title, - children, - }: LayoutProps<'a, G>, + LayoutProps { game, children }: LayoutProps<'a, G>, ) -> View { let children = children.call(cx); @@ -39,7 +34,7 @@ pub fn Layout<'a, G: Html>( view! { cx, // Main page header, including login functionality - Header(game = game, title = title) + Header(game = game) // Modals section(class = "flex-2") { diff --git a/src/endpoints.rs b/src/endpoints.rs index 0b90741..a166646 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,4 +1,6 @@ pub const REGISTER: &str = "/api/register"; pub const LOGIN: &str = "/api/login"; +// TODO -> remove once it's used +#[cfg(engine)] pub const LOGIN_TEST: &str = "/api/login-test"; pub const FORGOT_PASSWORD: &str = "/api/forgot-password"; diff --git a/src/server/auth/forgot_password.rs b/src/server/auth/forgot_password.rs index fc7d87a..1b7cb41 100644 --- a/src/server/auth/forgot_password.rs +++ b/src/server/auth/forgot_password.rs @@ -1,12 +1,33 @@ -use crate::{models::auth::ForgotPasswordRequest, server::server_state::ServerState}; +use crate::{ + entity::{prelude::*, user}, + models::{auth::ForgotPasswordRequest, generic::GenericResponse}, + server::server_state::ServerState, +}; use axum::{ extract::{Json, State}, http::StatusCode, }; +use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; pub async fn post_forgot_password( State(state): State, Json(password_request): Json, -) -> StatusCode { - StatusCode::OK +) -> (StatusCode, Json) { + // Get user + let existing_user: Option = User::find() + .filter(user::Column::Username.eq(password_request.username)) + .one(&state.db_conn) + .await + .unwrap(); + match existing_user { + Some(user) => { + let mut user = user.into_active_model(); + user.forgot_password_request = Set(Some(password_request.contact_info)); + (StatusCode::OK, Json(GenericResponse::ok())) + } + None => ( + StatusCode::BAD_REQUEST, + Json(GenericResponse::err("Username doesn't exist")), + ), + } } diff --git a/src/server/auth/login.rs b/src/server/auth/login.rs index 30a4251..dc0f8c4 100644 --- a/src/server/auth/login.rs +++ b/src/server/auth/login.rs @@ -73,7 +73,7 @@ pub async fn post_login_user( } pub async fn post_test_login( - State(state): State, + State(_): State, header_map: HeaderMap, ) -> Result, StatusCode> { if let Some(auth_header) = header_map.get("Authorization") { diff --git a/src/server/auth/register.rs b/src/server/auth/register.rs index e2204f9..6238874 100644 --- a/src/server/auth/register.rs +++ b/src/server/auth/register.rs @@ -71,11 +71,16 @@ pub async fn post_register_user( forgot_password_request: Set(None), ..Default::default() }; - // TODO -> error handling - let db_resp = user::Entity::insert(new_user) - .exec(&state.db_conn) - .await - .unwrap(); + let db_resp = user::Entity::insert(new_user).exec(&state.db_conn).await; + match db_resp { + Ok(_) => {} + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(GenericResponse::err(err.to_string().as_str())), + ); + } + }; return (StatusCode::OK, Json(GenericResponse::ok())); } diff --git a/src/state_enums.rs b/src/state_enums.rs index 028a43b..3fa9e45 100644 --- a/src/state_enums.rs +++ b/src/state_enums.rs @@ -15,6 +15,17 @@ pub enum GameState { TableTennis, } +impl ToString for GameState { + fn to_string(&self) -> String { + match self { + GameState::None => String::new(), + GameState::Pool => "Pool".to_owned(), + GameState::Pickleball => "Pool".to_owned(), + GameState::TableTennis => "Pool".to_owned(), + } + } +} + #[derive(Serialize, Deserialize, Clone)] pub enum OpenState { Open, diff --git a/src/templates/add_game_form.rs b/src/templates/add_game_form.rs index 95d682e..7c39863 100644 --- a/src/templates/add_game_form.rs +++ b/src/templates/add_game_form.rs @@ -4,14 +4,6 @@ use serde::{Deserialize, Serialize}; use sycamore::prelude::*; use web_sys::Event; -cfg_if::cfg_if! { - if #[cfg(client)] { - use crate::global_state::AppStateRx; - use crate::templates::get_api_path; - use chrono::Utc; - } -} - // Reactive page #[derive(Serialize, Deserialize, Clone, ReactiveState)] @@ -39,7 +31,7 @@ fn add_game_form_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStat }; view! { cx, - Layout(title = "Add Game Results", game = GameState::Pool) { + Layout(game = GameState::Pool) { div (class = "flex flex-wrap") { select { option (value="red") diff --git a/src/templates/global_state.rs b/src/templates/global_state.rs new file mode 100644 index 0000000..b146152 --- /dev/null +++ b/src/templates/global_state.rs @@ -0,0 +1,66 @@ +// Not a page, global state that is shared between all pages + +use perseus::{prelude::*, state::GlobalStateCreator}; +use serde::{Deserialize, Serialize}; + +use crate::{ + models::auth::Claims, + state_enums::{LoginState, OpenState}, +}; + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "AppStateRx")] +pub struct AppState { + #[rx(nested)] + pub auth: AuthData, + #[rx(nested)] + pub modals_open: ModalOpenData, +} + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "AuthDataRx")] +pub struct AuthData { + pub state: LoginState, + pub username: Option, + pub claims: Claims, +} + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "ModalOpenDataRx")] +pub struct ModalOpenData { + pub login: OpenState, +} + +pub fn get_global_state_creator() -> GlobalStateCreator { + GlobalStateCreator::new().build_state_fn(get_build_state) +} + +#[engine_only_fn] +pub async fn get_build_state() -> AppState { + AppState { + auth: AuthData { + state: LoginState::Unknown, + username: None, + claims: Claims { + sub: "".to_owned(), + exp: 0, + }, + }, + modals_open: ModalOpenData { + login: OpenState::Closed, + }, + } +} + +// Client only code to check if they're authenticated +#[cfg(client)] +impl AuthDataRx { + pub fn detect_state(&self) { + // If the user is in a known state, return + if let LoginState::Authenticated | LoginState::NotAuthenticated = *self.state.get() { + return; + } + // TODO -> Get state from storage + self.state.set(LoginState::NotAuthenticated); + } +} diff --git a/src/templates/index.rs b/src/templates/index.rs index dd25099..b43b89f 100644 --- a/src/templates/index.rs +++ b/src/templates/index.rs @@ -4,7 +4,7 @@ use sycamore::prelude::*; fn index_page(cx: Scope) -> View { view! { cx, - Layout(title = "Index", game = GameState::Pool) { + Layout(game = GameState::Pool) { // Anything we put in here will be rendered inside the `
` block of the layout p { "Hello World!" } br {} diff --git a/src/templates/one_v_one_board.rs b/src/templates/one_v_one_board.rs index f4b5954..8a0e3f9 100644 --- a/src/templates/one_v_one_board.rs +++ b/src/templates/one_v_one_board.rs @@ -9,7 +9,7 @@ struct PageState {} fn one_v_one_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageStateRx) -> View { view! { cx, - Layout(title = "1v1 Leaderboard", game = GameState::Pool) { + Layout(game = GameState::Pool) { p { "leaderboard" } } } diff --git a/src/templates/overall_board.rs b/src/templates/overall_board.rs index 0401a5a..79465fe 100644 --- a/src/templates/overall_board.rs +++ b/src/templates/overall_board.rs @@ -12,7 +12,7 @@ fn overall_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageSta let _global_state = Reactor::::from_cx(cx).get_global_state::(cx); view! { cx, - Layout(title = "Overall Leaderboard", game = GameState::Pool) { + Layout(game = GameState::Pool) { ul { (View::new_fragment( vec![],