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..3dcfea0 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -7,12 +7,12 @@ 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}, + models::generic::GenericResponse, + state_enums::{OpenState}, + templates::get_api_path, }; use reqwest::StatusCode; } @@ -28,6 +28,7 @@ struct LoginFormState { username: String, password: String, remember_me: bool, + error: String, } impl LoginFormStateRx { @@ -36,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()); } } @@ -93,7 +95,7 @@ fn login_form_capsule( #[cfg(client)] { spawn_local_scoped(cx, async move { - let remember_me = state.remember_me.get().as_ref().clone(); + let remember_me = *state.remember_me.get().as_ref(); let username = state.username.get().as_ref().clone(); let login_info = LoginInfo { username: username.clone(), @@ -113,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; } @@ -141,11 +144,26 @@ 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") { 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") {} @@ -191,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/capsules/register_form.rs b/src/capsules/register_form.rs index 9190576..8b9dd60 100644 --- a/src/capsules/register_form.rs +++ b/src/capsules/register_form.rs @@ -9,10 +9,12 @@ cfg_if::cfg_if! { use crate::{ models::auth::{RegisterRequest}, endpoints::REGISTER, - state_enums::{LoginState, OpenState}, - templates::{get_api_path}, - global_state::{self, AppStateRx}, - models::auth::WebAuthInfo, + state_enums::OpenState, + templates::get_api_path, + global_state::AppStateRx, + models::{ + generic::GenericResponse + }, }; use reqwest::StatusCode; } @@ -30,6 +32,7 @@ struct RegisterFormState { nickname: String, registration_code: String, email: String, + error: String, } impl RegisterFormStateRx { @@ -40,6 +43,7 @@ impl RegisterFormStateRx { self.nickname.set(String::new()); self.registration_code.set(String::new()); self.email.set(String::new()); + self.error.set(String::new()); } } @@ -90,10 +94,11 @@ fn register_form_capsule( .unwrap(); let global_state = Reactor::::from_cx(cx).get_global_state::(cx); - - if response.status() != StatusCode::OK { + let status = response.status(); + let response_data = response.json::().await.unwrap(); + if status != StatusCode::OK { // todo update to some type of alert - state.username.set(response.status().to_string()); + state.error.set(response_data.status); return; } @@ -114,11 +119,27 @@ 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") { h3 (class="text-xl font-medium text-gray-900 dark:text-white"){"Register"} + + + (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") {} @@ -174,6 +195,7 @@ async fn get_build_state(_info: StateGeneratorInfo<()>) -> RegisterFormState { RegisterFormState { username: String::new(), password: String::new(), + error: String::new(), nickname: String::new(), registration_code: String::new(), email: String::new(), diff --git a/src/components/header.rs b/src/components/header.rs index 9f0560c..c81d623 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -1,28 +1,27 @@ -use std::sync::Arc; - use perseus::prelude::*; use sycamore::prelude::*; use web_sys::Event; use crate::{ - capsules::{ - forgot_password_form::{ForgotPasswordFormProps, FORGOT_PASSWORD_FORM}, - login_form::{LoginFormProps, LOGIN_FORM}, - }, - endpoints::LOGIN, global_state::AppStateRx, - models::auth::LoginInfo, - state_enums::{GameState, LoginState, OpenState}, + state_enums::{GameState, LoginState}, }; +cfg_if::cfg_if! { + if #[cfg(client)] { + use crate::{ + state_enums::OpenState, + }; + } +} + #[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(cx: Scope, 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 7734ca2..6eddfa5 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -4,18 +4,16 @@ use crate::{ login_form::{LoginFormProps, LOGIN_FORM}, register_form::{RegisterFormProps, REGISTER_FORM}, }, - components::header::{Header, HeaderProps}, + components::header::Header, global_state::AppStateRx, - state_enums::{GameState, LoginState, OpenState}, + state_enums::{GameState, OpenState}, }; use perseus::prelude::*; use sycamore::prelude::*; -use web_sys::Event; #[derive(Prop)] pub struct LayoutProps<'a, G: Html> { pub game: GameState, - pub title: &'a str, pub children: Children<'a, G>, } @@ -24,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); @@ -40,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/global_state.rs b/src/global_state.rs index 98c0472..8e91b48 100644 --- a/src/global_state.rs +++ b/src/global_state.rs @@ -4,16 +4,10 @@ use perseus::{prelude::*, state::GlobalStateCreator}; use serde::{Deserialize, Serialize}; use crate::{ - models::auth::{Claims, WebAuthInfo}, + models::auth::WebAuthInfo, state_enums::{LoginState, OpenState}, }; -cfg_if::cfg_if! { - if #[cfg(engine)] { - - } -} - #[derive(Serialize, Deserialize, ReactiveState, Clone)] #[rx(alias = "AppStateRx")] pub struct AppState { @@ -33,6 +27,7 @@ pub struct AuthData { } impl AuthDataRx { + #[cfg(client)] pub fn handle_log_in(&self, auth_info: WebAuthInfo) { // Save new token to persistent storage if auth_info.remember_me { @@ -52,11 +47,11 @@ impl AuthDataRx { // Save token to session storage self.username.set(Some(auth_info.username.clone())); - self.remember_me.set(Some(auth_info.remember_me.clone())); + self.remember_me.set(Some(auth_info.remember_me)); self.auth_info.set(Some(auth_info)); self.state.set(LoginState::Authenticated); } - + #[cfg(client)] pub fn handle_log_out(&self) { // Delete persistent storage // TODO -> handle error if local storage is not readable in browser diff --git a/src/models/generic.rs b/src/models/generic.rs index 61938b8..7aa4029 100644 --- a/src/models/generic.rs +++ b/src/models/generic.rs @@ -6,11 +6,13 @@ pub struct GenericResponse { } impl GenericResponse { + #[cfg(engine)] pub fn ok() -> Self { GenericResponse { status: String::new(), } } + #[cfg(engine)] pub fn err(msg: &str) -> Self { GenericResponse { status: msg.to_string(), diff --git a/src/server/auth/forgot_password.rs b/src/server/auth/forgot_password.rs index 0df2656..10effbd 100644 --- a/src/server/auth/forgot_password.rs +++ b/src/server/auth/forgot_password.rs @@ -1,13 +1,40 @@ -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::{HeaderMap, StatusCode}, + http::StatusCode, }; -use sea_orm::DatabaseConnection; +use sea_orm::{ActiveModelTrait, 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)); + let user = user.update(&state.db_conn).await; + match user { + Ok(_) => (StatusCode::OK, Json(GenericResponse::ok())), + Err(_) => ( + StatusCode::BAD_REQUEST, + Json(GenericResponse::err("Database error")), + ), + } + } + 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 fec2d93..908c7d7 100644 --- a/src/server/auth/login.rs +++ b/src/server/auth/login.rs @@ -1,30 +1,27 @@ -use crate::entity::prelude::*; -use crate::models::auth::{Claims, LoginInfo, LoginResponse}; use crate::{ - entity::user::{self, Entity}, - models::auth::RegisterRequest, + entity::{ + prelude::*, + user::{self}, + }, + models::{ + auth::{Claims, LoginInfo, LoginResponse}, + generic::GenericResponse, + }, server::server_state::ServerState, }; -use argon2::password_hash::rand_core::OsRng; -use argon2::password_hash::SaltString; -use argon2::Argon2; -use argon2::PasswordHash; -use argon2::PasswordHasher; -use argon2::PasswordVerifier; +use argon2::{Argon2, PasswordHash, PasswordVerifier}; use axum::{ extract::{Json, State}, http::{HeaderMap, StatusCode}, }; -use futures::sink::Fanout; -use sea_orm::ColumnTrait; -use sea_orm::EntityTrait; -use sea_orm::InsertResult; -use sea_orm::QueryFilter; -use sea_orm::Set; - 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)) @@ -35,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), @@ -73,17 +77,21 @@ 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 }))) } } } 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") { @@ -91,13 +99,14 @@ 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 decode::( &token, &DecodingKey::from_secret("secret".as_ref()), &Validation::default(), - ) { - Ok(_) => return Ok(Json("Logged in".to_owned())), - Err(_) => {} + ) + .is_ok() + { + return Ok(Json("Logged in".to_owned())); } } } diff --git a/src/server/auth/register.rs b/src/server/auth/register.rs index e273a8a..d75f6de 100644 --- a/src/server/auth/register.rs +++ b/src/server/auth/register.rs @@ -1,23 +1,15 @@ -use crate::entity::prelude::*; -use crate::models::generic::GenericResponse; -use argon2::password_hash::rand_core::OsRng; -use argon2::password_hash::SaltString; -use argon2::Argon2; -use argon2::PasswordHash; -use argon2::PasswordHasher; -use axum::{extract::State, http::StatusCode, Json}; -use chrono::Utc; -use sea_orm::ColumnTrait; -use sea_orm::EntityTrait; -use sea_orm::InsertResult; -use sea_orm::QueryFilter; -use sea_orm::Set; - use crate::{ - entity::user::{self, Entity}, - models::auth::RegisterRequest, + entity::{prelude::*, user}, + models::{auth::RegisterRequest, generic::GenericResponse}, server::server_state::ServerState, }; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, +}; +use axum::{extract::State, http::StatusCode, Json}; +use chrono::Utc; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set}; pub async fn post_register_user( State(state): State, @@ -59,7 +51,7 @@ pub async fn post_register_user( username: Set(username), password_hash_and_salt: Set(phc_string), nickname: Set({ - if register_info.nickname == "" { + if register_info.nickname.is_empty() { None } else { Some(register_info.nickname) @@ -69,7 +61,7 @@ pub async fn post_register_user( last_active_time: Set(Utc::now().naive_utc()), is_admin: Set(false), email: Set({ - if register_info.email == "" { + if register_info.email.is_empty() { None } else { Some(register_info.email) @@ -79,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(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(GenericResponse::err("Database error")), + ); + } + }; - return (StatusCode::OK, Json(GenericResponse::ok())); + (StatusCode::OK, Json(GenericResponse::ok())) } diff --git a/src/server/routes.rs b/src/server/routes.rs index 6efbe3b..16c52ea 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -1,8 +1,6 @@ // (Server only) Routes use crate::endpoints::{FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER}; use axum::routing::{post, Router}; -use futures::executor::block_on; -use sea_orm::Database; use super::{ auth::{ diff --git a/src/state_enums.rs b/src/state_enums.rs index 028a43b..15d226a 100644 --- a/src/state_enums.rs +++ b/src/state_enums.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] @@ -15,6 +17,21 @@ pub enum GameState { TableTennis, } +impl Display for GameState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + GameState::None => "", + GameState::Pool => "Pool", + GameState::Pickleball => "Pickle Ball", + GameState::TableTennis => "Table Tennis", + } + ) + } +} + #[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/mod.rs b/src/templates/mod.rs index a7a618f..d6460a6 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -6,15 +6,9 @@ pub mod overall_board; #[cfg(client)] use perseus::utils::get_path_prefix_client; +#[cfg(client)] pub fn get_api_path(path: &str) -> String { - #[cfg(engine)] - { - path.to_string() - } - #[cfg(client)] - { - let origin = web_sys::window().unwrap().origin(); - let base_path = get_path_prefix_client(); - format!("{}{}{}", origin, base_path, path) - } + let origin = web_sys::window().unwrap().origin(); + let base_path = get_path_prefix_client(); + format!("{}{}{}", origin, base_path, path) } 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![],