Fix more clippy issues, implement forgot password
All checks were successful
Build Crate / build (push) Successful in 1m48s

This commit is contained in:
2024-08-28 23:03:52 -04:00
parent 1faaf65aad
commit ed780c9585
15 changed files with 181 additions and 49 deletions

View File

@@ -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<G: Html>(
{
spawn_local_scoped(cx, async move {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
// Close modal
state.reset();
global_state
@@ -63,6 +70,26 @@ fn forgot_password_form_capsule<G: Html>(
#[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::<GenericResponse>().await.unwrap();
if status != StatusCode::OK {
state.error.set(response_data.status);
return;
}
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
// Close modal
@@ -81,11 +108,26 @@ fn forgot_password_form_capsule<G: Html>(
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<G: Html>() -> Capsule<G, ForgotPasswordFormProps> {
#[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(),
}
}

View File

@@ -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<G: Html>(
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") {

View File

@@ -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<G: Html>(
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") {

View File

@@ -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<G> {
pub fn Header<'a, G: Html>(cx: Scope<'a>, props: HeaderProps) -> View<G> {
// Get global state to get authentication info
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(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

View File

@@ -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<G> {
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") {

View File

@@ -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";

View File

@@ -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<ServerState>,
Json(password_request): Json<ForgotPasswordRequest>,
) -> StatusCode {
StatusCode::OK
) -> (StatusCode, Json<GenericResponse>) {
// Get user
let existing_user: Option<user::Model> = 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")),
),
}
}

View File

@@ -73,7 +73,7 @@ pub async fn post_login_user(
}
pub async fn post_test_login(
State(state): State<ServerState>,
State(_): State<ServerState>,
header_map: HeaderMap,
) -> Result<Json<String>, StatusCode> {
if let Some(auth_header) = header_map.get("Authorization") {

View File

@@ -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()));
}

View File

@@ -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,

View File

@@ -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")

View File

@@ -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<String>,
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);
}
}

View File

@@ -4,7 +4,7 @@ use sycamore::prelude::*;
fn index_page<G: Html>(cx: Scope) -> View<G> {
view! { cx,
Layout(title = "Index", game = GameState::Pool) {
Layout(game = GameState::Pool) {
// Anything we put in here will be rendered inside the `<main>` block of the layout
p { "Hello World!" }
br {}

View File

@@ -9,7 +9,7 @@ struct PageState {}
fn one_v_one_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageStateRx) -> View<G> {
view! { cx,
Layout(title = "1v1 Leaderboard", game = GameState::Pool) {
Layout(game = GameState::Pool) {
p { "leaderboard" }
}
}

View File

@@ -12,7 +12,7 @@ fn overall_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageSta
let _global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
view! { cx,
Layout(title = "Overall Leaderboard", game = GameState::Pool) {
Layout(game = GameState::Pool) {
ul {
(View::new_fragment(
vec![],