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"
|
lazy_static = "1.5"
|
||||||
strum = "0.26.2"
|
strum = "0.26.2"
|
||||||
strum_macros = "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]
|
[target.'cfg(engine)'.dev-dependencies]
|
||||||
fantoccini = "0.19"
|
fantoccini = "0.19"
|
||||||
@@ -52,6 +40,18 @@ sea-orm = { version = "1.0", features = [
|
|||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.0"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
tower-http = { version = "0.3", features = ["fs"] }
|
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]
|
[target.'cfg(client)'.dependencies]
|
||||||
wasm-bindgen = "0.2.93"
|
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:
|
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
|
# Deploying the project
|
||||||
|
|
||||||
First run
|
First run
|
||||||
`perseus deploy`
|
`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`
|
The folder with everything necessary will be in `/pkg`
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
pub use sea_orm_migration::prelude::*;
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
mod m20240813_000001_create_users;
|
mod m20240813_000001_create_users;
|
||||||
|
mod m20240906_000002_create_cards;
|
||||||
|
mod m20240906_000003_create_user_prefs;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigratorTrait for Migrator {
|
impl MigratorTrait for Migrator {
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
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,
|
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||||
},
|
},
|
||||||
global_state::AppStateRx,
|
global_state::AppStateRx,
|
||||||
|
state_enums::OpenState,
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(client)] {
|
if #[cfg(client)] {
|
||||||
use crate::{
|
use crate::{
|
||||||
state_enums::{ OpenState},
|
|
||||||
templates::get_api_path,
|
templates::get_api_path,
|
||||||
endpoints::FORGOT_PASSWORD,
|
endpoints::FORGOT_PASSWORD,
|
||||||
models::{
|
models::{
|
||||||
@@ -35,7 +35,6 @@ lazy_static! {
|
|||||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||||
#[rx(alias = "ForgotPasswordFormStateRx")]
|
#[rx(alias = "ForgotPasswordFormStateRx")]
|
||||||
struct ForgotPasswordFormState {
|
struct ForgotPasswordFormState {
|
||||||
username: String,
|
|
||||||
how_to_reach: String,
|
how_to_reach: String,
|
||||||
error: String,
|
error: String,
|
||||||
}
|
}
|
||||||
@@ -43,7 +42,6 @@ struct ForgotPasswordFormState {
|
|||||||
impl ForgotPasswordFormStateRx {
|
impl ForgotPasswordFormStateRx {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
fn reset(&self) {
|
fn reset(&self) {
|
||||||
self.username.set(String::new());
|
|
||||||
self.how_to_reach.set(String::new());
|
self.how_to_reach.set(String::new());
|
||||||
self.error.set(String::new());
|
self.error.set(String::new());
|
||||||
}
|
}
|
||||||
@@ -58,13 +56,7 @@ fn forgot_password_form_capsule<G: Html>(
|
|||||||
state: &ForgotPasswordFormStateRx,
|
state: &ForgotPasswordFormStateRx,
|
||||||
_props: ForgotPasswordFormProps,
|
_props: ForgotPasswordFormProps,
|
||||||
) -> View<G> {
|
) -> View<G> {
|
||||||
// If there's a tentative username, set it
|
|
||||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
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| {
|
let close_modal = move |_event: Event| {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
{
|
{
|
||||||
@@ -84,7 +76,7 @@ fn forgot_password_form_capsule<G: Html>(
|
|||||||
{
|
{
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let request = ForgotPasswordRequest {
|
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(),
|
contact_info: state.how_to_reach.get().as_ref().clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,29 +108,37 @@ fn forgot_password_form_capsule<G: Html>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
dialog (class="modal-open modal modal-bottom sm:modal-middle animate-none") {
|
(match *global_state.modals_open.forgot_password.get() {
|
||||||
div (class="modal-box"){
|
OpenState::Open => { view! { cx,
|
||||||
// Header row - title and close button
|
dialog (class="modal-open modal modal-bottom sm:modal-middle animate-none") {
|
||||||
h3 (class="text-lg font-bold mb-4 text-center"){"Forgot Password"}
|
div (class="modal-box"){
|
||||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
// 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
|
// Add component for handling error messages
|
||||||
ErrorBlock(error = state.error.clone())
|
ErrorBlock(error = state.error.clone())
|
||||||
|
|
||||||
// Username field
|
// Username field
|
||||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||||
|
|
||||||
// Password field
|
// Password field
|
||||||
div (class = "label") { span (class = "label-text") { "Contact Info" } }
|
div (class = "label") { span (class = "label-text") { "Contact Info" } }
|
||||||
input (bind:value = state.how_to_reach, class = "input input-bordered w-full")
|
input (bind:value = state.how_to_reach, class = "input input-bordered w-full")
|
||||||
|
|
||||||
// Submit button
|
// Submit button
|
||||||
div (class = "flex justify-center mt-6") {
|
div (class = "flex justify-center mt-6") {
|
||||||
button (on:click = handle_submit, class="btn"){"Submit"}
|
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]
|
#[engine_only_fn]
|
||||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> ForgotPasswordFormState {
|
async fn get_build_state(_info: StateGeneratorInfo<()>) -> ForgotPasswordFormState {
|
||||||
ForgotPasswordFormState {
|
ForgotPasswordFormState {
|
||||||
username: String::new(),
|
|
||||||
how_to_reach: String::new(),
|
how_to_reach: String::new(),
|
||||||
error: String::new(),
|
error: String::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||||
},
|
},
|
||||||
global_state::AppStateRx,
|
global_state::AppStateRx,
|
||||||
|
state_enums::OpenState,
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
@@ -17,7 +18,6 @@ cfg_if::cfg_if! {
|
|||||||
endpoints::LOGIN,
|
endpoints::LOGIN,
|
||||||
models::auth::{LoginInfo, LoginResponse, WebAuthInfo},
|
models::auth::{LoginInfo, LoginResponse, WebAuthInfo},
|
||||||
models::generic::GenericResponse,
|
models::generic::GenericResponse,
|
||||||
state_enums::{OpenState},
|
|
||||||
templates::get_api_path,
|
templates::get_api_path,
|
||||||
};
|
};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
@@ -31,7 +31,6 @@ lazy_static! {
|
|||||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||||
#[rx(alias = "LoginFormStateRx")]
|
#[rx(alias = "LoginFormStateRx")]
|
||||||
struct LoginFormState {
|
struct LoginFormState {
|
||||||
username: String,
|
|
||||||
password: String,
|
password: String,
|
||||||
remember_me: bool,
|
remember_me: bool,
|
||||||
error: String,
|
error: String,
|
||||||
@@ -39,8 +38,7 @@ struct LoginFormState {
|
|||||||
|
|
||||||
impl LoginFormStateRx {
|
impl LoginFormStateRx {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
fn reset(&self) {
|
fn reset_state(&self) {
|
||||||
self.username.set(String::new());
|
|
||||||
self.password.set(String::new());
|
self.password.set(String::new());
|
||||||
self.remember_me.set(false);
|
self.remember_me.set(false);
|
||||||
self.error.set(String::new());
|
self.error.set(String::new());
|
||||||
@@ -58,19 +56,13 @@ fn login_form_capsule<G: Html>(
|
|||||||
state: &LoginFormStateRx,
|
state: &LoginFormStateRx,
|
||||||
props: LoginFormProps,
|
props: LoginFormProps,
|
||||||
) -> View<G> {
|
) -> View<G> {
|
||||||
// If there's a tentative username, set it
|
|
||||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
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| {
|
let close_modal = move |_event: Event| {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
{
|
{
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
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)
|
global_state.modals_open.login.set(OpenState::Closed)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,12 +74,6 @@ fn login_form_capsule<G: Html>(
|
|||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
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
|
// Open new modal
|
||||||
global_state
|
global_state
|
||||||
.modals_open
|
.modals_open
|
||||||
@@ -95,7 +81,7 @@ fn login_form_capsule<G: Html>(
|
|||||||
.set(OpenState::Open);
|
.set(OpenState::Open);
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
state.reset();
|
state.reset_state();
|
||||||
global_state.modals_open.login.set(OpenState::Closed);
|
global_state.modals_open.login.set(OpenState::Closed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,7 +92,7 @@ fn login_form_capsule<G: Html>(
|
|||||||
{
|
{
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let remember_me = *state.remember_me.get().as_ref();
|
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 {
|
let login_info = LoginInfo {
|
||||||
username: username.clone(),
|
username: username.clone(),
|
||||||
password: state.password.get().as_ref().clone(),
|
password: state.password.get().as_ref().clone(),
|
||||||
@@ -139,56 +125,65 @@ fn login_form_capsule<G: Html>(
|
|||||||
username,
|
username,
|
||||||
remember_me,
|
remember_me,
|
||||||
});
|
});
|
||||||
|
// Update preferences
|
||||||
|
global_state.handle_user_prefs(response.prefs);
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
state.reset();
|
state.reset_state();
|
||||||
global_state.modals_open.login.set(OpenState::Closed);
|
global_state.modals_open.login.set(OpenState::Closed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
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
|
(match *global_state.modals_open.login.get() {
|
||||||
ErrorBlock(error = state.error.clone())
|
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
|
// Add component for handling error messages
|
||||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
ErrorBlock(error = state.error.clone())
|
||||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
|
||||||
|
|
||||||
// Password field
|
// Username field
|
||||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||||
|
|
||||||
// Remember me button and forget password button
|
// Password field
|
||||||
div (class="flex justify-between items-center mt-1"){
|
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||||
// Remember me button
|
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||||
(match props.remember_me {
|
|
||||||
true => { view!{ cx,
|
// Remember me button and forget password button
|
||||||
div (class = "flex items-start form-control") {
|
div (class="flex justify-between items-center mt-1"){
|
||||||
label (class = "label cursor-pointer") {
|
// Remember me button
|
||||||
span (class = "label-text mr-4") { "Remember me" }
|
(match props.remember_me {
|
||||||
input (bind:checked = state.remember_me, type = "checkbox", class = "checkbox")
|
true => { view!{ cx,
|
||||||
}
|
div (class = "flex items-start form-control") {
|
||||||
}
|
label (class = "label cursor-pointer") {
|
||||||
}},
|
span (class = "label-text mr-4") { "Remember me" }
|
||||||
false => view!{cx, },
|
input (bind:checked = state.remember_me, type = "checkbox", class = "checkbox")
|
||||||
})
|
}
|
||||||
// Forget password button
|
}
|
||||||
button (on:click = handle_forgot_password, class="flex link link-primary"){"Lost Password?"}
|
}},
|
||||||
}
|
false => view!{cx, },
|
||||||
|
})
|
||||||
// Log in button
|
// Forget password button
|
||||||
div (class = "flex justify-center") {
|
button (on:click = handle_forgot_password, class="flex link link-primary"){"Lost Password?"}
|
||||||
button (on:click = handle_log_in, class="btn"){"Log in"}
|
}
|
||||||
|
// 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]
|
#[engine_only_fn]
|
||||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> LoginFormState {
|
async fn get_build_state(_info: StateGeneratorInfo<()>) -> LoginFormState {
|
||||||
LoginFormState {
|
LoginFormState {
|
||||||
username: String::new(),
|
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
remember_me: false,
|
remember_me: false,
|
||||||
error: String::new(),
|
error: String::new(),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
use crate::global_state::AppStateRx;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use perseus::prelude::*;
|
use perseus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::Event;
|
use web_sys::Event;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::{
|
||||||
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
components::{
|
||||||
|
static_components::close_button::CloseButtonSvg, sub_components::error_block::ErrorBlock,
|
||||||
|
},
|
||||||
|
state_enums::OpenState,
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
@@ -13,9 +17,7 @@ cfg_if::cfg_if! {
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::auth::{RegisterRequest},
|
models::auth::{RegisterRequest},
|
||||||
endpoints::REGISTER,
|
endpoints::REGISTER,
|
||||||
state_enums::OpenState,
|
|
||||||
templates::get_api_path,
|
templates::get_api_path,
|
||||||
global_state::AppStateRx,
|
|
||||||
models::{
|
models::{
|
||||||
generic::GenericResponse
|
generic::GenericResponse
|
||||||
},
|
},
|
||||||
@@ -31,7 +33,6 @@ lazy_static! {
|
|||||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||||
#[rx(alias = "RegisterFormStateRx")]
|
#[rx(alias = "RegisterFormStateRx")]
|
||||||
struct RegisterFormState {
|
struct RegisterFormState {
|
||||||
username: String,
|
|
||||||
password: String,
|
password: String,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
registration_code: String,
|
registration_code: String,
|
||||||
@@ -42,7 +43,6 @@ struct RegisterFormState {
|
|||||||
impl RegisterFormStateRx {
|
impl RegisterFormStateRx {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
fn reset(&self) {
|
fn reset(&self) {
|
||||||
self.username.set(String::new());
|
|
||||||
self.password.set(String::new());
|
self.password.set(String::new());
|
||||||
self.nickname.set(String::new());
|
self.nickname.set(String::new());
|
||||||
self.registration_code.set(String::new());
|
self.registration_code.set(String::new());
|
||||||
@@ -64,6 +64,7 @@ fn register_form_capsule<G: Html>(
|
|||||||
state: &RegisterFormStateRx,
|
state: &RegisterFormStateRx,
|
||||||
props: RegisterFormProps,
|
props: RegisterFormProps,
|
||||||
) -> View<G> {
|
) -> View<G> {
|
||||||
|
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||||
let close_modal = move |_event: Event| {
|
let close_modal = move |_event: Event| {
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
{
|
{
|
||||||
@@ -81,7 +82,7 @@ fn register_form_capsule<G: Html>(
|
|||||||
let registration_code = state.registration_code.get().as_ref().clone();
|
let registration_code = state.registration_code.get().as_ref().clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
let register_info = RegisterRequest {
|
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(),
|
password: state.password.get().as_ref().clone(),
|
||||||
nickname: state.nickname.get().as_ref().clone(),
|
nickname: state.nickname.get().as_ref().clone(),
|
||||||
email: state.email.get().as_ref().clone(),
|
email: state.email.get().as_ref().clone(),
|
||||||
@@ -106,12 +107,6 @@ fn register_form_capsule<G: Html>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tentative username
|
|
||||||
global_state
|
|
||||||
.auth
|
|
||||||
.pending_username
|
|
||||||
.set((*state.username.get()).clone());
|
|
||||||
|
|
||||||
// Open login modal
|
// Open login modal
|
||||||
global_state.modals_open.login.set(OpenState::Open);
|
global_state.modals_open.login.set(OpenState::Open);
|
||||||
|
|
||||||
@@ -123,51 +118,58 @@ fn register_form_capsule<G: Html>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
dialog (class="modal-open modal modal-bottom sm:modal-middle"){
|
(match *global_state.modals_open.register.get() {
|
||||||
div (class="modal-box") {
|
OpenState::Open => { view! { cx,
|
||||||
// Header row - title and close button
|
dialog (class="modal-open modal modal-bottom sm:modal-middle"){
|
||||||
h3 (class="text-lg font-bold mb-4 text-center"){"Register"}
|
div (class="modal-box") {
|
||||||
button (on:click = close_modal, class = "btn btn-circle right-2 top-2 absolute") { CloseButtonSvg {} }
|
// 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
|
// Add component for handling error messages
|
||||||
ErrorBlock(error = state.error.clone())
|
ErrorBlock(error = state.error.clone())
|
||||||
|
|
||||||
// Username field
|
// Username field
|
||||||
div (class = "label") { span (class = "label-text") { "Username" } }
|
div (class = "label") { span (class = "label-text") { "Username" } }
|
||||||
input (bind:value = state.username, class = "input input-bordered w-full")
|
input (bind:value = global_state.auth.pending_username, class = "input input-bordered w-full")
|
||||||
|
|
||||||
// Password field
|
// Password field
|
||||||
div (class = "label") { span (class = "label-text") { "Password" } }
|
div (class = "label") { span (class = "label-text") { "Password" } }
|
||||||
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
input (bind:value = state.password, type = "password", class = "input input-bordered w-full")
|
||||||
|
|
||||||
(match props.registration_code {
|
(match props.registration_code {
|
||||||
true => { view! {cx,
|
true => { view! {cx,
|
||||||
div (class = "label") { span (class = "label-text") { "Registration Code" } }
|
div (class = "label") { span (class = "label-text") { "Registration Code" } }
|
||||||
input (bind:value = state.registration_code, class = "input input-bordered w-full")
|
input (bind:value = state.registration_code, class = "input input-bordered w-full")
|
||||||
}},
|
}},
|
||||||
false => {view!{cx,}},
|
false => {view!{cx,}},
|
||||||
})
|
})
|
||||||
(match props.nickname {
|
(match props.nickname {
|
||||||
true => { view! {cx,
|
true => { view! {cx,
|
||||||
div (class = "label") { span (class = "label-text") { "Nickname (Optional)" } }
|
div (class = "label") { span (class = "label-text") { "Nickname (Optional)" } }
|
||||||
input (bind:value = state.nickname, class = "input input-bordered w-full")
|
input (bind:value = state.nickname, class = "input input-bordered w-full")
|
||||||
}},
|
}},
|
||||||
false => {view!{cx,}},
|
false => {view!{cx,}},
|
||||||
})
|
})
|
||||||
(match props.email {
|
(match props.email {
|
||||||
true => { view! {cx,
|
true => { view! {cx,
|
||||||
div (class = "label") { span (class = "label-text") { "Email (Optional)" } }
|
div (class = "label") { span (class = "label-text") { "Email (Optional)" } }
|
||||||
input (bind:value = state.email, class = "input input-bordered w-full")
|
input (bind:value = state.email, class = "input input-bordered w-full")
|
||||||
}},
|
}},
|
||||||
false => {view!{cx,}},
|
false => {view!{cx,}},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Register button
|
// Register button
|
||||||
div (class = "flex justify-center mt-6") {
|
div (class = "flex justify-center mt-6") {
|
||||||
button (on:click = handle_register, class="btn"){"Register"}
|
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]
|
#[engine_only_fn]
|
||||||
async fn get_build_state(_info: StateGeneratorInfo<()>) -> RegisterFormState {
|
async fn get_build_state(_info: StateGeneratorInfo<()>) -> RegisterFormState {
|
||||||
RegisterFormState {
|
RegisterFormState {
|
||||||
username: String::new(),
|
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
error: String::new(),
|
error: String::new(),
|
||||||
nickname: 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 {
|
spawn_local_scoped(cx, async move {
|
||||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||||
global_state.auth.handle_log_out();
|
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);
|
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)]
|
#[cfg(client)]
|
||||||
global_state.auth.detect_state();
|
global_state.detect_state();
|
||||||
|
|
||||||
let content_state_header = content_state.clone();
|
let content_state_header = content_state.clone();
|
||||||
|
|
||||||
|
// Load cards from server, if user preference is set
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
{
|
global_state.check_dl_cards_locally(cx);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
// Main page header, including login functionality
|
// Main page header, including login functionality
|
||||||
@@ -89,55 +69,22 @@ pub fn Layout<'a, G: Html>(
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
section(class = "flex-2") {
|
section(class = "flex-2") {
|
||||||
// (match (*global_state.constants.card_table.get()).clone() {
|
(match (*global_state.constants.card_table.get()).clone() {
|
||||||
// Some(card_table) => { view!{ cx,
|
Some(card_table) => { view!{ cx,
|
||||||
// p { "DONE" }
|
p { "DONE" }
|
||||||
// } },
|
} },
|
||||||
// None => { view!{ cx, p { "Loading cards" } } },
|
None => { view!{ cx, p { "Loading cards" } } },
|
||||||
// })
|
})
|
||||||
|
|
||||||
(match *global_state.modals_open.login.get() {
|
(LOGIN_FORM.widget(cx, "", LoginFormProps{ remember_me: true, }))
|
||||||
OpenState::Open => {
|
(FORGOT_PASSWORD_FORM.widget(cx, "", ForgotPasswordFormProps{}))
|
||||||
view! { cx,
|
(REGISTER_FORM.widget(cx, "",
|
||||||
(LOGIN_FORM.widget(cx, "",
|
RegisterFormProps{
|
||||||
LoginFormProps{
|
registration_code: true,
|
||||||
remember_me: 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") {
|
main(style = "my-8") {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub const REGISTER: &str = "/api/register";
|
pub const REGISTER: &str = "/api/register";
|
||||||
pub const LOGIN: &str = "/api/login";
|
pub const LOGIN: &str = "/api/login";
|
||||||
|
pub const UPDATE_USER_PREFS: &str = "/api/user-prefs";
|
||||||
// TODO -> remove once it's used
|
// TODO -> remove once it's used
|
||||||
#[cfg(engine)]
|
#[cfg(engine)]
|
||||||
pub const LOGIN_TEST: &str = "/api/login-test";
|
pub const LOGIN_TEST: &str = "/api/login-test";
|
||||||
|
|||||||
@@ -3,3 +3,6 @@
|
|||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod user;
|
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
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5
|
||||||
|
|
||||||
pub use super::user::Entity as User;
|
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)]
|
#[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 {}
|
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,
|
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)]
|
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
|
||||||
#[rx(alias = "AppStateRx")]
|
#[rx(alias = "AppStateRx")]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
#[rx(nested)]
|
#[rx(nested)]
|
||||||
pub user_pref: UserPreferences,
|
pub user_prefs: UserPrefs,
|
||||||
#[rx(nested)]
|
#[rx(nested)]
|
||||||
pub constants: ConstData,
|
pub constants: ConstData,
|
||||||
#[rx(nested)]
|
#[rx(nested)]
|
||||||
@@ -25,8 +35,9 @@ pub struct AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
|
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
|
||||||
#[rx(alias = "UserPreferencesRx")]
|
#[rx(alias = "UserPrefsRx")]
|
||||||
pub struct UserPreferences {
|
pub struct UserPrefs {
|
||||||
|
pub loaded: bool,
|
||||||
pub local_card_data: bool,
|
pub local_card_data: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +87,8 @@ pub fn get_global_state_creator() -> GlobalStateCreator {
|
|||||||
#[engine_only_fn]
|
#[engine_only_fn]
|
||||||
pub async fn get_build_state() -> AppState {
|
pub async fn get_build_state() -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
user_pref: UserPreferences {
|
user_prefs: UserPrefs {
|
||||||
|
loaded: false,
|
||||||
local_card_data: false,
|
local_card_data: false,
|
||||||
},
|
},
|
||||||
constants: ConstData {
|
constants: ConstData {
|
||||||
@@ -123,12 +135,13 @@ impl AuthDataRx {
|
|||||||
let value = serde_json::to_string(&auth_info).unwrap();
|
let value = serde_json::to_string(&auth_info).unwrap();
|
||||||
storage.set_item("auth", &value).unwrap();
|
storage.set_item("auth", &value).unwrap();
|
||||||
|
|
||||||
// Save token to session storage
|
// Set user state
|
||||||
self.username.set(Some(auth_info.username.clone()));
|
self.username.set(Some(auth_info.username.clone()));
|
||||||
self.remember_me.set(Some(auth_info.remember_me));
|
self.remember_me.set(Some(auth_info.remember_me));
|
||||||
self.auth_info.set(Some(auth_info));
|
self.auth_info.set(Some(auth_info));
|
||||||
self.state.set(LoginState::Authenticated);
|
self.state.set(LoginState::Authenticated);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
pub fn handle_log_out(&self) {
|
pub fn handle_log_out(&self) {
|
||||||
// Delete persistent storage
|
// Delete persistent storage
|
||||||
@@ -148,12 +161,9 @@ impl AuthDataRx {
|
|||||||
self.remember_me.set(None);
|
self.remember_me.set(None);
|
||||||
self.state.set(LoginState::NotAuthenticated);
|
self.state.set(LoginState::NotAuthenticated);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Client only code to check if they're authenticated
|
#[cfg(client)]
|
||||||
#[cfg(client)]
|
fn detect_state(&self) {
|
||||||
impl AuthDataRx {
|
|
||||||
pub fn detect_state(&self) {
|
|
||||||
// If the user is in a known state, return
|
// If the user is in a known state, return
|
||||||
if let LoginState::Authenticated | LoginState::NotAuthenticated = *self.state.get() {
|
if let LoginState::Authenticated | LoginState::NotAuthenticated = *self.state.get() {
|
||||||
return;
|
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::serde::ts_seconds;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::models::user::UserPreferences;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct LoginInfo {
|
pub struct LoginInfo {
|
||||||
@@ -14,6 +15,7 @@ pub struct LoginResponse {
|
|||||||
pub token: String,
|
pub token: String,
|
||||||
#[serde(with = "ts_seconds")]
|
#[serde(with = "ts_seconds")]
|
||||||
pub expires: DateTime<Utc>,
|
pub expires: DateTime<Utc>,
|
||||||
|
pub prefs: UserPreferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use core::panic;
|
use core::panic;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
#[cfg(engine)]
|
||||||
use polars::prelude::*;
|
use polars::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -116,10 +118,11 @@ pub struct CardTable {
|
|||||||
pub sets: HashMap<u32, CardSet>,
|
pub sets: HashMap<u32, CardSet>,
|
||||||
pub archetypes: HashMap<String, ArchetypeInfo>,
|
pub archetypes: HashMap<String, ArchetypeInfo>,
|
||||||
pub monster_types: HashSet<String>,
|
pub monster_types: HashSet<String>,
|
||||||
pub df: DataFrame,
|
// pub df: DataFrame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardTable {
|
impl CardTable {
|
||||||
|
#[cfg(engine)]
|
||||||
pub fn df_to_card_info_test(df: DataFrame) -> Vec<CardInfo> {
|
pub fn df_to_card_info_test(df: DataFrame) -> Vec<CardInfo> {
|
||||||
let id_idx = df.get_column_index("id").unwrap();
|
let id_idx = df.get_column_index("id").unwrap();
|
||||||
let name_idx = df.get_column_index("name").unwrap();
|
let name_idx = df.get_column_index("name").unwrap();
|
||||||
@@ -394,8 +397,9 @@ impl CardTable {
|
|||||||
"ygoprodeck_url",
|
"ygoprodeck_url",
|
||||||
"linkval",
|
"linkval",
|
||||||
"race",
|
"race",
|
||||||
|
"typeline",
|
||||||
])])
|
])])
|
||||||
// Remove link markers, unless it's needed later
|
// TODO add link marker support
|
||||||
.select([col("*").exclude(["linkmarkers"])])
|
.select([col("*").exclude(["linkmarkers"])])
|
||||||
// TODO add banlist support
|
// TODO add banlist support
|
||||||
.select([col("*").exclude(["banlist_info"])])
|
.select([col("*").exclude(["banlist_info"])])
|
||||||
@@ -428,26 +432,26 @@ impl CardTable {
|
|||||||
sets,
|
sets,
|
||||||
archetypes,
|
archetypes,
|
||||||
monster_types,
|
monster_types,
|
||||||
df,
|
// df,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(client)]
|
#[cfg(client)]
|
||||||
pub fn new_from_client_json() -> Self {
|
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 cards = HashMap::new();
|
||||||
let sets = HashMap::new();
|
let sets = HashMap::new();
|
||||||
let archetypes = HashMap::new();
|
let archetypes = HashMap::new();
|
||||||
let monster_types = HashSet::new();
|
let monster_types = HashSet::new();
|
||||||
let df = DataFrame::new(vec![id_col]).unwrap();
|
// let df = DataFrame::new(vec![id_col]).unwrap();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cards,
|
cards,
|
||||||
sets,
|
sets,
|
||||||
archetypes,
|
archetypes,
|
||||||
monster_types,
|
monster_types,
|
||||||
df,
|
// df,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
Plain,
|
Standard,
|
||||||
Purrely,
|
Purrely,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
use sea_orm::Set;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::{From, Into};
|
||||||
|
|
||||||
|
use crate::entity::user_pref_entry;
|
||||||
|
|
||||||
use super::theme::Theme;
|
use super::theme::Theme;
|
||||||
|
|
||||||
@@ -7,3 +11,30 @@ pub struct UserPreferences {
|
|||||||
pub use_local_card_db: bool,
|
pub use_local_card_db: bool,
|
||||||
pub theme: Theme,
|
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::{
|
entity::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
user::{self},
|
user::{self},
|
||||||
|
user_pref_entry, user_to_user_pref,
|
||||||
},
|
},
|
||||||
models::{
|
models::{
|
||||||
auth::{Claims, LoginInfo, LoginResponse},
|
auth::{Claims, LoginInfo, LoginResponse},
|
||||||
generic::GenericResponse,
|
generic::GenericResponse,
|
||||||
|
theme::Theme,
|
||||||
|
user::UserPreferences,
|
||||||
},
|
},
|
||||||
server::server_state::ServerState,
|
server::server_state::ServerState,
|
||||||
};
|
};
|
||||||
@@ -15,7 +18,7 @@ use axum::{
|
|||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
};
|
};
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
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(
|
pub async fn credentials_are_correct(
|
||||||
username: &str,
|
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::{
|
use crate::{
|
||||||
entity::{prelude::*, user},
|
entity::{prelude::*, user, user_pref_entry, user_to_user_pref},
|
||||||
models::{auth::RegisterRequest, generic::GenericResponse},
|
models::{auth::RegisterRequest, generic::GenericResponse, user::UserPreferences},
|
||||||
server::server_state::ServerState,
|
server::server_state::ServerState,
|
||||||
};
|
};
|
||||||
use argon2::{
|
use argon2::{
|
||||||
@@ -72,8 +72,22 @@ pub async fn post_register_user(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let db_resp = user::Entity::insert(new_user).exec(&state.db_conn).await;
|
let db_resp = user::Entity::insert(new_user).exec(&state.db_conn).await;
|
||||||
match db_resp {
|
let user_id = match db_resp {
|
||||||
Ok(_) => {}
|
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(_) => {
|
Err(_) => {
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
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()))
|
(StatusCode::OK, Json(GenericResponse::ok()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod auth;
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod server_state;
|
pub mod server_state;
|
||||||
|
pub mod user;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// (Server only) Routes
|
// (Server only) Routes
|
||||||
use crate::endpoints::{CARD_INFO, FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER};
|
use crate::endpoints::{
|
||||||
use axum::routing::{get, post, Router};
|
CARD_INFO, FORGOT_PASSWORD, LOGIN, LOGIN_TEST, REGISTER, UPDATE_USER_PREFS,
|
||||||
|
};
|
||||||
|
use axum::routing::{get, post, put, Router};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::{
|
auth::{
|
||||||
@@ -10,6 +12,7 @@ use super::{
|
|||||||
},
|
},
|
||||||
constants::card_table::get_card_table,
|
constants::card_table::get_card_table,
|
||||||
server_state::ServerState,
|
server_state::ServerState,
|
||||||
|
user::update_prefs::put_user_prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn get_api_router(state: ServerState) -> Router {
|
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(LOGIN_TEST, post(post_test_login))
|
||||||
.route(FORGOT_PASSWORD, post(post_forgot_password))
|
.route(FORGOT_PASSWORD, post(post_forgot_password))
|
||||||
.route(CARD_INFO, get(get_card_table))
|
.route(CARD_INFO, get(get_card_table))
|
||||||
|
.route(UPDATE_USER_PREFS, put(put_user_prefs))
|
||||||
.with_state(state)
|
.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 crate::{components::layout::Layout, state_enums::ContentState};
|
||||||
use perseus::prelude::*;
|
use perseus::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use sycamore::prelude::*;
|
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,
|
view! { cx,
|
||||||
Layout(content_state = ContentState::User) {
|
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
|
// Anything we put in here will be rendered inside the `<main>` block of the layout
|
||||||
p { "Hello World!" }
|
(match *global_state.auth.state.get() {
|
||||||
br {}
|
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> {
|
pub fn get_template<G: Html>() -> Template<G> {
|
||||||
Template::build("user")
|
Template::build("user")
|
||||||
.view(user_index_page)
|
.build_state_fn(get_build_state)
|
||||||
|
.view_with_state(user_index_page)
|
||||||
.head(head)
|
.head(head)
|
||||||
.build()
|
.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