add password changing
This commit is contained in:
parent
056cb951a2
commit
68c0d0e4b3
|
|
@ -737,25 +737,6 @@ dependencies = [
|
||||||
"gloo-worker 0.4.0",
|
"gloo-worker 0.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-console 0.3.0",
|
|
||||||
"gloo-dialogs 0.2.0",
|
|
||||||
"gloo-events 0.2.0",
|
|
||||||
"gloo-file 0.3.0",
|
|
||||||
"gloo-history 0.2.2",
|
|
||||||
"gloo-net 0.5.0",
|
|
||||||
"gloo-render 0.2.0",
|
|
||||||
"gloo-storage 0.3.0",
|
|
||||||
"gloo-timers 0.3.0",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"gloo-worker 0.5.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-console"
|
name = "gloo-console"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|
@ -922,27 +903,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-net"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"http 0.2.12",
|
|
||||||
"js-sys",
|
|
||||||
"pin-project",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-render"
|
name = "gloo-render"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -1077,25 +1037,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-worker"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d"
|
|
||||||
dependencies = [
|
|
||||||
"bincode",
|
|
||||||
"futures",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"gloo-worker-macros",
|
|
||||||
"js-sys",
|
|
||||||
"pinned",
|
|
||||||
"serde",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-worker-macros"
|
name = "gloo-worker-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1845,7 +1786,7 @@ dependencies = [
|
||||||
"chrono-humanize",
|
"chrono-humanize",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo 0.11.0",
|
"gloo 0.10.0",
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ edition = "2024"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
thiserror = { version = "2" }
|
thiserror = { version = "2" }
|
||||||
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
axum = { version = "*", optional = true }
|
axum = { version = "*", optional = true }
|
||||||
argon2 = { version = "*", optional = true }
|
argon2 = { version = "*", optional = true }
|
||||||
sqlx = { version = "*", optional = true }
|
sqlx = { version = "*", optional = true }
|
||||||
ciborium = { version = "*", optional = true }
|
ciborium = { version = "*", optional = true }
|
||||||
bytes = { version = "1.10.1", features = ["serde"], optional = true }
|
bytes = { version = "1.10.1", features = ["serde"], optional = true }
|
||||||
axum-extra = { version = "*", optional = true }
|
axum-extra = { version = "*", optional = true }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
|
||||||
log = { version = "0.4", optional = true }
|
log = { version = "0.4", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ pub mod cbor;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod limited;
|
pub mod limited;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod path;
|
||||||
pub mod plan;
|
pub mod plan;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
use core::{fmt::Display, ops::RangeBounds};
|
use core::fmt::Display;
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Timelike, Utc};
|
use chrono::{NaiveTime, Timelike};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ impl<const LEN: usize> Serialize for FixedLenString<LEN> {
|
||||||
pub struct ClampedString<const MIN: usize, const MAX: usize>(String);
|
pub struct ClampedString<const MIN: usize, const MAX: usize>(String);
|
||||||
|
|
||||||
impl<const MIN: usize, const MAX: usize> ClampedString<MIN, MAX> {
|
impl<const MIN: usize, const MAX: usize> ClampedString<MIN, MAX> {
|
||||||
|
pub const MIN_LEN: usize = MIN;
|
||||||
|
pub const MAX_LEN: usize = MAX;
|
||||||
|
|
||||||
pub fn new(s: String) -> Result<Self, RangeInclusive<usize>> {
|
pub fn new(s: String) -> Result<Self, RangeInclusive<usize>> {
|
||||||
let str_len = s.chars().take(MAX.saturating_add(1)).count();
|
let str_len = s.chars().take(MAX.saturating_add(1)).count();
|
||||||
(str_len >= MIN && str_len <= MAX)
|
(str_len >= MIN && str_len <= MAX)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
macro_rules! paths {
|
||||||
|
($($name:ident: $path:literal,)*) => {
|
||||||
|
pub struct Paths {
|
||||||
|
$(
|
||||||
|
pub $name: &'static str,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PATHS: Paths = Paths {
|
||||||
|
$(
|
||||||
|
$name: $path,
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
paths! {
|
||||||
|
new_user: "/s/users",
|
||||||
|
signin: "/s/tokens",
|
||||||
|
plan_session: "/s/plans/{id}",
|
||||||
|
plans: "/s/plans",
|
||||||
|
check_token: "/s/tokens/check",
|
||||||
|
user_password: "/s/user/password",
|
||||||
|
}
|
||||||
|
|
@ -10,5 +10,10 @@ pub struct UserLogin {
|
||||||
pub username: Username,
|
pub username: Username,
|
||||||
pub password: Password,
|
pub password: Password,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct ChangePassword {
|
||||||
|
pub current: Password,
|
||||||
|
pub new: Password,
|
||||||
|
}
|
||||||
|
|
||||||
crate::id_impl!(UserId);
|
crate::id_impl!(UserId);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use chrono::{TimeDelta, Utc};
|
||||||
use plan_proto::{
|
use plan_proto::{
|
||||||
error::{DatabaseError, ServerError},
|
error::{DatabaseError, ServerError},
|
||||||
token,
|
token,
|
||||||
user::UserId,
|
user::{Password, UserId},
|
||||||
};
|
};
|
||||||
use rand::distr::SampleString;
|
use rand::distr::SampleString;
|
||||||
use sqlx::{Decode, Encode, Pool, Postgres, prelude::FromRow, query, query_as};
|
use sqlx::{Decode, Encode, Pool, Postgres, prelude::FromRow, query, query_as};
|
||||||
|
|
@ -101,6 +101,38 @@ impl UserDatabase {
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn change_password(&self, user: User, password: Password) -> Result<User> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let password_hash = argon2
|
||||||
|
.hash_password(password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let updated_at = chrono::Utc::now();
|
||||||
|
query!(
|
||||||
|
r#"
|
||||||
|
update
|
||||||
|
users
|
||||||
|
set
|
||||||
|
password_hash = $1,
|
||||||
|
updated_at = $2
|
||||||
|
where
|
||||||
|
id = $3
|
||||||
|
"#,
|
||||||
|
password_hash,
|
||||||
|
updated_at,
|
||||||
|
user.id.into_uuid(),
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(User {
|
||||||
|
password_hash,
|
||||||
|
updated_at,
|
||||||
|
..user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_user(&self, get_user_by: GetUserBy<'_>) -> Result<User> {
|
pub async fn get_user(&self, get_user_by: GetUserBy<'_>) -> Result<User> {
|
||||||
Ok(match get_user_by {
|
Ok(match get_user_by {
|
||||||
GetUserBy::Username(username) => {
|
GetUserBy::Username(username) => {
|
||||||
|
|
@ -138,11 +170,11 @@ impl UserDatabase {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn verify_login(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> core::result::Result<LoginToken, ServerError> {
|
) -> core::result::Result<User, ServerError> {
|
||||||
let user = self.get_user(GetUserBy::Username(username)).await?;
|
let user = self.get_user(GetUserBy::Username(username)).await?;
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&user.password_hash).map_err(DatabaseError::from)?;
|
let parsed_hash = PasswordHash::new(&user.password_hash).map_err(DatabaseError::from)?;
|
||||||
|
|
@ -153,6 +185,16 @@ impl UserDatabase {
|
||||||
err => ServerError::DatabaseError(err.into()),
|
err => ServerError::DatabaseError(err.into()),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> core::result::Result<LoginToken, ServerError> {
|
||||||
|
let user = self.verify_login(username, password).await?;
|
||||||
|
|
||||||
let token = LoginToken::new(user.id);
|
let token = LoginToken::new(user.id);
|
||||||
|
|
||||||
query!(
|
query!(
|
||||||
|
|
@ -173,7 +215,7 @@ impl UserDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_token(&self, token: &str) -> core::result::Result<User, ServerError> {
|
pub async fn check_token(&self, token: &str) -> core::result::Result<User, ServerError> {
|
||||||
let token = query_as!(
|
let token: LoginToken = query_as!(
|
||||||
LoginToken,
|
LoginToken,
|
||||||
r#" select
|
r#" select
|
||||||
token, user_id, created_at, expires_at
|
token, user_id, created_at, expires_at
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_extra::{
|
use axum_extra::{
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
handler::HandlerCallWithExtractors,
|
|
||||||
headers::{self, Authorization},
|
headers::{self, Authorization},
|
||||||
};
|
};
|
||||||
use core::{fmt::Display, net::SocketAddr, str::FromStr, time::Duration};
|
use core::{fmt::Display, net::SocketAddr, str::FromStr, time::Duration};
|
||||||
|
|
@ -22,18 +21,15 @@ use plan_proto::{
|
||||||
error::ServerError,
|
error::ServerError,
|
||||||
limited::FixedLenString,
|
limited::FixedLenString,
|
||||||
message::ClientMessage,
|
message::ClientMessage,
|
||||||
|
path::PATHS,
|
||||||
plan::{CreatePlan, UserPlans},
|
plan::{CreatePlan, UserPlans},
|
||||||
token::{Token, TokenLogin},
|
token::{Token, TokenLogin},
|
||||||
user::UserLogin,
|
user::{ChangePassword, UserLogin},
|
||||||
};
|
};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use tower::{
|
use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer};
|
||||||
ServiceBuilder,
|
|
||||||
buffer::BufferLayer,
|
|
||||||
limit::{RateLimit, RateLimitLayer, rate::Rate},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{db::Database, identity::SessionIdentity, runner::Runner, session::SessionManager};
|
use crate::{db::Database, identity::SessionIdentity, runner::Runner, session::SessionManager};
|
||||||
|
|
||||||
|
|
@ -174,12 +170,12 @@ async fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/s/users", put(signup))
|
.route(PATHS.new_user, put(signup))
|
||||||
.route("/s/tokens", post(signin))
|
.route(PATHS.signin, post(signin))
|
||||||
// .route("/s/tokens/check", get(check_token))
|
.route(PATHS.plan_session, any(session::calendar_session))
|
||||||
.route("/s/plans/{id}", any(session::calendar_session))
|
.route(PATHS.plans, get(my_plans))
|
||||||
.route("/s/plans", get(my_plans))
|
.route(PATHS.plans, post(new_plan))
|
||||||
.route("/s/plans", post(new_plan))
|
.route(PATHS.user_password, post(change_password))
|
||||||
.route(
|
.route(
|
||||||
"/s/tokens/check",
|
"/s/tokens/check",
|
||||||
get(check_token).layer(
|
get(check_token).layer(
|
||||||
|
|
@ -210,6 +206,18 @@ async fn main() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn change_password(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
TypedHeader(Authorization(login)): TypedHeader<Authorization<TokenLogin>>,
|
||||||
|
Cbor(ChangePassword { current, new }): Cbor<ChangePassword>,
|
||||||
|
) -> Result<impl IntoResponse, ServerError> {
|
||||||
|
let user = db.user().check_token(&login.0).await?;
|
||||||
|
let user = db.user().verify_login(&user.username, ¤t).await?;
|
||||||
|
db.user().change_password(user, new).await?;
|
||||||
|
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
async fn new_plan(
|
async fn new_plan(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
TypedHeader(Authorization(login)): TypedHeader<Authorization<TokenLogin>>,
|
TypedHeader(Authorization(login)): TypedHeader<Authorization<TokenLogin>>,
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ web-sys = { version = "0.3.77", features = [
|
||||||
"AbortController",
|
"AbortController",
|
||||||
"HtmlSpanElement",
|
"HtmlSpanElement",
|
||||||
"ReadableStreamDefaultReader",
|
"ReadableStreamDefaultReader",
|
||||||
|
"HtmlDialogElement",
|
||||||
|
"DomRect",
|
||||||
] }
|
] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
yew = { version = "0.21", features = ["csr"] }
|
yew = { version = "0.21", features = ["csr"] }
|
||||||
yew-router = "0.18.0"
|
yew-router = "0.18.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
gloo = "0.11"
|
gloo = "*"
|
||||||
wasm-logger = "0.2"
|
wasm-logger = "0.2"
|
||||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,27 @@ button {
|
||||||
.details {}
|
.details {}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-container {}
|
.error-container {
|
||||||
|
$error_border: rgba(255, 0, 0, 0.7);
|
||||||
|
padding: 3px 5px 3px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(255, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&>button {
|
||||||
|
border: 1px solid $error_border;
|
||||||
|
color: $error_border;
|
||||||
|
background-color: color.change($error_border, $alpha: 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color.change($error_border, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.calendar {
|
.calendar {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -201,9 +221,13 @@ nav.user-nav {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sign-out {
|
.right-side {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
@ -215,6 +239,10 @@ nav.user-nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.validation-fail {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.new-plan {
|
.new-plan {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -303,11 +331,15 @@ nav.user-nav {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 200vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|
@ -321,7 +353,11 @@ nav.user-nav {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.dialog-box {
|
.dialog-box {
|
||||||
|
|
||||||
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -333,17 +369,34 @@ nav.user-nav {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
gap: 5px;
|
||||||
|
color: white;
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
|
margin-top: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&>button {
|
&>button {
|
||||||
min-width: 4cm;
|
min-width: 4cm;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$close_color: rgba(255, 0, 0, 1);
|
||||||
|
|
||||||
|
.close {
|
||||||
|
border: 1px solid $close_color;
|
||||||
|
color: $close_color;
|
||||||
|
background-color: change-color($color: $close_color, $alpha: 0.1);
|
||||||
|
;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: change-color($color: $close_color, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -649,3 +702,28 @@ select {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#change-password {
|
||||||
|
input {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-settings {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.user-options {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct DialogProps {
|
pub struct DialogProps {
|
||||||
pub message: String,
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
pub options: Box<[String]>,
|
pub options: Box<[String]>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub cancel_callback: Option<Callback<()>>,
|
pub cancel_callback: Option<Callback<()>>,
|
||||||
|
|
@ -14,7 +13,7 @@ pub struct DialogProps {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Dialog(
|
pub fn Dialog(
|
||||||
DialogProps {
|
DialogProps {
|
||||||
message,
|
children,
|
||||||
options,
|
options,
|
||||||
cancel_callback,
|
cancel_callback,
|
||||||
callback,
|
callback,
|
||||||
|
|
@ -43,7 +42,7 @@ pub fn Dialog(
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<div class="dialog-box">
|
<div class="dialog-box">
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<p>{message.clone()}</p>
|
{children.clone()}
|
||||||
</div>
|
</div>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{options}
|
{options}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::HtmlDialogElement;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub enum DialogMode {
|
||||||
|
#[default]
|
||||||
|
Close,
|
||||||
|
ConfirmOrClose(Callback<()>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct DialogProps {
|
||||||
|
pub id: String,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub button_content: Html,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub mode: DialogMode,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub close_backdrop: bool,
|
||||||
|
}
|
||||||
|
#[function_component]
|
||||||
|
pub fn DialogModal(
|
||||||
|
DialogProps {
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
button_content,
|
||||||
|
mode,
|
||||||
|
close_backdrop,
|
||||||
|
}: &DialogProps,
|
||||||
|
) -> Html {
|
||||||
|
let close_cb = {
|
||||||
|
let id = id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
{
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let close = {
|
||||||
|
html! {
|
||||||
|
<button onclick={close_cb.clone()} class="close">{"close"}</button>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mode_buttons = match mode {
|
||||||
|
DialogMode::Close => close.clone(),
|
||||||
|
DialogMode::ConfirmOrClose(on_confirm) => {
|
||||||
|
let on_confirm = {
|
||||||
|
let cb = on_confirm.clone();
|
||||||
|
Callback::from(move |_| cb.emit(()))
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<button onclick={on_confirm}>{"confirm"}</button>
|
||||||
|
{close.clone()}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_backdrop_click = close_backdrop.then_some({
|
||||||
|
let id = id.clone();
|
||||||
|
let close_cb = close_cb.clone();
|
||||||
|
Callback::from(move |ev: MouseEvent| {
|
||||||
|
let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(Some(dialog)) = dialog.query_selector(".dialog-box") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let rect = dialog.get_bounding_client_rect();
|
||||||
|
|
||||||
|
let is_in_dialog = rect.top() as i32 <= ev.client_y()
|
||||||
|
&& ev.client_y() <= rect.top() as i32 + rect.height() as i32
|
||||||
|
&& rect.left() as i32 <= ev.client_x()
|
||||||
|
&& ev.client_x() <= rect.left() as i32 + rect.width() as i32;
|
||||||
|
|
||||||
|
if !is_in_dialog {
|
||||||
|
close_cb.emit(ev);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let modal = html! {
|
||||||
|
<dialog id={id.clone()} onclick={on_backdrop_click}>
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-box">
|
||||||
|
{children.clone()}
|
||||||
|
<div class="options">
|
||||||
|
{mode_buttons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_click = {
|
||||||
|
let id = id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
&& let Err(err) = dialog.show_modal()
|
||||||
|
{
|
||||||
|
gloo::console::log!("[ERR] show dialog modal: ", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let button = (*button_content != html! {}).then_some(html! {
|
||||||
|
<button onclick={on_click}>{button_content.clone()}</button>
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="dialog-modal">
|
||||||
|
{button}
|
||||||
|
{modal}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,12 @@ use yew::prelude::*;
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct ErrorDisplayProps {
|
pub struct ErrorDisplayProps {
|
||||||
pub state: UseStateHandle<Option<String>>,
|
pub state: UseStateHandle<Option<String>>,
|
||||||
|
#[prop_or(true)]
|
||||||
|
pub closable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn ErrorDisplay(ErrorDisplayProps { state }: &ErrorDisplayProps) -> Html {
|
pub fn ErrorDisplay(ErrorDisplayProps { state, closable }: &ErrorDisplayProps) -> Html {
|
||||||
state
|
state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|err| {
|
.map(|err| {
|
||||||
|
|
@ -14,10 +16,13 @@ pub fn ErrorDisplay(ErrorDisplayProps { state }: &ErrorDisplayProps) -> Html {
|
||||||
let on_click = Callback::from(move |_| {
|
let on_click = Callback::from(move |_| {
|
||||||
setter.set(None);
|
setter.set(None);
|
||||||
});
|
});
|
||||||
|
let close = closable.then_some(html! {
|
||||||
|
<button onclick={on_click}>{"Ok"}</button>
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<span>{err.clone()}</span>
|
<span>{err.clone()}</span>
|
||||||
<button onclick={on_click}>{"Ok"}</button>
|
{close}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use core::ops::{Range, RangeInclusive};
|
use core::ops::RangeInclusive;
|
||||||
|
|
||||||
use plan_proto::HalfHour;
|
use plan_proto::HalfHour;
|
||||||
use web_sys::HtmlSelectElement;
|
use web_sys::HtmlSelectElement;
|
||||||
|
|
|
||||||
|
|
@ -52,16 +52,20 @@ pub fn Nav() -> Html {
|
||||||
let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]);
|
let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]);
|
||||||
html! {
|
html! {
|
||||||
<Dialog
|
<Dialog
|
||||||
message={String::from("really sign out?")}
|
|
||||||
options={options}
|
options={options}
|
||||||
cancel_callback={Some(cancel_signout)}
|
cancel_callback={Some(cancel_signout)}
|
||||||
callback={callback}
|
callback={callback}
|
||||||
/>
|
>
|
||||||
|
<p>{"really sign out?"}</p>
|
||||||
|
</Dialog>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<button onclick={confirm_signout} class="sign-out">{"sign out"}</button>
|
<div class="right-side">
|
||||||
|
<a href="/user/settings"><button>{"⚙️"}</button></a>
|
||||||
|
<button onclick={confirm_signout} >{"sign out"}</button>
|
||||||
|
</div>
|
||||||
<a href="/plans/new"><button>{"new plan"}</button></a>
|
<a href="/plans/new"><button>{"new plan"}</button></a>
|
||||||
{dialog}
|
{dialog}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ mod pages {
|
||||||
pub mod plan;
|
pub mod plan;
|
||||||
pub mod signin;
|
pub mod signin;
|
||||||
pub mod signup;
|
pub mod signup;
|
||||||
|
pub mod user_settings_page;
|
||||||
}
|
}
|
||||||
mod components {
|
mod components {
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
pub mod dialog_modal;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod half_hour_range_select;
|
pub mod half_hour_range_select;
|
||||||
pub mod month;
|
pub mod month;
|
||||||
|
|
@ -21,10 +23,7 @@ mod login;
|
||||||
mod request;
|
mod request;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod weekday;
|
mod weekday;
|
||||||
use core::{
|
use core::ops::{Bound, RangeBounds, RangeInclusive};
|
||||||
fmt::Display,
|
|
||||||
ops::{Bound, RangeBounds, RangeInclusive, Sub},
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gloo::net::http::Request;
|
use gloo::net::http::Request;
|
||||||
|
|
@ -43,6 +42,7 @@ use crate::{
|
||||||
plan::PlanPage,
|
plan::PlanPage,
|
||||||
signin::Signin,
|
signin::Signin,
|
||||||
signup::Signup,
|
signup::Signup,
|
||||||
|
user_settings_page::UserSettingsPage,
|
||||||
},
|
},
|
||||||
storage::{LastPlanVisitedLoggedOut, StorageKey},
|
storage::{LastPlanVisitedLoggedOut, StorageKey},
|
||||||
};
|
};
|
||||||
|
|
@ -70,6 +70,8 @@ enum Route {
|
||||||
#[not_found]
|
#[not_found]
|
||||||
#[at("/404")]
|
#[at("/404")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
#[at("/user/settings")]
|
||||||
|
UserSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route(route: Route) -> Html {
|
fn route(route: Route) -> Html {
|
||||||
|
|
@ -167,6 +169,9 @@ fn route(route: Route) -> Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = match route {
|
let body = match route {
|
||||||
|
Route::UserSettings => html! {
|
||||||
|
<UserSettingsPage />
|
||||||
|
},
|
||||||
Route::Plan { id } => html! {
|
Route::Plan { id } => html! {
|
||||||
<PlanPage id={id}/>
|
<PlanPage id={id}/>
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
use gloo::net::http::Request;
|
||||||
|
use plan_proto::{
|
||||||
|
error::ServerError,
|
||||||
|
path::PATHS,
|
||||||
|
token::Token,
|
||||||
|
user::{ChangePassword, Password},
|
||||||
|
};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::HtmlDialogElement;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
SERVER_URL,
|
||||||
|
components::{
|
||||||
|
dialog_modal::{DialogModal, DialogMode},
|
||||||
|
error::ErrorDisplay,
|
||||||
|
state_input::{InputType, StateInput},
|
||||||
|
},
|
||||||
|
pages::mainpage::SignedOutMainPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn UserSettingsPage() -> Html {
|
||||||
|
const SUCCESS_MODAL_ID: &str = "success-modal";
|
||||||
|
let token = match use_context::<Token>() {
|
||||||
|
Some(token) => token,
|
||||||
|
None => {
|
||||||
|
return html! {
|
||||||
|
<SignedOutMainPage />
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let error_state = use_state(|| None);
|
||||||
|
let error_obj = html! {
|
||||||
|
<ErrorDisplay state={error_state.clone()}/>
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_password_state = use_state(String::new);
|
||||||
|
let new_password_state = use_state(String::new);
|
||||||
|
let new_password_repeat_state = use_state(String::new);
|
||||||
|
let change_password = {
|
||||||
|
let change_password_modal_id = "change-password".to_string();
|
||||||
|
let pw_err = use_state(|| None);
|
||||||
|
let pw_error_obj = html! {
|
||||||
|
<ErrorDisplay state={pw_err.clone()} closable=false/>
|
||||||
|
};
|
||||||
|
let confirm_mode = {
|
||||||
|
let current_password_state = current_password_state.clone();
|
||||||
|
let new_password_state = new_password_state.clone();
|
||||||
|
let new_password_repeat_state = new_password_repeat_state.clone();
|
||||||
|
let error_state = pw_err.clone();
|
||||||
|
let token = token.clone();
|
||||||
|
let change_password_modal_id = change_password_modal_id.clone();
|
||||||
|
|
||||||
|
DialogMode::ConfirmOrClose(Callback::from(move |_| {
|
||||||
|
pw_err.set(None);
|
||||||
|
if current_password_state.is_empty() {
|
||||||
|
error_state.set(Some("current password required".to_string()));
|
||||||
|
return;
|
||||||
|
// error_state.set(Some("".to_string()));
|
||||||
|
}
|
||||||
|
if new_password_state.is_empty() {
|
||||||
|
error_state.set(Some("new password required".to_string()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if *new_password_state != *new_password_repeat_state {
|
||||||
|
error_state.set(Some("new passwords do not match".to_string()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if new_password_state.len() < Password::MIN_LEN {
|
||||||
|
error_state.set(Some(format!(
|
||||||
|
"password too short; must be at least {} characters",
|
||||||
|
Password::MIN_LEN
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if new_password_state.len() > Password::MAX_LEN {
|
||||||
|
error_state.set(Some(format!(
|
||||||
|
"password too long; must be at most {} characters",
|
||||||
|
Password::MAX_LEN
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(current) = Password::new((*current_password_state).clone()) else {
|
||||||
|
error_state.set(Some(format!(
|
||||||
|
"current password must be between {} and {} characters",
|
||||||
|
Password::MIN_LEN,
|
||||||
|
Password::MAX_LEN,
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(new) = Password::new((*new_password_state).clone()) else {
|
||||||
|
error_state.set(Some(format!(
|
||||||
|
"new password must be between {} and {} characters",
|
||||||
|
Password::MIN_LEN,
|
||||||
|
Password::MAX_LEN,
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let on_success = {
|
||||||
|
let change_password_modal_id = change_password_modal_id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&change_password_modal_id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
{
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(SUCCESS_MODAL_ID)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
&& let Err(err) = dialog.show_modal()
|
||||||
|
{
|
||||||
|
gloo::console::log!("[ERR] show dialog modal: ", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
change_password_request(
|
||||||
|
token.clone(),
|
||||||
|
current,
|
||||||
|
new,
|
||||||
|
error_state.setter(),
|
||||||
|
on_success,
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<DialogModal
|
||||||
|
id={change_password_modal_id.clone()}
|
||||||
|
button_content={html!{{"change password"}}}
|
||||||
|
mode={confirm_mode}
|
||||||
|
close_backdrop=true
|
||||||
|
>
|
||||||
|
{pw_error_obj}
|
||||||
|
<label for="current-password">{"current password"}</label>
|
||||||
|
<StateInput
|
||||||
|
id={Some(String::from("current-password"))}
|
||||||
|
state={current_password_state.clone()}
|
||||||
|
input_type={InputType::Password}
|
||||||
|
/>
|
||||||
|
<label for="new-password">{"new password"}</label>
|
||||||
|
<StateInput
|
||||||
|
id={Some(String::from("new-password"))}
|
||||||
|
state={new_password_state}
|
||||||
|
input_type={InputType::Password}
|
||||||
|
/>
|
||||||
|
<label for="new-password-repeat">{"new password (repeat)"}</label>
|
||||||
|
<StateInput
|
||||||
|
id={Some(String::from("new-password-repeat"))}
|
||||||
|
state={new_password_repeat_state}
|
||||||
|
input_type={InputType::Password}
|
||||||
|
/>
|
||||||
|
</DialogModal>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<div class="user-settings">
|
||||||
|
<DialogModal
|
||||||
|
id={SUCCESS_MODAL_ID.to_string()}
|
||||||
|
close_backdrop=true
|
||||||
|
>
|
||||||
|
<h1 class="success">
|
||||||
|
{"task completed"}
|
||||||
|
</h1>
|
||||||
|
</DialogModal>
|
||||||
|
<h1>{token.username.to_string()}</h1>
|
||||||
|
<div class="user-options">
|
||||||
|
{change_password}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error_obj}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_password_request(
|
||||||
|
token: Token,
|
||||||
|
current: Password,
|
||||||
|
new: Password,
|
||||||
|
on_error: UseStateSetter<Option<String>>,
|
||||||
|
on_success: Callback<()>,
|
||||||
|
) {
|
||||||
|
let token = token.clone();
|
||||||
|
let current = current.clone();
|
||||||
|
let new = new.clone();
|
||||||
|
let on_error = on_error.clone();
|
||||||
|
let mut request = vec![];
|
||||||
|
if let Err(err) = ciborium::into_writer(&ChangePassword { current, new }, &mut request) {
|
||||||
|
on_error.set(Some(format!("serializing: {err}")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let on_success = on_success.clone();
|
||||||
|
|
||||||
|
yew::platform::spawn_local(async move {
|
||||||
|
let req = match Request::post(format!("{SERVER_URL}{}", PATHS.user_password).as_str())
|
||||||
|
.header("content-type", crate::CBOR_CONTENT_TYPE)
|
||||||
|
.header("authorization", format!("Bearer {}", token.token).as_str())
|
||||||
|
.body(request)
|
||||||
|
{
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(err) => {
|
||||||
|
on_error.set(Some(format!("creating login request: {err}")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let resp = match req.send().await {
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(err) => {
|
||||||
|
on_error.set(Some(format!("sending password change request: {err}")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if resp.ok() {
|
||||||
|
on_success.emit(());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match resp
|
||||||
|
.binary()
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.and_then(|body| {
|
||||||
|
ciborium::from_reader::<ServerError, _>(body.as_slice())
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}) {
|
||||||
|
Ok(err) => {
|
||||||
|
on_error.set(Some(format!(
|
||||||
|
"[{} {}]: {err}",
|
||||||
|
resp.status(),
|
||||||
|
resp.status_text()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
on_error.set(Some(format!(
|
||||||
|
"[{} {}] could not parse body: {err}",
|
||||||
|
resp.status(),
|
||||||
|
resp.status_text()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue