auto use pregenerated passwords (down to just 1 field now)
This commit is contained in:
parent
68c0d0e4b3
commit
3083fb17d9
|
|
@ -694,9 +694,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1786,12 +1788,14 @@ dependencies = [
|
|||
"chrono-humanize",
|
||||
"ciborium",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"gloo 0.10.0",
|
||||
"instant",
|
||||
"log",
|
||||
"once_cell",
|
||||
"plan-proto",
|
||||
"postcard",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"thiserror 2.0.17",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ web-sys = { version = "0.3.77", features = [
|
|||
"ReadableStreamDefaultReader",
|
||||
"HtmlDialogElement",
|
||||
"DomRect",
|
||||
"ShareData",
|
||||
"Clipboard",
|
||||
] }
|
||||
log = "0.4"
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
|
|
@ -42,3 +44,5 @@ thiserror = { version = "2" }
|
|||
plan-proto = { path = "../plan-proto", features = ["client"] }
|
||||
ciborium = { version = "0.2" }
|
||||
chrono-humanize = { version = "0.2.3", features = ["wasmbind"] }
|
||||
rand = { version = "0.9" }
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
|
|
|
|||
|
|
@ -356,9 +356,6 @@ dialog::backdrop {
|
|||
|
||||
|
||||
.dialog-box {
|
||||
|
||||
|
||||
font-size: 2rem;
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -372,6 +369,8 @@ dialog::backdrop {
|
|||
gap: 5px;
|
||||
color: white;
|
||||
|
||||
font-size: 1.5em;
|
||||
|
||||
.options {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
|
|
@ -382,7 +381,6 @@ dialog::backdrop {
|
|||
|
||||
&>button {
|
||||
min-width: 4cm;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
$close_color: rgba(255, 0, 0, 1);
|
||||
|
|
@ -432,11 +430,13 @@ dialog::backdrop {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
gap: 10px;
|
||||
|
||||
input {
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.submit {
|
||||
|
|
@ -458,6 +458,15 @@ dialog::backdrop {
|
|||
}
|
||||
}
|
||||
|
||||
.login-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -546,21 +555,28 @@ dialog::backdrop {
|
|||
}
|
||||
|
||||
.splash {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
gap: 5vw;
|
||||
width: 100%;
|
||||
|
||||
.section {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1cm;
|
||||
|
||||
& button {
|
||||
font-size: 2rem;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -727,3 +743,20 @@ select {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
border: 1px solid white;
|
||||
padding: 20px;
|
||||
// font-size: 2em;
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
dialog:has(.notification) {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.share-button {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct DialogProps {
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub options: Box<[String]>,
|
||||
#[prop_or_default]
|
||||
pub cancel_callback: Option<Callback<()>>,
|
||||
pub callback: Callback<String>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Dialog(
|
||||
DialogProps {
|
||||
children,
|
||||
options,
|
||||
cancel_callback,
|
||||
callback,
|
||||
}: &DialogProps,
|
||||
) -> Html {
|
||||
let options = options
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
let callback = callback.clone();
|
||||
let option = opt.clone();
|
||||
let cb = Callback::from(move |_| {
|
||||
callback.emit(option.clone());
|
||||
});
|
||||
html! {
|
||||
<button onclick={cb}>{opt.clone()}</button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let backdrop_click = cancel_callback.clone().map(|cancel_callback| {
|
||||
Callback::from(move |_| {
|
||||
cancel_callback.emit(());
|
||||
})
|
||||
});
|
||||
html! {
|
||||
<div class="click-backdrop" onclick={backdrop_click}>
|
||||
<div class="dialog">
|
||||
<div class="dialog-box">
|
||||
<div class="message">
|
||||
{children.clone()}
|
||||
</div>
|
||||
<div class="options">
|
||||
{options}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
use plan_proto::token::Token;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::{components::dialog::Dialog, storage::StorageKey};
|
||||
use crate::{
|
||||
components::dialog_modal::{DialogModal, DialogMode},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
||||
#[function_component]
|
||||
pub fn Nav() -> Html {
|
||||
|
|
@ -23,51 +26,30 @@ pub fn Nav() -> Html {
|
|||
</>
|
||||
}
|
||||
});
|
||||
let dialog = use_state(|| false);
|
||||
let logged_in_buttons = token.is_some().then(|| {
|
||||
let confirm_signout = {
|
||||
let dialog = dialog.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(true);
|
||||
})
|
||||
};
|
||||
|
||||
let dialog = dialog.then(|| {
|
||||
let cancel_signout = {
|
||||
let dialog = dialog.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(false);
|
||||
})
|
||||
};
|
||||
let callback = Callback::from(move |option: String| match option.as_str() {
|
||||
"yes" => {
|
||||
let signout = {
|
||||
let callback = Callback::from(move |_| {
|
||||
Token::delete();
|
||||
let _ = gloo::utils::window().location().reload();
|
||||
}
|
||||
"no" => {
|
||||
dialog.set(false);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]);
|
||||
html! {
|
||||
<Dialog
|
||||
options={options}
|
||||
cancel_callback={Some(cancel_signout)}
|
||||
callback={callback}
|
||||
<DialogModal
|
||||
id="signout-dialog"
|
||||
mode={DialogMode::ConfirmOrClose(callback)}
|
||||
close_backdrop=true
|
||||
button_content={html!{"sign out"}}
|
||||
>
|
||||
<p>{"really sign out?"}</p>
|
||||
</Dialog>
|
||||
</DialogModal>
|
||||
}
|
||||
});
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<div class="right-side">
|
||||
<a href="/user/settings"><button>{"⚙️"}</button></a>
|
||||
<button onclick={confirm_signout} >{"sign out"}</button>
|
||||
{signout}
|
||||
</div>
|
||||
<a href="/plans/new"><button>{"new plan"}</button></a>
|
||||
{dialog}
|
||||
</>
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ mod pages {
|
|||
pub mod user_settings_page;
|
||||
}
|
||||
mod components {
|
||||
pub mod dialog;
|
||||
pub mod dialog_modal;
|
||||
pub mod error;
|
||||
pub mod half_hour_range_select;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use core::{num::NonZeroU8, time::Duration};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{SERVER_URL, storage::StorageKey};
|
||||
use crate::{
|
||||
SERVER_URL,
|
||||
pages::{signin::Signin, signup::Signup},
|
||||
storage::StorageKey,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, lock::Mutex, select};
|
||||
use gloo::net::http::Request;
|
||||
use plan_proto::{
|
||||
|
|
@ -169,10 +173,15 @@ pub fn MainPage() -> Html {
|
|||
pub fn SignedOutMainPage() -> Html {
|
||||
html! {
|
||||
<div class="splash">
|
||||
<h1>{"plan"}</h1>
|
||||
<div class="options">
|
||||
<a href="/signin"><button>{"sign in"}</button></a>
|
||||
<a href="/signup"><button>{"sign up"}</button></a>
|
||||
<div class="section">
|
||||
<h1 class="instruction">{"create new account"}</h1>
|
||||
<h2 class="instruction">{"with just a username"}</h2>
|
||||
<Signup />
|
||||
</div>
|
||||
<div class="section">
|
||||
<h1 class="instruction">{"or sign in with an account"}</h1>
|
||||
<h3 class="instruction">{"that has a password set"}</h3>
|
||||
<Signin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
use core::{cell::RefCell, ops::Not, sync::atomic::AtomicBool, time::Duration};
|
||||
use core::{cell::RefCell, sync::atomic::AtomicBool, time::Duration};
|
||||
use std::rc::Rc;
|
||||
|
||||
use chrono::TimeDelta;
|
||||
use chrono_humanize::{HumanTime, Humanize, Tense};
|
||||
use chrono_humanize::{HumanTime, Tense};
|
||||
use core::ops::Deref;
|
||||
use futures::{FutureExt, SinkExt, StreamExt, channel::mpsc::Receiver, stream::Fuse};
|
||||
use futures::{SinkExt, StreamExt, stream::Fuse};
|
||||
use gloo::net::websocket::{Message, futures::WebSocket};
|
||||
use plan_proto::{
|
||||
error::ServerError,
|
||||
message::{ClientMessage, ServerMessage},
|
||||
plan::{Plan, PlanId, UpdateTiles},
|
||||
plan::{Plan, PlanId},
|
||||
token::{Token, TokenLogin},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsError;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{HtmlDialogElement, ShareData, js_sys::Reflect};
|
||||
use yew::{platform::pinned::mpsc::UnboundedReceiver, prelude::*};
|
||||
|
||||
use crate::{
|
||||
WS_URL,
|
||||
components::{error::ErrorDisplay, planview::PlanView},
|
||||
pages::{mainpage::SignedOutMainPage, not_found::NotFound},
|
||||
request::RequestError,
|
||||
storage::{LastPlanVisitedLoggedOut, StorageKey},
|
||||
};
|
||||
|
||||
|
|
@ -167,41 +168,6 @@ impl Session {
|
|||
let ws = WebSocket::open(url)
|
||||
.map_err(|err| ConnectionError::SocketOpeningError(err.to_string()))?;
|
||||
Ok(ws)
|
||||
// struct WsConnectFuture(RefCell<Option<WebSocket>>);
|
||||
// let ws_fut = WsConnectFuture(RefCell::new(Some(ws)));
|
||||
// impl Future for WsConnectFuture {
|
||||
// type Output = Result<WebSocket, ConnectionError>;
|
||||
|
||||
// fn poll(
|
||||
// self: std::pin::Pin<&mut Self>,
|
||||
// cx: &mut std::task::Context<'_>,
|
||||
// ) -> std::task::Poll<Self::Output> {
|
||||
// let ws = match self.0.try_borrow_mut() {
|
||||
// Ok(ws) => ws,
|
||||
// Err(_) => {
|
||||
// log::warn!("ref mut");
|
||||
// cx.waker().wake_by_ref();
|
||||
// return std::task::Poll::Pending;
|
||||
// }
|
||||
// };
|
||||
// match ws.as_ref().map(|ws| ws.state()) {
|
||||
// Some(gloo::net::websocket::State::Connecting) => {
|
||||
// cx.waker().wake_by_ref();
|
||||
// std::task::Poll::Pending
|
||||
// }
|
||||
// Some(gloo::net::websocket::State::Open) => {
|
||||
// std::task::Poll::Ready(Ok(self.0.take().unwrap()))
|
||||
// }
|
||||
// Some(gloo::net::websocket::State::Closing)
|
||||
// | Some(gloo::net::websocket::State::Closed) => {
|
||||
// std::task::Poll::Ready(Err(ConnectionError::SocketClosed))
|
||||
// }
|
||||
// None => std::task::Poll::Pending,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// ws_fut.await
|
||||
}
|
||||
|
||||
#[allow(clippy::await_holding_refcell_ref)]
|
||||
|
|
@ -314,6 +280,70 @@ impl Session {
|
|||
|
||||
#[function_component]
|
||||
pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
||||
const SHARE_SUCCESS_ID: &str = "share-link-copied";
|
||||
let share_button = {
|
||||
let share_cb = {
|
||||
Callback::from(move |_| {
|
||||
if let Some(href) = gloo::utils::document()
|
||||
.location()
|
||||
.and_then(|a| a.href().ok())
|
||||
{
|
||||
let share_data = ShareData::new();
|
||||
share_data.set_url(&href);
|
||||
yew::platform::spawn_local(async move {
|
||||
let nav = gloo::utils::window().navigator();
|
||||
let can_share = Reflect::get(&nav, &JsValue::from_str("share"))
|
||||
.map(|v| !v.is_undefined())
|
||||
.unwrap_or_default();
|
||||
|
||||
if can_share && nav.can_share_with_data(&share_data) {
|
||||
if let Err(err) =
|
||||
Into::<JsFuture>::into(nav.share_with_data(&share_data)).await
|
||||
{
|
||||
log::error!("share url [{href}]: {err:?}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// not supported on this platform;
|
||||
// copy url to clipboard and show the copied link modal
|
||||
if let Err(err) = Into::<JsFuture>::into(
|
||||
gloo::utils::window()
|
||||
.navigator()
|
||||
.clipboard()
|
||||
.write_text(&href),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::error!("error writing to clipboard: {err:?}");
|
||||
return;
|
||||
}
|
||||
let Some(dialog) = gloo::utils::document()
|
||||
.get_element_by_id(SHARE_SUCCESS_ID)
|
||||
.and_then(|d| d.dyn_into::<HtmlDialogElement>().ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = dialog.show_modal() {
|
||||
log::error!("show success modal: {err:?}");
|
||||
return;
|
||||
}
|
||||
gloo::timers::future::sleep(Duration::from_secs(1)).await;
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<button onclick={share_cb} class="share-button">{"share"}</button>
|
||||
<dialog id={SHARE_SUCCESS_ID}>
|
||||
<div class="notification">
|
||||
<p>{"link copied to clipboard"}</p>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
}
|
||||
};
|
||||
let plan_state = use_state_eq(|| PlanState::Loading);
|
||||
let token = match use_context::<Token>() {
|
||||
Some(token) => token,
|
||||
|
|
@ -330,9 +360,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
|||
let error_obj = html! {
|
||||
<ErrorDisplay state={error_state.clone()}/>
|
||||
};
|
||||
let on_error = use_callback(error_state.setter(), |err: Option<String>, err_state| {
|
||||
err_state.set(err);
|
||||
});
|
||||
|
||||
let (send, recv) = yew::platform::pinned::mpsc::unbounded();
|
||||
let send = use_state(|| send);
|
||||
let recv = use_mut_ref(|| recv);
|
||||
|
|
@ -381,6 +409,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
|||
|
||||
html! {
|
||||
<div class="plan">
|
||||
{share_button}
|
||||
{content}
|
||||
{error_obj}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
state_input::{InputType, StateInput},
|
||||
},
|
||||
error::JsError,
|
||||
storage::{LoginState, StorageKey},
|
||||
};
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -19,8 +20,17 @@ pub fn Signin() -> Html {
|
|||
let error_obj = html! {
|
||||
<ErrorDisplay state={error_state.clone()}/>
|
||||
};
|
||||
let username_state = use_state(String::new);
|
||||
let password_state = use_state(String::new);
|
||||
|
||||
let (u, p) = if let Ok(LoginState::Passwordless { username, password }) =
|
||||
LoginState::load_from_storage()
|
||||
{
|
||||
(username.to_string(), password.to_string())
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
|
||||
let username_state = use_state(|| u);
|
||||
let password_state = use_state(|| p);
|
||||
|
||||
let (submit_username, submit_password) = (username_state.clone(), password_state.clone());
|
||||
let on_submit = {
|
||||
|
|
@ -96,8 +106,7 @@ pub fn Signin() -> Html {
|
|||
};
|
||||
html! {
|
||||
<div class="signin" onkeyup={on_key_up}>
|
||||
<h1>{"signin"}</h1>
|
||||
<div class="fields">
|
||||
<div class="login-fields">
|
||||
<label>{"username"}</label>
|
||||
<StateInput name="username" id={Some("username".to_string())} state={username_state}/>
|
||||
<label>{"password"}</label>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
use gloo::net::http::Request;
|
||||
use plan_proto::{
|
||||
error::ServerError, limited::ClampedString, message::ServerMessage, token::Token,
|
||||
user::UserLogin,
|
||||
error::ServerError,
|
||||
limited::ClampedString,
|
||||
message::ServerMessage,
|
||||
token::Token,
|
||||
user::{Password, UserLogin},
|
||||
};
|
||||
use rand::distr::SampleString;
|
||||
use wasm_bindgen::{JsCast, convert::OptionIntoWasmAbi};
|
||||
use web_sys::{
|
||||
HtmlElement, ReadableStreamDefaultReader,
|
||||
|
|
@ -18,7 +22,7 @@ use crate::{
|
|||
},
|
||||
error::JsError,
|
||||
login,
|
||||
storage::StorageKey,
|
||||
storage::{LoginState, StorageKey},
|
||||
};
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -29,14 +33,23 @@ pub fn Signup() -> Html {
|
|||
<ErrorDisplay state={error_state.clone()}/>
|
||||
};
|
||||
let username_state = use_state(String::new);
|
||||
let password_state = use_state(String::new);
|
||||
|
||||
let (submit_username, submit_password) = (username_state.clone(), password_state.clone());
|
||||
let on_submit = {
|
||||
let username_state = username_state.clone();
|
||||
let err_set = error_set.clone();
|
||||
Callback::from(move |_| {
|
||||
let password = match Password::new(
|
||||
rand::distr::Alphanumeric.sample_string(&mut rand::rng(), Password::MAX_LEN),
|
||||
) {
|
||||
Ok(password) => password,
|
||||
Err(err) => {
|
||||
err_set.set(Some(format!("generated password not in range: {err:?}",)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let user = UserLogin {
|
||||
username: match ClampedString::new((*submit_username).clone()) {
|
||||
username: match ClampedString::new((*username_state).clone()) {
|
||||
Ok(username) => username,
|
||||
Err(range) => {
|
||||
err_set.set(Some(format!(
|
||||
|
|
@ -46,16 +59,7 @@ pub fn Signup() -> Html {
|
|||
return;
|
||||
}
|
||||
},
|
||||
password: match ClampedString::new((*submit_password).clone()) {
|
||||
Ok(pass) => pass,
|
||||
Err(range) => {
|
||||
err_set.set(Some(format!(
|
||||
"password length must be between {} characters",
|
||||
range.bounds_string_human()
|
||||
)));
|
||||
return;
|
||||
}
|
||||
},
|
||||
password,
|
||||
};
|
||||
let mut user_serialized = vec![];
|
||||
if let Err(err) = ciborium::into_writer(&user, &mut user_serialized) {
|
||||
|
|
@ -72,6 +76,17 @@ pub fn Signup() -> Html {
|
|||
match req.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.ok() {
|
||||
// Save the created credentials
|
||||
let login_state = LoginState::Passwordless {
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
};
|
||||
if let Err(err) = login_state.save_to_storage() {
|
||||
error_set.set(Some(format!(
|
||||
"failed saving generated credentials: {err}"
|
||||
)));
|
||||
return;
|
||||
}
|
||||
crate::login::login(
|
||||
user_serialized.into_boxed_slice(),
|
||||
error_set,
|
||||
|
|
@ -136,12 +151,9 @@ pub fn Signup() -> Html {
|
|||
};
|
||||
html! {
|
||||
<div class="signup" onkeyup={on_key_up}>
|
||||
<h1>{"signup"}</h1>
|
||||
<div class="fields">
|
||||
<label>{"username"}</label>
|
||||
<div class="login-fields">
|
||||
<label for="username">{"username"}</label>
|
||||
<StateInput name="username" id={Some("username".to_string())} state={username_state}/>
|
||||
<label>{"password"}</label>
|
||||
<StateInput name="password" id={Some("password".to_string())} state={password_state} input_type={InputType::Password}/>
|
||||
</div>
|
||||
<button class="submit" onclick={on_submit}>{"submit"}</button>
|
||||
{error_obj}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use gloo::storage::{LocalStorage, Storage, errors::StorageError};
|
||||
use plan_proto::{plan::PlanId, token::Token, user::UserLogin};
|
||||
use plan_proto::{
|
||||
plan::PlanId,
|
||||
token::Token,
|
||||
user::{Password, Username},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::JsError;
|
||||
|
||||
pub trait StorageKey: for<'a> Deserialize<'a> + Serialize {
|
||||
const KEY: &str;
|
||||
|
||||
|
|
@ -28,3 +30,16 @@ pub struct LastPlanVisitedLoggedOut(pub PlanId);
|
|||
impl StorageKey for LastPlanVisitedLoggedOut {
|
||||
const KEY: &str = "last-plan-visited-logged-out";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum LoginState {
|
||||
#[default]
|
||||
NoLogin,
|
||||
Passwordless {
|
||||
username: Username,
|
||||
password: Password,
|
||||
},
|
||||
}
|
||||
impl StorageKey for LoginState {
|
||||
const KEY: &str = "login-state";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue