Add user prefs, update capsule to always exist, fix lifetime bug somehow
This commit is contained in:
24
Cargo.toml
24
Cargo.toml
@@ -22,18 +22,6 @@ chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] }
|
||||
lazy_static = "1.5"
|
||||
strum = "0.26.2"
|
||||
strum_macros = "0.26.2"
|
||||
polars = { version = "0.39.2", default-features = false, features = [
|
||||
"fmt_no_tty",
|
||||
"rows",
|
||||
"lazy",
|
||||
"concat_str",
|
||||
"strings",
|
||||
"regex",
|
||||
"csv",
|
||||
"json",
|
||||
"dtype-struct",
|
||||
"serde",
|
||||
] }
|
||||
|
||||
[target.'cfg(engine)'.dev-dependencies]
|
||||
fantoccini = "0.19"
|
||||
@@ -52,6 +40,18 @@ sea-orm = { version = "1.0", features = [
|
||||
jsonwebtoken = "9.3.0"
|
||||
argon2 = "0.5"
|
||||
tower-http = { version = "0.3", features = ["fs"] }
|
||||
polars = { version = "0.39.2", default-features = false, features = [
|
||||
"fmt_no_tty",
|
||||
"rows",
|
||||
"lazy",
|
||||
"concat_str",
|
||||
"strings",
|
||||
"regex",
|
||||
"csv",
|
||||
"json",
|
||||
"dtype-struct",
|
||||
"serde",
|
||||
] }
|
||||
|
||||
[target.'cfg(client)'.dependencies]
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
12
README.md
12
README.md
@@ -91,11 +91,21 @@ See https://docs.rs/polars/latest/polars/#config-with-env-vars
|
||||
|
||||
Default Windows:
|
||||
|
||||
`Env:RUST_LOG = "info"; $Env:POLARS_FMT_MAX_COLS = "100"; $Env:POLARS_TABLE_WIDTH = "200";`
|
||||
`$Env:RUST_LOG = "info"; $Env:POLARS_FMT_MAX_COLS = "100"; $Env:POLARS_TABLE_WIDTH = "200";`
|
||||
|
||||
# Deploying the project
|
||||
|
||||
First run
|
||||
`perseus deploy`
|
||||
|
||||
For windows local testing, you can use:
|
||||
|
||||
`sudo New-Item -ItemType SymbolicLink -Name 'card_downloader' -Target '.\..\card_downloader'`
|
||||
|
||||
`sudo New-Item -ItemType SymbolicLink -Name 'data' -Target '.\..\data'`
|
||||
|
||||
For ubuntu, use:
|
||||
|
||||
`TODO`
|
||||
|
||||
The folder with everything necessary will be in `/pkg`
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20240813_000001_create_users;
|
||||
mod m20240906_000002_create_cards;
|
||||
mod m20240906_000003_create_user_prefs;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20240813_000001_create_users::Migration)]
|
||||
vec![
|
||||
Box::new(m20240813_000001_create_users::Migration),
|
||||
Box::new(m20240906_000002_create_cards::Migration),
|
||||
Box::new(m20240906_000003_create_user_prefs::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
32
migration/src/m20240906_000002_create_cards.rs
Normal file
32
migration/src/m20240906_000002_create_cards.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// User table
|
||||
// @todo verify all data saved is length-checked
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(YugiohCard::Table)
|
||||
.col(pk_auto(YugiohCard::Id))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(YugiohCard::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum YugiohCard {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
75
migration/src/m20240906_000003_create_user_prefs.rs
Normal file
75
migration/src/m20240906_000003_create_user_prefs.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use crate::m20240813_000001_create_users::User;
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// User table
|
||||
// @todo verify all data saved is length-checked
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(UserPrefEntry::Table)
|
||||
.col(pk_auto(UserPrefEntry::Id))
|
||||
.col(boolean(UserPrefEntry::LoadLocalCardData))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// User to UserPref assoc
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(UserToUserPref::Table)
|
||||
.col(integer(UserToUserPref::UserId))
|
||||
.col(integer(UserToUserPref::UserPrefId))
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.name("pk-user_to_user_pref")
|
||||
.col(UserToUserPref::UserId)
|
||||
.col(UserToUserPref::UserPrefId),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk-user_to_user_pref-user_id")
|
||||
.from(UserToUserPref::Table, UserToUserPref::UserId)
|
||||
.to(User::Table, User::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk-user_to_user_pref-user_pref_id")
|
||||
.from(UserToUserPref::Table, UserToUserPref::UserPrefId)
|
||||
.to(UserPrefEntry::Table, UserPrefEntry::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserToUserPref::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserPrefEntry::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum UserPrefEntry {
|
||||
Table,
|
||||
Id,
|
||||
LoadLocalCardData,
|
||||
}
|
||||
|
||||
// Assoc
|
||||
#[derive(DeriveIden)]
|
||||
enum UserToUserPref {
|
||||
Table,
|
||||
UserId,
|
||||
UserPrefId,
|
||||
}
|
||||
19
src/auth.rs
19
src/auth.rs
@@ -1,19 +0,0 @@
|
||||
use crate::user;
|
||||
use axum_login::{AuthUser, AuthnBackend, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// References
|
||||
// https://github.com/maxcountryman/axum-login/tree/main/examples/sqlite/src
|
||||
// https://framesurge.sh/perseus/en-US/docs/0.4.x/state/intro
|
||||
|
||||
impl AuthUser for user::Model {
|
||||
type Id = i32;
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn session_auth_hash(&self) -> &[u8] {
|
||||
self.password.as_bytes()
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ use crate::{
|
||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||
},
|
||||
global_state::AppStateRx,
|
||||
state_enums::OpenState,
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(client)] {
|
||||
use crate::{
|
||||
state_enums::{ OpenState},
|
||||
templates::get_api_path,
|
||||
endpoints::FORGOT_PASSWORD,
|
||||
models::{
|
||||
@@ -35,7 +35,6 @@ lazy_static! {
|
||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||
#[rx(alias = "ForgotPasswordFormStateRx")]
|
||||
struct ForgotPasswordFormState {
|
||||
username: String,
|
||||
how_to_reach: String,
|
||||
error: String,
|
||||
}
|
||||
@@ -43,7 +42,6 @@ struct ForgotPasswordFormState {
|
||||
impl ForgotPasswordFormStateRx {
|
||||
#[cfg(client)]
|
||||
fn reset(&self) {
|
||||
self.username.set(String::new());
|
||||
self.how_to_reach.set(String::new());
|
||||
self.error.set(String::new());
|
||||
}
|
||||
@@ -58,13 +56,7 @@ fn forgot_password_form_capsule<G: Html>(
|
||||
state: &ForgotPasswordFormStateRx,
|
||||
_props: ForgotPasswordFormProps,
|
||||
) -> View<G> {
|
||||
// If there's a tentative username, set it
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
state
|
||||
.username
|
||||
.set((*global_state.auth.pending_username.get()).clone());
|
||||
global_state.auth.pending_username.set(String::new());
|
||||
|
||||
let close_modal = move |_event: Event| {
|
||||
#[cfg(client)]
|
||||
{
|
||||
@@ -84,7 +76,7 @@ fn forgot_password_form_capsule<G: Html>(
|
||||
{
|
||||
spawn_local_scoped(cx, async move {
|
||||
let request = ForgotPasswordRequest {
|
||||
username: state.username.get().as_ref().clone(),
|
||||
username: global_state.auth.pending_username.get().as_ref().clone(),
|
||||
contact_info: state.how_to_reach.get().as_ref().clone(),
|
||||
};
|
||||
|
||||
@@ -116,29 +108,37 @@ fn forgot_password_form_capsule<G: Html>(
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle animate-none") {
|
||||
div (class="modal-box"){
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Forgot Password"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
(match *global_state.modals_open.forgot_password.get() {
|
||||
OpenState::Open => { view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle animate-none") {
|
||||
div (class="modal-box"){
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Forgot Password"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Contact Info" } }
|
||||
input (bind:value = state.how_to_reach, class = "input input-bordered w-full")
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Contact Info" } }
|
||||
input (bind:value = state.how_to_reach, class = "input input-bordered w-full")
|
||||
|
||||
// Submit button
|
||||
div (class = "flex justify-center mt-6") {
|
||||
button (on:click = handle_submit, class="btn"){"Submit"}
|
||||
// Submit button
|
||||
div (class = "flex justify-center mt-6") {
|
||||
button (on:click = handle_submit, class="btn"){"Submit"}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@ pub fn get_capsule<G: Html>() -> Capsule<G, ForgotPasswordFormProps> {
|
||||
#[engine_only_fn]
|
||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> ForgotPasswordFormState {
|
||||
ForgotPasswordFormState {
|
||||
username: String::new(),
|
||||
how_to_reach: String::new(),
|
||||
error: String::new(),
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||
},
|
||||
global_state::AppStateRx,
|
||||
state_enums::OpenState,
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
@@ -17,7 +18,6 @@ cfg_if::cfg_if! {
|
||||
endpoints::LOGIN,
|
||||
models::auth::{LoginInfo, LoginResponse, WebAuthInfo},
|
||||
models::generic::GenericResponse,
|
||||
state_enums::{OpenState},
|
||||
templates::get_api_path,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
@@ -31,7 +31,6 @@ lazy_static! {
|
||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||
#[rx(alias = "LoginFormStateRx")]
|
||||
struct LoginFormState {
|
||||
username: String,
|
||||
password: String,
|
||||
remember_me: bool,
|
||||
error: String,
|
||||
@@ -39,8 +38,7 @@ struct LoginFormState {
|
||||
|
||||
impl LoginFormStateRx {
|
||||
#[cfg(client)]
|
||||
fn reset(&self) {
|
||||
self.username.set(String::new());
|
||||
fn reset_state(&self) {
|
||||
self.password.set(String::new());
|
||||
self.remember_me.set(false);
|
||||
self.error.set(String::new());
|
||||
@@ -58,19 +56,13 @@ fn login_form_capsule<G: Html>(
|
||||
state: &LoginFormStateRx,
|
||||
props: LoginFormProps,
|
||||
) -> View<G> {
|
||||
// If there's a tentative username, set it
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
state
|
||||
.username
|
||||
.set((*global_state.auth.pending_username.get()).clone());
|
||||
global_state.auth.pending_username.set(String::new());
|
||||
|
||||
let close_modal = move |_event: Event| {
|
||||
#[cfg(client)]
|
||||
{
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
state.reset();
|
||||
state.reset_state();
|
||||
global_state.modals_open.login.set(OpenState::Closed)
|
||||
});
|
||||
}
|
||||
@@ -82,12 +74,6 @@ fn login_form_capsule<G: Html>(
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
|
||||
// Update tentative username
|
||||
global_state
|
||||
.auth
|
||||
.pending_username
|
||||
.set((*state.username.get()).clone());
|
||||
|
||||
// Open new modal
|
||||
global_state
|
||||
.modals_open
|
||||
@@ -95,7 +81,7 @@ fn login_form_capsule<G: Html>(
|
||||
.set(OpenState::Open);
|
||||
|
||||
// Close modal
|
||||
state.reset();
|
||||
state.reset_state();
|
||||
global_state.modals_open.login.set(OpenState::Closed);
|
||||
});
|
||||
}
|
||||
@@ -106,7 +92,7 @@ fn login_form_capsule<G: Html>(
|
||||
{
|
||||
spawn_local_scoped(cx, async move {
|
||||
let remember_me = *state.remember_me.get().as_ref();
|
||||
let username = state.username.get().as_ref().clone();
|
||||
let username = global_state.auth.pending_username.get().as_ref().clone();
|
||||
let login_info = LoginInfo {
|
||||
username: username.clone(),
|
||||
password: state.password.get().as_ref().clone(),
|
||||
@@ -139,56 +125,65 @@ fn login_form_capsule<G: Html>(
|
||||
username,
|
||||
remember_me,
|
||||
});
|
||||
// Update preferences
|
||||
global_state.handle_user_prefs(response.prefs);
|
||||
|
||||
// Close modal
|
||||
state.reset();
|
||||
state.reset_state();
|
||||
global_state.modals_open.login.set(OpenState::Closed);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle") {
|
||||
div (class="modal-box"){
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Sign in"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
(match *global_state.modals_open.login.get() {
|
||||
OpenState::Open => { view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle") {
|
||||
div (class="modal-box"){
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Sign in"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||
|
||||
// Remember me button and forget password button
|
||||
div (class="flex justify-between items-center mt-1"){
|
||||
// Remember me button
|
||||
(match props.remember_me {
|
||||
true => { view!{ cx,
|
||||
div (class = "flex items-start form-control") {
|
||||
label (class = "label cursor-pointer") {
|
||||
span (class = "label-text mr-4") { "Remember me" }
|
||||
input (bind:checked = state.remember_me, type = "checkbox", class = "checkbox")
|
||||
}
|
||||
}
|
||||
}},
|
||||
false => view!{cx, },
|
||||
})
|
||||
// Forget password button
|
||||
button (on:click = handle_forgot_password, class="flex link link-primary"){"Lost Password?"}
|
||||
}
|
||||
|
||||
// Log in button
|
||||
div (class = "flex justify-center") {
|
||||
button (on:click = handle_log_in, class="btn"){"Log in"}
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||
|
||||
// Remember me button and forget password button
|
||||
div (class="flex justify-between items-center mt-1"){
|
||||
// Remember me button
|
||||
(match props.remember_me {
|
||||
true => { view!{ cx,
|
||||
div (class = "flex items-start form-control") {
|
||||
label (class = "label cursor-pointer") {
|
||||
span (class = "label-text mr-4") { "Remember me" }
|
||||
input (bind:checked = state.remember_me, type = "checkbox", class = "checkbox")
|
||||
}
|
||||
}
|
||||
}},
|
||||
false => view!{cx, },
|
||||
})
|
||||
// Forget password button
|
||||
button (on:click = handle_forgot_password, class="flex link link-primary"){"Lost Password?"}
|
||||
}
|
||||
// Log in button
|
||||
div (class = "flex justify-center") {
|
||||
button (on:click = handle_log_in, class="btn"){"Log in"}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +197,6 @@ pub fn get_capsule<G: Html>() -> Capsule<G, LoginFormProps> {
|
||||
#[engine_only_fn]
|
||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> LoginFormState {
|
||||
LoginFormState {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
remember_me: false,
|
||||
error: String::new(),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::global_state::AppStateRx;
|
||||
use lazy_static::lazy_static;
|
||||
use perseus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::Event;
|
||||
|
||||
use crate::components::{
|
||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||
use crate::{
|
||||
components::{
|
||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||
},
|
||||
state_enums::OpenState,
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
@@ -13,9 +17,7 @@ cfg_if::cfg_if! {
|
||||
use crate::{
|
||||
models::auth::{RegisterRequest},
|
||||
endpoints::REGISTER,
|
||||
state_enums::OpenState,
|
||||
templates::get_api_path,
|
||||
global_state::AppStateRx,
|
||||
models::{
|
||||
generic::GenericResponse
|
||||
},
|
||||
@@ -31,7 +33,6 @@ lazy_static! {
|
||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||
#[rx(alias = "RegisterFormStateRx")]
|
||||
struct RegisterFormState {
|
||||
username: String,
|
||||
password: String,
|
||||
nickname: String,
|
||||
registration_code: String,
|
||||
@@ -42,7 +43,6 @@ struct RegisterFormState {
|
||||
impl RegisterFormStateRx {
|
||||
#[cfg(client)]
|
||||
fn reset(&self) {
|
||||
self.username.set(String::new());
|
||||
self.password.set(String::new());
|
||||
self.nickname.set(String::new());
|
||||
self.registration_code.set(String::new());
|
||||
@@ -64,6 +64,7 @@ fn register_form_capsule<G: Html>(
|
||||
state: &RegisterFormStateRx,
|
||||
props: RegisterFormProps,
|
||||
) -> View<G> {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
let close_modal = move |_event: Event| {
|
||||
#[cfg(client)]
|
||||
{
|
||||
@@ -81,7 +82,7 @@ fn register_form_capsule<G: Html>(
|
||||
let registration_code = state.registration_code.get().as_ref().clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
let register_info = RegisterRequest {
|
||||
username: state.username.get().as_ref().clone(),
|
||||
username: global_state.auth.pending_username.get().as_ref().clone(),
|
||||
password: state.password.get().as_ref().clone(),
|
||||
nickname: state.nickname.get().as_ref().clone(),
|
||||
email: state.email.get().as_ref().clone(),
|
||||
@@ -106,12 +107,6 @@ fn register_form_capsule<G: Html>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tentative username
|
||||
global_state
|
||||
.auth
|
||||
.pending_username
|
||||
.set((*state.username.get()).clone());
|
||||
|
||||
// Open login modal
|
||||
global_state.modals_open.login.set(OpenState::Open);
|
||||
|
||||
@@ -123,51 +118,58 @@ fn register_form_capsule<G: Html>(
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle"){
|
||||
div (class="modal-box") {
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Register"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
(match *global_state.modals_open.register.get() {
|
||||
OpenState::Open => { view! { cx,
|
||||
dialog (class="modal-open modal modal-bottom sm:modal-middle"){
|
||||
div (class="modal-box") {
|
||||
// Header row - title and close button
|
||||
h3 (class="text-lg font-bold mb-4 text-center"){"Register"}
|
||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
||||
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
||||
// Username field
|
||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||
// Password field
|
||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||
|
||||
(match props.registration_code {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Registration Code" } }
|
||||
input (bind:value = state.registration_code, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
(match props.nickname {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Nickname (Optional)" } }
|
||||
input (bind:value = state.nickname, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
(match props.email {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Email (Optional)" } }
|
||||
input (bind:value = state.email, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
(match props.registration_code {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Registration Code" } }
|
||||
input (bind:value = state.registration_code, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
(match props.nickname {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Nickname (Optional)" } }
|
||||
input (bind:value = state.nickname, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
(match props.email {
|
||||
true => { view! {cx,
|
||||
div (class = "label") { span (class = "label-text") { "Email (Optional)" } }
|
||||
input (bind:value = state.email, class = "input input-bordered w-full")
|
||||
}},
|
||||
false => {view!{cx,}},
|
||||
})
|
||||
|
||||
// Register button
|
||||
div (class = "flex justify-center mt-6") {
|
||||
button (on:click = handle_register, class="btn"){"Register"}
|
||||
// Register button
|
||||
div (class = "flex justify-center mt-6") {
|
||||
button (on:click = handle_register, class="btn"){"Register"}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +183,6 @@ pub fn get_capsule<G: Html>() -> Capsule<G, RegisterFormProps> {
|
||||
#[engine_only_fn]
|
||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> RegisterFormState {
|
||||
RegisterFormState {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
error: String::new(),
|
||||
nickname: String::new(),
|
||||
|
||||
@@ -82,6 +82,7 @@ pub fn Header<G: Html>(cx: Scope, props: HeaderProps) -> View<G> {
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
global_state.auth.handle_log_out();
|
||||
global_state.handle_user_pref_log_out();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,35 +53,15 @@ pub fn Layout<'a, G: Html>(
|
||||
|
||||
let children = children.call(cx);
|
||||
|
||||
// Check if the client is authenticated or not
|
||||
// Check if the client is authenticated or not, load prefs, etc
|
||||
#[cfg(client)]
|
||||
global_state.auth.detect_state();
|
||||
global_state.detect_state();
|
||||
|
||||
let content_state_header = content_state.clone();
|
||||
|
||||
// Load cards from server, if user preference is set
|
||||
#[cfg(client)]
|
||||
{
|
||||
// TODO -> try to use suspense
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
|
||||
let local_card_user_pref = (*global_state.user_pref.local_card_data.get()).clone();
|
||||
let card_table_loaded = (*global_state.constants.is_loaded.get()).clone();
|
||||
if local_card_user_pref && !card_table_loaded {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(get_api_path(CARD_INFO).as_str())
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO add error handling
|
||||
let response = response.json::<CardTable>().await.unwrap();
|
||||
global_state.constants.card_table.set(Some(response));
|
||||
global_state.constants.is_loaded.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
global_state.check_dl_cards_locally(cx);
|
||||
|
||||
view! { cx,
|
||||
// Main page header, including login functionality
|
||||
@@ -89,55 +69,22 @@ pub fn Layout<'a, G: Html>(
|
||||
|
||||
// Modals
|
||||
section(class = "flex-2") {
|
||||
// (match (*global_state.constants.card_table.get()).clone() {
|
||||
// Some(card_table) => { view!{ cx,
|
||||
// p { "DONE" }
|
||||
// } },
|
||||
// None => { view!{ cx, p { "Loading cards" } } },
|
||||
// })
|
||||
(match (*global_state.constants.card_table.get()).clone() {
|
||||
Some(card_table) => { view!{ cx,
|
||||
p { "DONE" }
|
||||
} },
|
||||
None => { view!{ cx, p { "Loading cards" } } },
|
||||
})
|
||||
|
||||
(match *global_state.modals_open.login.get() {
|
||||
OpenState::Open => {
|
||||
view! { cx,
|
||||
(LOGIN_FORM.widget(cx, "",
|
||||
LoginFormProps{
|
||||
remember_me: true,
|
||||
}
|
||||
))
|
||||
}
|
||||
(LOGIN_FORM.widget(cx, "", LoginFormProps{ remember_me: true, }))
|
||||
(FORGOT_PASSWORD_FORM.widget(cx, "", ForgotPasswordFormProps{}))
|
||||
(REGISTER_FORM.widget(cx, "",
|
||||
RegisterFormProps{
|
||||
registration_code: true,
|
||||
nickname: true,
|
||||
email: true,
|
||||
}
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
})
|
||||
(match *global_state.modals_open.register.get() {
|
||||
OpenState::Open => {
|
||||
view! { cx,
|
||||
(REGISTER_FORM.widget(cx, "",
|
||||
RegisterFormProps{
|
||||
registration_code: true,
|
||||
nickname: true,
|
||||
email: true,
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
})
|
||||
(match *global_state.modals_open.forgot_password.get() {
|
||||
OpenState::Open => {
|
||||
view! { cx,
|
||||
(FORGOT_PASSWORD_FORM.widget(cx, "",
|
||||
ForgotPasswordFormProps{}
|
||||
))
|
||||
}
|
||||
}
|
||||
OpenState::Closed => {
|
||||
view!{ cx, }
|
||||
}
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
main(style = "my-8") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub const REGISTER: &str = "/api/register";
|
||||
pub const LOGIN: &str = "/api/login";
|
||||
pub const UPDATE_USER_PREFS: &str = "/api/user-prefs";
|
||||
// TODO -> remove once it's used
|
||||
#[cfg(engine)]
|
||||
pub const LOGIN_TEST: &str = "/api/login-test";
|
||||
|
||||
@@ -3,3 +3,6 @@
|
||||
pub mod prelude;
|
||||
|
||||
pub mod user;
|
||||
pub mod user_pref_entry;
|
||||
pub mod user_to_user_pref;
|
||||
pub mod yugioh_card;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
||||
|
||||
pub use super::user::Entity as User;
|
||||
pub use super::user_pref_entry::Entity as UserPrefEntry;
|
||||
pub use super::user_to_user_pref::Entity as UserToUserPref;
|
||||
pub use super::yugioh_card::Entity as YugiohCard;
|
||||
|
||||
@@ -21,6 +21,24 @@ pub struct Model {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::user_to_user_pref::Entity")]
|
||||
UserToUserPref,
|
||||
}
|
||||
|
||||
impl Related<super::user_to_user_pref::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserToUserPref.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user_pref_entry::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::user_to_user_pref::Relation::UserPrefEntry.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::user_to_user_pref::Relation::User.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
39
src/entity/user_pref_entry.rs
Normal file
39
src/entity/user_pref_entry.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_pref_entry")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub load_local_card_data: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::user_to_user_pref::Entity")]
|
||||
UserToUserPref,
|
||||
}
|
||||
|
||||
impl Related<super::user_to_user_pref::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserToUserPref.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::user_to_user_pref::Relation::User.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(
|
||||
super::user_to_user_pref::Relation::UserPrefEntry
|
||||
.def()
|
||||
.rev(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
47
src/entity/user_to_user_pref.rs
Normal file
47
src/entity/user_to_user_pref.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_to_user_pref")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: i32,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_pref_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
User,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user_pref_entry::Entity",
|
||||
from = "Column::UserPrefId",
|
||||
to = "super::user_pref_entry::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
UserPrefEntry,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user_pref_entry::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserPrefEntry.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
16
src/entity/yugioh_card.rs
Normal file
16
src/entity/yugioh_card.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "yugioh_card")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -9,11 +9,21 @@ use crate::{
|
||||
DEFAULT_THEME,
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(client)] {
|
||||
use crate::endpoints::CARD_INFO;
|
||||
use crate::templates::get_api_path;
|
||||
use crate::models::user::UserPreferences;
|
||||
use sycamore::futures::spawn_local;
|
||||
use sycamore::prelude::Scope;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
|
||||
#[rx(alias = "AppStateRx")]
|
||||
pub struct AppState {
|
||||
#[rx(nested)]
|
||||
pub user_pref: UserPreferences,
|
||||
pub user_prefs: UserPrefs,
|
||||
#[rx(nested)]
|
||||
pub constants: ConstData,
|
||||
#[rx(nested)]
|
||||
@@ -25,8 +35,9 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
|
||||
#[rx(alias = "UserPreferencesRx")]
|
||||
pub struct UserPreferences {
|
||||
#[rx(alias = "UserPrefsRx")]
|
||||
pub struct UserPrefs {
|
||||
pub loaded: bool,
|
||||
pub local_card_data: bool,
|
||||
}
|
||||
|
||||
@@ -76,7 +87,8 @@ pub fn get_global_state_creator() -> GlobalStateCreator {
|
||||
#[engine_only_fn]
|
||||
pub async fn get_build_state() -> AppState {
|
||||
AppState {
|
||||
user_pref: UserPreferences {
|
||||
user_prefs: UserPrefs {
|
||||
loaded: false,
|
||||
local_card_data: false,
|
||||
},
|
||||
constants: ConstData {
|
||||
@@ -123,12 +135,13 @@ impl AuthDataRx {
|
||||
let value = serde_json::to_string(&auth_info).unwrap();
|
||||
storage.set_item("auth", &value).unwrap();
|
||||
|
||||
// Save token to session storage
|
||||
// Set user state
|
||||
self.username.set(Some(auth_info.username.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
|
||||
@@ -148,12 +161,9 @@ impl AuthDataRx {
|
||||
self.remember_me.set(None);
|
||||
self.state.set(LoginState::NotAuthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
// Client only code to check if they're authenticated
|
||||
#[cfg(client)]
|
||||
impl AuthDataRx {
|
||||
pub fn detect_state(&self) {
|
||||
#[cfg(client)]
|
||||
fn detect_state(&self) {
|
||||
// If the user is in a known state, return
|
||||
if let LoginState::Authenticated | LoginState::NotAuthenticated = *self.state.get() {
|
||||
return;
|
||||
@@ -190,3 +200,106 @@ impl AuthDataRx {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppStateRx {
|
||||
#[cfg(client)]
|
||||
pub fn detect_state(&self) {
|
||||
self.auth.detect_state();
|
||||
if *self.user_prefs.loaded.get() == true {
|
||||
return;
|
||||
}
|
||||
|
||||
// User prefs
|
||||
let storage: web_sys::Storage =
|
||||
web_sys::window().unwrap().local_storage().unwrap().unwrap();
|
||||
let saved_prefs = storage.get("prefs").unwrap();
|
||||
match saved_prefs {
|
||||
Some(prefs_info) => {
|
||||
// TODO check if session is expiring
|
||||
let prefs_info = serde_json::from_str(&prefs_info).unwrap();
|
||||
self.handle_user_prefs(prefs_info);
|
||||
}
|
||||
None => {
|
||||
// Try session storage
|
||||
let storage: web_sys::Storage = web_sys::window()
|
||||
.unwrap()
|
||||
.session_storage()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let saved_prefs = storage.get("prefs").unwrap();
|
||||
match saved_prefs {
|
||||
Some(prefs_info) => {
|
||||
let prefs_info = serde_json::from_str(&prefs_info).unwrap();
|
||||
self.handle_user_prefs(prefs_info);
|
||||
}
|
||||
None => {
|
||||
self.handle_user_prefs(UserPreferences::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(client)]
|
||||
pub fn handle_user_prefs(&self, prefs: UserPreferences) {
|
||||
// Save prefs to global storage
|
||||
if let Some(remember_me) = *self.auth.remember_me.get() {
|
||||
if remember_me {
|
||||
let storage: web_sys::Storage =
|
||||
web_sys::window().unwrap().local_storage().unwrap().unwrap();
|
||||
let value = serde_json::to_string(&prefs).unwrap();
|
||||
storage.set_item("prefs", &value).unwrap();
|
||||
}
|
||||
}
|
||||
// Save prefs to session storage
|
||||
let storage: web_sys::Storage = web_sys::window()
|
||||
.unwrap()
|
||||
.session_storage()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let value = serde_json::to_string(&prefs).unwrap();
|
||||
storage.set_item("prefs", &value).unwrap();
|
||||
|
||||
// Set prefs state
|
||||
self.user_prefs.local_card_data.set(prefs.use_local_card_db);
|
||||
}
|
||||
#[cfg(client)]
|
||||
pub fn handle_user_pref_log_out(&self) {
|
||||
// TODO -> handle error if local storage is not readable in browser
|
||||
let storage: web_sys::Storage =
|
||||
web_sys::window().unwrap().local_storage().unwrap().unwrap();
|
||||
storage.remove_item("prefs").unwrap();
|
||||
let storage: web_sys::Storage = web_sys::window()
|
||||
.unwrap()
|
||||
.session_storage()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
storage.remove_item("prefs").unwrap();
|
||||
// Set default prefs
|
||||
self.handle_user_prefs(UserPreferences::new());
|
||||
}
|
||||
|
||||
// Client only code to check if card info should be loaded
|
||||
#[cfg(client)]
|
||||
pub fn check_dl_cards_locally<'a>(&'a self, cx: Scope<'a>) {
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = self.clone();
|
||||
|
||||
let local_card_user_pref = (*global_state.user_prefs.local_card_data.get()).clone();
|
||||
let card_table_loaded = (*global_state.constants.is_loaded.get()).clone();
|
||||
if local_card_user_pref && !card_table_loaded {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(get_api_path(CARD_INFO).as_str())
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO add error handling
|
||||
let response = response.json::<CardTable>().await.unwrap();
|
||||
global_state.constants.card_table.set(Some(response));
|
||||
global_state.constants.is_loaded.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::user::UserPreferences;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LoginInfo {
|
||||
@@ -14,6 +15,7 @@ pub struct LoginResponse {
|
||||
pub token: String,
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub expires: DateTime<Utc>,
|
||||
pub prefs: UserPreferences,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use core::panic;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[cfg(engine)]
|
||||
use polars::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Cursor;
|
||||
@@ -116,10 +118,11 @@ pub struct CardTable {
|
||||
pub sets: HashMap<u32, CardSet>,
|
||||
pub archetypes: HashMap<String, ArchetypeInfo>,
|
||||
pub monster_types: HashSet<String>,
|
||||
pub df: DataFrame,
|
||||
// pub df: DataFrame,
|
||||
}
|
||||
|
||||
impl CardTable {
|
||||
#[cfg(engine)]
|
||||
pub fn df_to_card_info_test(df: DataFrame) -> Vec<CardInfo> {
|
||||
let id_idx = df.get_column_index("id").unwrap();
|
||||
let name_idx = df.get_column_index("name").unwrap();
|
||||
@@ -394,8 +397,9 @@ impl CardTable {
|
||||
"ygoprodeck_url",
|
||||
"linkval",
|
||||
"race",
|
||||
"typeline",
|
||||
])])
|
||||
// Remove link markers, unless it's needed later
|
||||
// TODO add link marker support
|
||||
.select([col("*").exclude(["linkmarkers"])])
|
||||
// TODO add banlist support
|
||||
.select([col("*").exclude(["banlist_info"])])
|
||||
@@ -428,26 +432,26 @@ impl CardTable {
|
||||
sets,
|
||||
archetypes,
|
||||
monster_types,
|
||||
df,
|
||||
// df,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(client)]
|
||||
pub fn new_from_client_json() -> Self {
|
||||
let id_col = UInt32Chunked::new("id_row", &[1]).into_series();
|
||||
// let id_col = UInt32Chunked::new("id_row", &[1]).into_series();
|
||||
|
||||
let cards = HashMap::new();
|
||||
let sets = HashMap::new();
|
||||
let archetypes = HashMap::new();
|
||||
let monster_types = HashSet::new();
|
||||
let df = DataFrame::new(vec![id_col]).unwrap();
|
||||
// let df = DataFrame::new(vec![id_col]).unwrap();
|
||||
|
||||
Self {
|
||||
cards,
|
||||
sets,
|
||||
archetypes,
|
||||
monster_types,
|
||||
df,
|
||||
// df,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum Theme {
|
||||
Plain,
|
||||
Standard,
|
||||
Purrely,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{From, Into};
|
||||
|
||||
use crate::entity::user_pref_entry;
|
||||
|
||||
use super::theme::Theme;
|
||||
|
||||
@@ -7,3 +11,30 @@ pub struct UserPreferences {
|
||||
pub use_local_card_db: bool,
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
impl UserPreferences {
|
||||
pub fn new() -> Self {
|
||||
UserPreferences {
|
||||
use_local_card_db: false,
|
||||
theme: Theme::Standard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<user_pref_entry::Model> for UserPreferences {
|
||||
fn from(model: user_pref_entry::Model) -> Self {
|
||||
UserPreferences {
|
||||
use_local_card_db: model.load_local_card_data,
|
||||
theme: Theme::Standard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<user_pref_entry::ActiveModel> for UserPreferences {
|
||||
fn into(self) -> user_pref_entry::ActiveModel {
|
||||
user_pref_entry::ActiveModel {
|
||||
load_local_card_data: Set(self.use_local_card_db),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ use crate::{
|
||||
entity::{
|
||||
prelude::*,
|
||||
user::{self},
|
||||
user_pref_entry, user_to_user_pref,
|
||||
},
|
||||
models::{
|
||||
auth::{Claims, LoginInfo, LoginResponse},
|
||||
generic::GenericResponse,
|
||||
theme::Theme,
|
||||
user::UserPreferences,
|
||||
},
|
||||
server::server_state::ServerState,
|
||||
};
|
||||
@@ -15,7 +18,7 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait};
|
||||
|
||||
pub async fn credentials_are_correct(
|
||||
username: &str,
|
||||
@@ -85,7 +88,29 @@ pub async fn post_login_user(
|
||||
}
|
||||
};
|
||||
|
||||
(StatusCode::OK, Ok(Json(LoginResponse { token, expires })))
|
||||
// Get user preferences
|
||||
// TODO error handling
|
||||
let prefs = user_pref_entry::Entity::find()
|
||||
.join(
|
||||
JoinType::InnerJoin,
|
||||
user_pref_entry::Relation::UserToUserPref.def(),
|
||||
)
|
||||
.join(JoinType::InnerJoin, user_to_user_pref::Relation::User.def())
|
||||
.filter(user::Column::Username.eq(login_info.username))
|
||||
.one(&state.db_conn)
|
||||
.await
|
||||
.unwrap()
|
||||
// Assuming username exists here
|
||||
.unwrap();
|
||||
let prefs = UserPreferences::from(prefs);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Ok(Json(LoginResponse {
|
||||
token,
|
||||
expires,
|
||||
prefs,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
entity::{prelude::*, user},
|
||||
models::{auth::RegisterRequest, generic::GenericResponse},
|
||||
entity::{prelude::*, user, user_pref_entry, user_to_user_pref},
|
||||
models::{auth::RegisterRequest, generic::GenericResponse, user::UserPreferences},
|
||||
server::server_state::ServerState,
|
||||
};
|
||||
use argon2::{
|
||||
@@ -72,8 +72,22 @@ pub async fn post_register_user(
|
||||
..Default::default()
|
||||
};
|
||||
let db_resp = user::Entity::insert(new_user).exec(&state.db_conn).await;
|
||||
match db_resp {
|
||||
Ok(_) => {}
|
||||
let user_id = match db_resp {
|
||||
Ok(entry) => entry.last_insert_id,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(GenericResponse::err("Database error")),
|
||||
);
|
||||
}
|
||||
};
|
||||
// Add user preferences
|
||||
let default_prefs: user_pref_entry::ActiveModel = UserPreferences::new().into();
|
||||
let db_resp = user_pref_entry::Entity::insert(default_prefs)
|
||||
.exec(&state.db_conn)
|
||||
.await;
|
||||
let user_pref_id = match db_resp {
|
||||
Ok(entry) => entry.last_insert_id,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -82,5 +96,19 @@ pub async fn post_register_user(
|
||||
}
|
||||
};
|
||||
|
||||
// Add assoc entry
|
||||
let db_resp = user_to_user_pref::Entity::insert(user_to_user_pref::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
user_pref_id: Set(user_pref_id),
|
||||
})
|
||||
.exec(&state.db_conn)
|
||||
.await;
|
||||
if db_resp.is_err() {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(GenericResponse::err("Database error")),
|
||||
);
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(GenericResponse::ok()))
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod auth;
|
||||
pub mod constants;
|
||||
pub mod routes;
|
||||
pub mod server_state;
|
||||
pub mod user;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// (Server only) Routes
|
||||
use crate::endpoints::{CARD_INFO, FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER};
|
||||
use axum::routing::{get, post, Router};
|
||||
use crate::endpoints::{
|
||||
CARD_INFO, FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER, UPDATE_USER_PREFS,
|
||||
};
|
||||
use axum::routing::{get, post, put, Router};
|
||||
|
||||
use super::{
|
||||
auth::{
|
||||
@@ -10,6 +12,7 @@ use super::{
|
||||
},
|
||||
constants::card_table::get_card_table,
|
||||
server_state::ServerState,
|
||||
user::update_prefs::put_user_prefs,
|
||||
};
|
||||
|
||||
pub fn get_api_router(state: ServerState) -> Router {
|
||||
@@ -19,5 +22,6 @@ pub fn get_api_router(state: ServerState) -> Router {
|
||||
.route(LOGIN_TEST, post(post_test_login))
|
||||
.route(FORGOT_PASSWORD, post(post_forgot_password))
|
||||
.route(CARD_INFO, get(get_card_table))
|
||||
.route(UPDATE_USER_PREFS, put(put_user_prefs))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
1
src/server/user/mod.rs
Normal file
1
src/server/user/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod update_prefs;
|
||||
73
src/server/user/update_prefs.rs
Normal file
73
src/server/user/update_prefs.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use crate::{
|
||||
entity::{user, user_pref_entry, user_to_user_pref},
|
||||
models::{auth::Claims, generic::GenericResponse, user::UserPreferences},
|
||||
server::server_state::ServerState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Json, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
};
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait,
|
||||
Set,
|
||||
};
|
||||
|
||||
pub async fn put_user_prefs(
|
||||
State(state): State<ServerState>,
|
||||
header_map: HeaderMap,
|
||||
Json(user_prefs): Json<UserPreferences>,
|
||||
) -> (StatusCode, Json<GenericResponse>) {
|
||||
if let Some(auth_header) = header_map.get("Authorization") {
|
||||
if let Ok(auth_header_str) = auth_header.to_str() {
|
||||
if auth_header_str.starts_with("Bearer ") {
|
||||
let token = auth_header_str.trim_start_matches("Bearer ").to_string();
|
||||
// @todo change secret
|
||||
if let Ok(claims) = decode::<Claims>(
|
||||
&token,
|
||||
&DecodingKey::from_secret("secret".as_ref()),
|
||||
&Validation::default(),
|
||||
) {
|
||||
let username = claims.claims.sub;
|
||||
|
||||
// Get user prefs
|
||||
let db_user_prefs: Option<user_pref_entry::Model> =
|
||||
user_pref_entry::Entity::find()
|
||||
.join(
|
||||
JoinType::InnerJoin,
|
||||
user_pref_entry::Relation::UserToUserPref.def(),
|
||||
)
|
||||
.join(JoinType::InnerJoin, user_to_user_pref::Relation::User.def())
|
||||
.filter(user::Column::Username.eq(username))
|
||||
.one(&state.db_conn)
|
||||
.await
|
||||
.unwrap();
|
||||
if db_user_prefs.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(GenericResponse::err("User incorrect")),
|
||||
);
|
||||
}
|
||||
let db_user_prefs = db_user_prefs.unwrap();
|
||||
// Get new active model
|
||||
let mut new_user_prefs: user_pref_entry::ActiveModel = user_prefs.into();
|
||||
new_user_prefs.id = Set(db_user_prefs.id);
|
||||
let result = new_user_prefs.update(&state.db_conn).await;
|
||||
match result {
|
||||
Ok(_) => return (StatusCode::OK, Json(GenericResponse::ok())),
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(GenericResponse::err("Database error")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(GenericResponse::err("Unauthorized")),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,96 @@
|
||||
use crate::components::sub_components::error_block::ErrorBlock;
|
||||
use crate::global_state::AppStateRx;
|
||||
use crate::state_enums::LoginState;
|
||||
use crate::{components::layout::Layout, state_enums::ContentState};
|
||||
use perseus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::Event;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(client)] {
|
||||
use crate::models::generic::GenericResponse;
|
||||
use crate::models::theme::Theme;
|
||||
use crate::models::user::UserPreferences;
|
||||
use reqwest::StatusCode;
|
||||
use crate::endpoints::UPDATE_USER_PREFS;
|
||||
use crate::templates::get_api_path;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||
#[rx(alias = "UserPrefStateRx")]
|
||||
struct UserPrefState {
|
||||
error: String,
|
||||
}
|
||||
|
||||
fn user_index_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a UserPrefStateRx) -> View<G> {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
|
||||
let check_card_load = move |_event: Event| {
|
||||
#[cfg(client)]
|
||||
{
|
||||
spawn_local_scoped(cx, async move {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
// TODO bring out of function
|
||||
// TODO get theme
|
||||
let new_prefs = UserPreferences {
|
||||
use_local_card_db: *global_state.user_prefs.local_card_data.get(),
|
||||
theme: Theme::Standard,
|
||||
};
|
||||
|
||||
// Set local state
|
||||
global_state.handle_user_prefs(new_prefs.clone());
|
||||
|
||||
// Now that preferences were updated, if the cards should be downloaded locally, get it
|
||||
global_state.check_dl_cards_locally(cx);
|
||||
|
||||
// Also update the server
|
||||
let client = reqwest::Client::new();
|
||||
let token = match (*global_state.auth.auth_info.get()).clone() {
|
||||
Some(auth_info) => auth_info.token,
|
||||
None => String::new(),
|
||||
};
|
||||
let response = client
|
||||
.put(get_api_path(UPDATE_USER_PREFS).as_str())
|
||||
.bearer_auth(token)
|
||||
.json(&new_prefs)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
if response.status() != StatusCode::OK {
|
||||
let response = response.json::<GenericResponse>().await.unwrap();
|
||||
state.error.set(response.status.to_string());
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fn user_index_page<G: Html>(cx: Scope) -> View<G> {
|
||||
view! { cx,
|
||||
Layout(content_state = ContentState::User) {
|
||||
// Add component for handling error messages
|
||||
ErrorBlock(error = state.error.clone())
|
||||
|
||||
// Anything we put in here will be rendered inside the `<main>` block of the layout
|
||||
p { "Hello World!" }
|
||||
br {}
|
||||
(match *global_state.auth.state.get() {
|
||||
LoginState::Authenticated => { view! { cx,
|
||||
label (class = "label cursor-pointer") {
|
||||
input (
|
||||
bind:checked = global_state.user_prefs.local_card_data,
|
||||
on:change = check_card_load,
|
||||
type = "checkbox", class = "checkbox mr-4")
|
||||
span (class = "label-text") { "Use local yugioh database" }
|
||||
}
|
||||
} },
|
||||
LoginState::NotAuthenticated => { view! { cx,
|
||||
h2 {
|
||||
"You must log in to modify user preferences"
|
||||
}
|
||||
} },
|
||||
LoginState::Unknown => { view! { cx,
|
||||
} },
|
||||
} )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +104,17 @@ fn head(cx: Scope) -> View<SsrNode> {
|
||||
|
||||
pub fn get_template<G: Html>() -> Template<G> {
|
||||
Template::build("user")
|
||||
.view(user_index_page)
|
||||
.build_state_fn(get_build_state)
|
||||
.view_with_state(user_index_page)
|
||||
.head(head)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[engine_only_fn]
|
||||
async fn get_build_state(
|
||||
_info: StateGeneratorInfo<()>,
|
||||
) -> Result<UserPrefState, BlamedError<std::io::Error>> {
|
||||
Ok(UserPrefState {
|
||||
error: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user