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"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1786,12 +1788,14 @@ dependencies = [
|
||||||
"chrono-humanize",
|
"chrono-humanize",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"futures",
|
"futures",
|
||||||
|
"getrandom 0.3.4",
|
||||||
"gloo 0.10.0",
|
"gloo 0.10.0",
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"plan-proto",
|
"plan-proto",
|
||||||
"postcard",
|
"postcard",
|
||||||
|
"rand 0.9.2",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ web-sys = { version = "0.3.77", features = [
|
||||||
"ReadableStreamDefaultReader",
|
"ReadableStreamDefaultReader",
|
||||||
"HtmlDialogElement",
|
"HtmlDialogElement",
|
||||||
"DomRect",
|
"DomRect",
|
||||||
|
"ShareData",
|
||||||
|
"Clipboard",
|
||||||
] }
|
] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
yew = { version = "0.21", features = ["csr"] }
|
yew = { version = "0.21", features = ["csr"] }
|
||||||
|
|
@ -42,3 +44,5 @@ thiserror = { version = "2" }
|
||||||
plan-proto = { path = "../plan-proto", features = ["client"] }
|
plan-proto = { path = "../plan-proto", features = ["client"] }
|
||||||
ciborium = { version = "0.2" }
|
ciborium = { version = "0.2" }
|
||||||
chrono-humanize = { version = "0.2.3", features = ["wasmbind"] }
|
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 {
|
.dialog-box {
|
||||||
|
|
||||||
|
|
||||||
font-size: 2rem;
|
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -372,6 +369,8 @@ dialog::backdrop {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -382,7 +381,6 @@ dialog::backdrop {
|
||||||
|
|
||||||
&>button {
|
&>button {
|
||||||
min-width: 4cm;
|
min-width: 4cm;
|
||||||
font-size: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$close_color: rgba(255, 0, 0, 1);
|
$close_color: rgba(255, 0, 0, 1);
|
||||||
|
|
@ -432,11 +430,13 @@ dialog::backdrop {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
color: white;
|
color: white;
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit {
|
.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 {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -546,21 +555,28 @@ dialog::backdrop {
|
||||||
}
|
}
|
||||||
|
|
||||||
.splash {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
min-height: 50vh;
|
||||||
|
|
||||||
.options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1cm;
|
|
||||||
|
|
||||||
& button {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 plan_proto::token::Token;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::{components::dialog::Dialog, storage::StorageKey};
|
use crate::{
|
||||||
|
components::dialog_modal::{DialogModal, DialogMode},
|
||||||
|
storage::StorageKey,
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Nav() -> Html {
|
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 logged_in_buttons = token.is_some().then(|| {
|
||||||
let confirm_signout = {
|
let signout = {
|
||||||
let dialog = dialog.clone();
|
let callback = Callback::from(move |_| {
|
||||||
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" => {
|
|
||||||
Token::delete();
|
Token::delete();
|
||||||
let _ = gloo::utils::window().location().reload();
|
let _ = gloo::utils::window().location().reload();
|
||||||
}
|
|
||||||
"no" => {
|
|
||||||
dialog.set(false);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
});
|
});
|
||||||
let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]);
|
|
||||||
html! {
|
html! {
|
||||||
<Dialog
|
<DialogModal
|
||||||
options={options}
|
id="signout-dialog"
|
||||||
cancel_callback={Some(cancel_signout)}
|
mode={DialogMode::ConfirmOrClose(callback)}
|
||||||
callback={callback}
|
close_backdrop=true
|
||||||
|
button_content={html!{"sign out"}}
|
||||||
>
|
>
|
||||||
<p>{"really sign out?"}</p>
|
<p>{"really sign out?"}</p>
|
||||||
</Dialog>
|
</DialogModal>
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div class="right-side">
|
<div class="right-side">
|
||||||
<a href="/user/settings"><button>{"⚙️"}</button></a>
|
<a href="/user/settings"><button>{"⚙️"}</button></a>
|
||||||
<button onclick={confirm_signout} >{"sign out"}</button>
|
{signout}
|
||||||
</div>
|
</div>
|
||||||
<a href="/plans/new"><button>{"new plan"}</button></a>
|
<a href="/plans/new"><button>{"new plan"}</button></a>
|
||||||
{dialog}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ mod pages {
|
||||||
pub mod user_settings_page;
|
pub mod user_settings_page;
|
||||||
}
|
}
|
||||||
mod components {
|
mod components {
|
||||||
pub mod dialog;
|
|
||||||
pub mod dialog_modal;
|
pub mod dialog_modal;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod half_hour_range_select;
|
pub mod half_hour_range_select;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
use core::{num::NonZeroU8, time::Duration};
|
use core::{num::NonZeroU8, time::Duration};
|
||||||
use std::rc::Rc;
|
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 futures::{FutureExt, StreamExt, lock::Mutex, select};
|
||||||
use gloo::net::http::Request;
|
use gloo::net::http::Request;
|
||||||
use plan_proto::{
|
use plan_proto::{
|
||||||
|
|
@ -169,10 +173,15 @@ pub fn MainPage() -> Html {
|
||||||
pub fn SignedOutMainPage() -> Html {
|
pub fn SignedOutMainPage() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<div class="splash">
|
<div class="splash">
|
||||||
<h1>{"plan"}</h1>
|
<div class="section">
|
||||||
<div class="options">
|
<h1 class="instruction">{"create new account"}</h1>
|
||||||
<a href="/signin"><button>{"sign in"}</button></a>
|
<h2 class="instruction">{"with just a username"}</h2>
|
||||||
<a href="/signup"><button>{"sign up"}</button></a>
|
<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>
|
||||||
</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 std::rc::Rc;
|
||||||
|
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use chrono_humanize::{HumanTime, Humanize, Tense};
|
use chrono_humanize::{HumanTime, Tense};
|
||||||
use core::ops::Deref;
|
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 gloo::net::websocket::{Message, futures::WebSocket};
|
||||||
use plan_proto::{
|
use plan_proto::{
|
||||||
error::ServerError,
|
error::ServerError,
|
||||||
message::{ClientMessage, ServerMessage},
|
message::{ClientMessage, ServerMessage},
|
||||||
plan::{Plan, PlanId, UpdateTiles},
|
plan::{Plan, PlanId},
|
||||||
token::{Token, TokenLogin},
|
token::{Token, TokenLogin},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
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 yew::{platform::pinned::mpsc::UnboundedReceiver, prelude::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
WS_URL,
|
WS_URL,
|
||||||
components::{error::ErrorDisplay, planview::PlanView},
|
components::{error::ErrorDisplay, planview::PlanView},
|
||||||
pages::{mainpage::SignedOutMainPage, not_found::NotFound},
|
pages::{mainpage::SignedOutMainPage, not_found::NotFound},
|
||||||
request::RequestError,
|
|
||||||
storage::{LastPlanVisitedLoggedOut, StorageKey},
|
storage::{LastPlanVisitedLoggedOut, StorageKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,41 +168,6 @@ impl Session {
|
||||||
let ws = WebSocket::open(url)
|
let ws = WebSocket::open(url)
|
||||||
.map_err(|err| ConnectionError::SocketOpeningError(err.to_string()))?;
|
.map_err(|err| ConnectionError::SocketOpeningError(err.to_string()))?;
|
||||||
Ok(ws)
|
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)]
|
#[allow(clippy::await_holding_refcell_ref)]
|
||||||
|
|
@ -314,6 +280,70 @@ impl Session {
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
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 plan_state = use_state_eq(|| PlanState::Loading);
|
||||||
let token = match use_context::<Token>() {
|
let token = match use_context::<Token>() {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
|
|
@ -330,9 +360,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
||||||
let error_obj = html! {
|
let error_obj = html! {
|
||||||
<ErrorDisplay state={error_state.clone()}/>
|
<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, recv) = yew::platform::pinned::mpsc::unbounded();
|
||||||
let send = use_state(|| send);
|
let send = use_state(|| send);
|
||||||
let recv = use_mut_ref(|| recv);
|
let recv = use_mut_ref(|| recv);
|
||||||
|
|
@ -381,6 +409,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html {
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="plan">
|
<div class="plan">
|
||||||
|
{share_button}
|
||||||
{content}
|
{content}
|
||||||
{error_obj}
|
{error_obj}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use crate::{
|
||||||
state_input::{InputType, StateInput},
|
state_input::{InputType, StateInput},
|
||||||
},
|
},
|
||||||
error::JsError,
|
error::JsError,
|
||||||
|
storage::{LoginState, StorageKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -19,8 +20,17 @@ pub fn Signin() -> Html {
|
||||||
let error_obj = html! {
|
let error_obj = html! {
|
||||||
<ErrorDisplay state={error_state.clone()}/>
|
<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 (submit_username, submit_password) = (username_state.clone(), password_state.clone());
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
|
|
@ -96,8 +106,7 @@ pub fn Signin() -> Html {
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<div class="signin" onkeyup={on_key_up}>
|
<div class="signin" onkeyup={on_key_up}>
|
||||||
<h1>{"signin"}</h1>
|
<div class="login-fields">
|
||||||
<div class="fields">
|
|
||||||
<label>{"username"}</label>
|
<label>{"username"}</label>
|
||||||
<StateInput name="username" id={Some("username".to_string())} state={username_state}/>
|
<StateInput name="username" id={Some("username".to_string())} state={username_state}/>
|
||||||
<label>{"password"}</label>
|
<label>{"password"}</label>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
use gloo::net::http::Request;
|
use gloo::net::http::Request;
|
||||||
use plan_proto::{
|
use plan_proto::{
|
||||||
error::ServerError, limited::ClampedString, message::ServerMessage, token::Token,
|
error::ServerError,
|
||||||
user::UserLogin,
|
limited::ClampedString,
|
||||||
|
message::ServerMessage,
|
||||||
|
token::Token,
|
||||||
|
user::{Password, UserLogin},
|
||||||
};
|
};
|
||||||
|
use rand::distr::SampleString;
|
||||||
use wasm_bindgen::{JsCast, convert::OptionIntoWasmAbi};
|
use wasm_bindgen::{JsCast, convert::OptionIntoWasmAbi};
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
HtmlElement, ReadableStreamDefaultReader,
|
HtmlElement, ReadableStreamDefaultReader,
|
||||||
|
|
@ -18,7 +22,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
error::JsError,
|
error::JsError,
|
||||||
login,
|
login,
|
||||||
storage::StorageKey,
|
storage::{LoginState, StorageKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -29,14 +33,23 @@ pub fn Signup() -> Html {
|
||||||
<ErrorDisplay state={error_state.clone()}/>
|
<ErrorDisplay state={error_state.clone()}/>
|
||||||
};
|
};
|
||||||
let username_state = use_state(String::new);
|
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 on_submit = {
|
||||||
|
let username_state = username_state.clone();
|
||||||
let err_set = error_set.clone();
|
let err_set = error_set.clone();
|
||||||
Callback::from(move |_| {
|
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 {
|
let user = UserLogin {
|
||||||
username: match ClampedString::new((*submit_username).clone()) {
|
username: match ClampedString::new((*username_state).clone()) {
|
||||||
Ok(username) => username,
|
Ok(username) => username,
|
||||||
Err(range) => {
|
Err(range) => {
|
||||||
err_set.set(Some(format!(
|
err_set.set(Some(format!(
|
||||||
|
|
@ -46,16 +59,7 @@ pub fn Signup() -> Html {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
password: match ClampedString::new((*submit_password).clone()) {
|
password,
|
||||||
Ok(pass) => pass,
|
|
||||||
Err(range) => {
|
|
||||||
err_set.set(Some(format!(
|
|
||||||
"password length must be between {} characters",
|
|
||||||
range.bounds_string_human()
|
|
||||||
)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
let mut user_serialized = vec![];
|
let mut user_serialized = vec![];
|
||||||
if let Err(err) = ciborium::into_writer(&user, &mut user_serialized) {
|
if let Err(err) = ciborium::into_writer(&user, &mut user_serialized) {
|
||||||
|
|
@ -72,6 +76,17 @@ pub fn Signup() -> Html {
|
||||||
match req.send().await {
|
match req.send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
if resp.ok() {
|
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(
|
crate::login::login(
|
||||||
user_serialized.into_boxed_slice(),
|
user_serialized.into_boxed_slice(),
|
||||||
error_set,
|
error_set,
|
||||||
|
|
@ -136,12 +151,9 @@ pub fn Signup() -> Html {
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<div class="signup" onkeyup={on_key_up}>
|
<div class="signup" onkeyup={on_key_up}>
|
||||||
<h1>{"signup"}</h1>
|
<div class="login-fields">
|
||||||
<div class="fields">
|
<label for="username">{"username"}</label>
|
||||||
<label>{"username"}</label>
|
|
||||||
<StateInput name="username" id={Some("username".to_string())} state={username_state}/>
|
<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>
|
</div>
|
||||||
<button class="submit" onclick={on_submit}>{"submit"}</button>
|
<button class="submit" onclick={on_submit}>{"submit"}</button>
|
||||||
{error_obj}
|
{error_obj}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use gloo::storage::{LocalStorage, Storage, errors::StorageError};
|
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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::JsError;
|
|
||||||
|
|
||||||
pub trait StorageKey: for<'a> Deserialize<'a> + Serialize {
|
pub trait StorageKey: for<'a> Deserialize<'a> + Serialize {
|
||||||
const KEY: &str;
|
const KEY: &str;
|
||||||
|
|
||||||
|
|
@ -28,3 +30,16 @@ pub struct LastPlanVisitedLoggedOut(pub PlanId);
|
||||||
impl StorageKey for LastPlanVisitedLoggedOut {
|
impl StorageKey for LastPlanVisitedLoggedOut {
|
||||||
const KEY: &str = "last-plan-visited-logged-out";
|
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