Display request errors
This commit is contained in:
parent
353cdd602a
commit
0f88287929
41
Cargo.lock
generated
41
Cargo.lock
generated
@ -663,7 +663,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
"validator 0.14.0",
|
||||
]
|
||||
|
||||
@ -805,7 +805,7 @@ dependencies = [
|
||||
"model",
|
||||
"pretty_env_logger",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -834,6 +834,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.43",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -1124,7 +1125,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1263,7 +1264,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tinytemplate",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1319,7 +1320,7 @@ dependencies = [
|
||||
"dummy",
|
||||
"http",
|
||||
"rand",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1407,7 +1408,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1804,7 +1805,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"termcolor",
|
||||
"toml",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2275,7 +2276,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
"validator 0.15.0",
|
||||
]
|
||||
|
||||
@ -2504,7 +2505,7 @@ dependencies = [
|
||||
"model",
|
||||
"pretty_env_logger",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2622,7 +2623,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3151,7 +3152,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sonic-channel",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3197,7 +3198,7 @@ dependencies = [
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
"version_check 0.9.4",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
@ -3223,7 +3224,7 @@ dependencies = [
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
"version_check 0.9.4",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
@ -3538,7 +3539,7 @@ dependencies = [
|
||||
"time 0.2.27",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
"webpki 0.21.4",
|
||||
"webpki-roots 0.21.1",
|
||||
"whoami",
|
||||
@ -3854,7 +3855,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4183,6 +4184,15 @@ dependencies = [
|
||||
"sha1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.14.0"
|
||||
@ -4360,6 +4370,7 @@ dependencies = [
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"uuid 1.0.0",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
@ -98,6 +98,20 @@ macro_rules! query_db {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($db: expr, $msg: expr, passthrough $db_fail: expr, $act_fail: expr) => {
|
||||
match $db.send($msg).await {
|
||||
Ok(Ok(r)) => r,
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{e}");
|
||||
return Err($db_fail(e.into()));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
return Err($act_fail);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -7,10 +7,11 @@ use std::sync::Arc;
|
||||
use actix::Addr;
|
||||
use actix_session::Session;
|
||||
use actix_web::body::BoxBody;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
|
||||
use model::api::Failure;
|
||||
use model::{AccessTokenString, RecordId, Token};
|
||||
use serde::Serialize;
|
||||
use token_manager::{query_tm, TokenManager};
|
||||
|
||||
pub use self::admin::Error as AdminError;
|
||||
@ -37,6 +38,7 @@ impl RequireLogin for Session {
|
||||
pub enum Error {
|
||||
#[from(ignore)]
|
||||
Unauthorized,
|
||||
CriticalFailure,
|
||||
Admin(routes::admin::Error),
|
||||
Public(routes::public::Error),
|
||||
}
|
||||
@ -53,64 +55,68 @@ impl From<V1ShoppingCartError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Failure {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let msg = match self {
|
||||
Error::Unauthorized => String::from("Unauthorized"),
|
||||
Error::Admin(e) => format!("{e}"),
|
||||
Error::Public(e) => format!("{e}"),
|
||||
Error::CriticalFailure => String::from("Something went wrong"),
|
||||
};
|
||||
f.write_str(&serde_json::to_string(&Failure { errors: vec![msg] }).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ReqFailure {
|
||||
success: bool,
|
||||
msg: String,
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Error::Public(PublicError::DatabaseConnection) | Error::CriticalFailure => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
Error::Admin(_) => StatusCode::BAD_REQUEST,
|
||||
Error::Public(_) => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for Error {}
|
||||
|
||||
impl Responder for Error {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match self {
|
||||
Error::Public(PublicError::DatabaseConnection) | Error::CriticalFailure => {
|
||||
HttpResponse::InternalServerError()
|
||||
.content_type("application/json")
|
||||
.json(Failure {
|
||||
errors: vec![format!("{}", self)],
|
||||
})
|
||||
}
|
||||
Error::Unauthorized => HttpResponse::Unauthorized()
|
||||
.content_type("application/json")
|
||||
.json(ReqFailure {
|
||||
success: false,
|
||||
msg: format!("{}", self),
|
||||
}),
|
||||
Error::Public(PublicError::DatabaseConnection)
|
||||
| Error::Public(PublicError::Database(..))
|
||||
| Error::Admin(..) => HttpResponse::InternalServerError()
|
||||
.content_type("application/json")
|
||||
.json(ReqFailure {
|
||||
success: false,
|
||||
msg: format!("{}", self),
|
||||
.json(Failure {
|
||||
errors: vec![format!("{}", self)],
|
||||
}),
|
||||
Error::Public(PublicError::Database(..)) | Error::Admin(..) => {
|
||||
HttpResponse::BadRequest()
|
||||
.content_type("application/json")
|
||||
.json(Failure {
|
||||
errors: vec![format!("{}", self)],
|
||||
})
|
||||
}
|
||||
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
|
||||
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
|
||||
.content_type("application/json")
|
||||
.json(ReqFailure {
|
||||
success: false,
|
||||
msg: format!("{}", self),
|
||||
.json(Failure {
|
||||
errors: vec![format!("{}", self)],
|
||||
}),
|
||||
},
|
||||
Error::Public(PublicError::ApiV1(
|
||||
V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder,
|
||||
)) => HttpResponse::BadRequest()
|
||||
.content_type("application/json")
|
||||
.json(ReqFailure {
|
||||
success: false,
|
||||
msg: format!("{}", self),
|
||||
.json(Failure {
|
||||
errors: vec![format!("{}", self)],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ async fn refresh_token(
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
db: Data<Addr<Database>>,
|
||||
credentials: BearerAuth,
|
||||
) -> routes::Result<Json<api::SignInOutput>> {
|
||||
) -> routes::Result<Json<api::SessionOutput>> {
|
||||
let account_id: model::AccountId = credentials
|
||||
.require_user(tm.clone().into_inner())
|
||||
.await?
|
||||
@ -50,7 +50,7 @@ async fn refresh_token(
|
||||
refresh_token_string,
|
||||
} = create_auth_pair(tm, account).await?;
|
||||
|
||||
Ok(Json(api::SignInOutput {
|
||||
Ok(Json(api::SessionOutput {
|
||||
access_token: access_token_string,
|
||||
refresh_token: refresh_token_string,
|
||||
exp: access_token.expiration_time,
|
||||
|
@ -131,7 +131,8 @@ pub async fn create_account(
|
||||
db: Data<Addr<Database>>,
|
||||
Json(payload): Json<api::CreateAccountInput>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> routes::Result<HttpResponse> {
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
) -> routes::Result<Json<model::api::SessionOutput>> {
|
||||
if payload.password != payload.password_confirmation {
|
||||
return Err(routes::Error::Admin(
|
||||
routes::admin::Error::DifferentPasswords,
|
||||
@ -147,15 +148,30 @@ pub async fn create_account(
|
||||
}
|
||||
};
|
||||
|
||||
public_send_db!(
|
||||
let account: model::FullAccount = query_db!(
|
||||
db,
|
||||
database_manager::CreateAccount {
|
||||
email: payload.email,
|
||||
login: payload.login,
|
||||
pass_hash: model::PassHash::from(hash),
|
||||
role: model::Role::User,
|
||||
}
|
||||
},
|
||||
passthrough routes::Error::Public,
|
||||
routes::Error::CriticalFailure
|
||||
);
|
||||
|
||||
let AuthPair {
|
||||
access_token,
|
||||
access_token_string,
|
||||
_refresh_token: _,
|
||||
refresh_token_string,
|
||||
} = create_auth_pair(tm, account).await?;
|
||||
|
||||
Ok(Json(api::SessionOutput {
|
||||
access_token: access_token_string,
|
||||
refresh_token: refresh_token_string,
|
||||
exp: access_token.expiration_time,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) struct AuthPair {
|
||||
@ -205,7 +221,7 @@ async fn sign_in(
|
||||
Json(payload): Json<api::SignInInput>,
|
||||
db: Data<Addr<Database>>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
) -> Result<Json<api::SignInOutput>> {
|
||||
) -> Result<Json<api::SessionOutput>> {
|
||||
let db = db.into_inner();
|
||||
|
||||
let account: model::FullAccount = query_db!(
|
||||
@ -227,7 +243,7 @@ async fn sign_in(
|
||||
refresh_token_string,
|
||||
} = create_auth_pair(tm, account).await?;
|
||||
|
||||
Ok(Json(api::SignInOutput {
|
||||
Ok(Json(api::SessionOutput {
|
||||
access_token: access_token_string,
|
||||
refresh_token: refresh_token_string,
|
||||
exp: access_token.expiration_time,
|
||||
@ -253,5 +269,6 @@ pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||
.service(product)
|
||||
.service(products)
|
||||
.service(stocks)
|
||||
.service(sign_in);
|
||||
.service(sign_in)
|
||||
.service(create_account);
|
||||
}
|
||||
|
@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Failure {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(transparent)]
|
||||
@ -307,7 +312,7 @@ pub struct SignInInput {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SignInOutput {
|
||||
pub struct SessionOutput {
|
||||
pub access_token: AccessTokenString,
|
||||
pub refresh_token: RefreshTokenString,
|
||||
pub exp: NaiveDateTime,
|
||||
|
@ -9,7 +9,7 @@ pub mod encrypt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{Deref, Display, From};
|
||||
use derive_more::{Deref, DerefMut, Display, From};
|
||||
#[cfg(feature = "dummy")]
|
||||
use fake::Fake;
|
||||
#[cfg(feature = "dummy")]
|
||||
@ -305,10 +305,20 @@ impl Login {
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Debug, Deref, From, Display)]
|
||||
#[derive(Serialize, Debug, Clone, Deref, DerefMut, From, Display)]
|
||||
#[serde(transparent)]
|
||||
pub struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn invalid_empty() -> Self {
|
||||
Self("".into())
|
||||
}
|
||||
|
||||
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||
Self(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Email {
|
||||
type Err = TransformError;
|
||||
|
||||
@ -319,10 +329,6 @@ impl FromStr for Email {
|
||||
Err(TransformError::NotEmail)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_empty() -> Self {
|
||||
Self("".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Email {
|
||||
@ -582,7 +588,7 @@ impl ResetToken {
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Deref, From, Display)]
|
||||
#[serde(transparent)]
|
||||
pub struct Password(String);
|
||||
|
||||
@ -595,10 +601,16 @@ impl Password {
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Deref, From, Display)]
|
||||
#[serde(transparent)]
|
||||
pub struct PasswordConfirmation(String);
|
||||
|
||||
impl PasswordConfirmation {
|
||||
pub fn new<S: Into<String>>(pass: S) -> Self {
|
||||
Self(pass.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
||||
|
@ -12,9 +12,11 @@ model = { path = "../shared/model", features = ["dummy"] }
|
||||
seed = { version = "0.9.1", features = [] }
|
||||
seed_heroicons = { git = "https://github.com/mh84/seed_heroicons.git" }
|
||||
|
||||
chrono = { version = "*" }
|
||||
chrono = { version = "*", features = ["wasm-bindgen"] }
|
||||
gloo-timers = { version = "*", features = ["futures"] }
|
||||
|
||||
uuid = { version = "1.0.0", features = ["v4"] }
|
||||
|
||||
serde = { version = "1.0.137", features = ["derive"] }
|
||||
serde_json = { version = "1.0.81" }
|
||||
serde-wasm-bindgen = { version = "0.4.2" }
|
||||
|
@ -1 +1,45 @@
|
||||
use std::convert::Infallible;
|
||||
use std::ops::FromResidual;
|
||||
|
||||
use seed::fetch::{FetchError, Request};
|
||||
|
||||
pub mod public;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NetRes<S> {
|
||||
Success(S),
|
||||
Error(model::api::Failure),
|
||||
Http(FetchError),
|
||||
}
|
||||
|
||||
impl<S> FromResidual<Result<Infallible, NetRes<S>>> for NetRes<S> {
|
||||
fn from_residual(residual: Result<Infallible, NetRes<S>>) -> Self {
|
||||
match residual {
|
||||
Ok(_s) => unreachable!(),
|
||||
Err(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform<S: serde::de::DeserializeOwned + 'static>(req: Request<'_>) -> NetRes<S> {
|
||||
let res = match req.fetch().await {
|
||||
Ok(res) => res,
|
||||
Err(err) => return NetRes::Http(err),
|
||||
};
|
||||
if res.status().is_error() {
|
||||
NetRes::Error(match res.json().await {
|
||||
Ok(json) => json,
|
||||
Err(_err) => match res.text().await {
|
||||
Ok(text) => model::api::Failure { errors: vec![text] },
|
||||
Err(err) => model::api::Failure {
|
||||
errors: vec![format!("{:?}", err)],
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
match res.json().await {
|
||||
Ok(json) => NetRes::Success(json),
|
||||
Err(err) => NetRes::Http(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +1,63 @@
|
||||
use model::{AccessTokenString, RefreshTokenString};
|
||||
use seed::prelude::*;
|
||||
use seed::fetch::{Header, Method, Request};
|
||||
|
||||
pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
||||
Request::new("/api/v1/products")
|
||||
.method(Method::Get)
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
use crate::api::perform;
|
||||
|
||||
pub async fn fetch_products() -> super::NetRes<model::api::Products> {
|
||||
perform(Request::new("/api/v1/products").method(Method::Get)).await
|
||||
}
|
||||
|
||||
pub async fn fetch_product(product_id: model::ProductId) -> fetch::Result<model::api::Product> {
|
||||
Request::new(format!("/api/v1/product/{}", product_id))
|
||||
.method(Method::Get)
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
pub async fn fetch_product(product_id: model::ProductId) -> super::NetRes<model::api::Product> {
|
||||
perform(Request::new(format!("/api/v1/product/{}", product_id)).method(Method::Get)).await
|
||||
}
|
||||
|
||||
pub async fn fetch_me(access_token: AccessTokenString) -> fetch::Result<model::Account> {
|
||||
Request::new("/api/v1/me")
|
||||
.header(fetch::Header::bearer(access_token.as_str()))
|
||||
.method(Method::Get)
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes<model::Account> {
|
||||
perform(
|
||||
Request::new("/api/v1/me")
|
||||
.header(Header::bearer(access_token.as_str()))
|
||||
.method(Method::Get),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sign_in(input: model::api::SignInInput) -> fetch::Result<model::api::SignInOutput> {
|
||||
Request::new("/api/v1/sign-in")
|
||||
.method(Method::Post)
|
||||
.json(&input)?
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes<model::api::SessionOutput> {
|
||||
perform(
|
||||
Request::new("/api/v1/sign-in")
|
||||
.method(Method::Post)
|
||||
.json(&input)
|
||||
.map_err(crate::api::NetRes::Http)?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn verify_token(access_token: AccessTokenString) -> fetch::Result<String> {
|
||||
Request::new("/api/v1/token/verify")
|
||||
.method(Method::Post)
|
||||
.header(fetch::Header::bearer(access_token.as_str()))
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<String> {
|
||||
perform(
|
||||
Request::new("/api/v1/token/verify")
|
||||
.method(Method::Post)
|
||||
.header(Header::bearer(access_token.as_str())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn refresh_token(
|
||||
access_token: RefreshTokenString,
|
||||
) -> fetch::Result<model::api::SignInOutput> {
|
||||
Request::new("/api/v1/token/refresh")
|
||||
.method(Method::Post)
|
||||
.header(fetch::Header::bearer(access_token.as_str()))
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
) -> super::NetRes<model::api::SessionOutput> {
|
||||
perform(
|
||||
Request::new("/api/v1/token/refresh")
|
||||
.method(Method::Post)
|
||||
.header(Header::bearer(access_token.as_str())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sign_up(
|
||||
input: model::api::CreateAccountInput,
|
||||
) -> super::NetRes<model::api::SessionOutput> {
|
||||
perform(
|
||||
Request::new("/api/v1/register")
|
||||
.method(Method::Post)
|
||||
.json(&input)
|
||||
.map_err(crate::api::NetRes::Http)?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
#![feature(try_trait_v2)]
|
||||
|
||||
pub mod api;
|
||||
mod i18n;
|
||||
mod model;
|
||||
mod pages;
|
||||
pub mod session;
|
||||
pub mod shared;
|
||||
|
||||
use seed::empty;
|
||||
@ -10,6 +13,7 @@ use seed::prelude::*;
|
||||
use crate::i18n::I18n;
|
||||
use crate::model::Model;
|
||||
use crate::pages::{Msg, Page, PublicPage};
|
||||
use crate::session::SessionMsg;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fetch_page {
|
||||
@ -62,11 +66,9 @@ macro_rules! fetch_page {
|
||||
}
|
||||
|
||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
orders
|
||||
.stream(streams::interval(500, || Msg::CheckAccessToken))
|
||||
.subscribe(Msg::UrlChanged);
|
||||
orders.subscribe(Msg::UrlChanged);
|
||||
|
||||
Model {
|
||||
let mut model = Model {
|
||||
url: url.clone().set_path(&[] as &[&str]),
|
||||
token: LocalStorage::get("auth-token").ok(),
|
||||
page: Page::init(url, orders),
|
||||
@ -77,7 +79,11 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
.and_then(|el: web_sys::Element| el.get_attribute("href")),
|
||||
shared: shared::Model::default(),
|
||||
i18n: I18n::load(),
|
||||
}
|
||||
};
|
||||
|
||||
session::init(&mut model, orders);
|
||||
|
||||
model
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
@ -85,20 +91,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::Shared(msg) => {
|
||||
shared::update(msg, &mut model.shared, orders);
|
||||
}
|
||||
Msg::CheckAccessToken => {
|
||||
orders.skip();
|
||||
if model.shared.refresh_token.is_none() {
|
||||
return;
|
||||
}
|
||||
if let Some(exp) = model.shared.exp {
|
||||
if exp > chrono::Utc::now().naive_utc() - chrono::Duration::seconds(1) {
|
||||
return;
|
||||
}
|
||||
if let Some(token) = model.shared.refresh_token.take() {
|
||||
orders.send_msg(Msg::Shared(shared::Msg::RefreshToken(token)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url, orders),
|
||||
Msg::Public(pages::public::Msg::Listing(msg)) => {
|
||||
let page = fetch_page!(public model, Listing);
|
||||
@ -112,11 +104,21 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let page = fetch_page!(public model, SignIn);
|
||||
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Public(pages::public::Msg::SignUp(pages::public::sign_up::Msg::AccountCreated(
|
||||
res,
|
||||
))) => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Session(SessionMsg::TokenRefreshed(res)));
|
||||
}
|
||||
Msg::Public(pages::public::Msg::SignUp(msg)) => {
|
||||
let page = fetch_page!(public model, SignUp);
|
||||
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Admin(_) => {}
|
||||
Msg::Session(msg) => {
|
||||
session::update(msg, model, orders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,8 @@ pub enum Msg {
|
||||
Public(public::Msg),
|
||||
Admin(admin::Msg),
|
||||
UrlChanged(subs::UrlChanged),
|
||||
CheckAccessToken,
|
||||
Shared(shared::Msg),
|
||||
Session(crate::session::SessionMsg),
|
||||
}
|
||||
|
||||
pub enum AdminPage {
|
||||
|
@ -69,11 +69,11 @@ pub mod layout {
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
pub fn view<Msg>(
|
||||
pub fn view(
|
||||
model: &crate::Model,
|
||||
content: Node<Msg>,
|
||||
content: Node<crate::Msg>,
|
||||
categories: Option<&[model::api::Category]>,
|
||||
) -> Node<Msg> {
|
||||
) -> Node<crate::Msg> {
|
||||
let sidebar = match categories {
|
||||
Some(categories) => {
|
||||
let sidebar = super::sidebar::view(model, categories);
|
||||
@ -84,9 +84,11 @@ pub mod layout {
|
||||
}
|
||||
_ => empty![],
|
||||
};
|
||||
let notifications = crate::shared::notification::view(model);
|
||||
div![
|
||||
C!["flex"],
|
||||
sidebar,
|
||||
notifications,
|
||||
div![C!["w-full h-full p-4 m-8 overflow-y-auto"], content]
|
||||
]
|
||||
}
|
||||
@ -108,7 +110,7 @@ pub mod sidebar {
|
||||
}
|
||||
|
||||
fn item<Msg>(model: &crate::Model, category: &model::api::Category) -> Node<Msg> {
|
||||
let url = Urls::new(model.url.clone())
|
||||
let url = Urls::new(&model.url)
|
||||
.listing()
|
||||
.add_path_part(category.key.as_str());
|
||||
li![
|
||||
|
@ -4,6 +4,7 @@ use seed::app::Orders;
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::api::NetRes;
|
||||
use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -19,21 +20,19 @@ pub struct ListingPage {
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
FetchProducts,
|
||||
ProductFetched(fetch::Result<model::api::Products>),
|
||||
ProductFetched(crate::api::NetRes<model::api::Products>),
|
||||
}
|
||||
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
||||
orders.send_msg(Msg::FetchProducts);
|
||||
let model = ListingPage {
|
||||
ListingPage {
|
||||
product_ids: vec![],
|
||||
filters: url_to_filters(url),
|
||||
products: Default::default(),
|
||||
errors: vec![],
|
||||
categories: vec![],
|
||||
visible_products: vec![],
|
||||
};
|
||||
seed::log!(&model);
|
||||
model
|
||||
}
|
||||
}
|
||||
|
||||
fn url_to_filters(mut url: Url) -> HashSet<String> {
|
||||
@ -77,7 +76,7 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>)
|
||||
async { Msg::ProductFetched(crate::api::public::fetch_products().await) }
|
||||
});
|
||||
}
|
||||
Msg::ProductFetched(Ok(products)) => {
|
||||
Msg::ProductFetched(NetRes::Success(products)) => {
|
||||
model.categories = products
|
||||
.0
|
||||
.iter()
|
||||
@ -103,7 +102,7 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>)
|
||||
};
|
||||
filter_products(model);
|
||||
}
|
||||
Msg::ProductFetched(Err(_e)) => {
|
||||
Msg::ProductFetched(NetRes::Error(_)) | Msg::ProductFetched(NetRes::Http(_)) => {
|
||||
model.errors.push("Failed to load products".into());
|
||||
}
|
||||
}
|
||||
@ -148,7 +147,7 @@ fn product(model: &crate::Model, product: &model::api::Product) -> Node<Msg> {
|
||||
.map(|photo| photo.url.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
let url = Urls::new(model.url.clone())
|
||||
let url = Urls::new(&model.url)
|
||||
.product()
|
||||
.add_path_part((*product.id as i32).to_string());
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::api::NetRes;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
ProductFetched(fetch::Result<model::api::Product>),
|
||||
ProductFetched(crate::api::NetRes<model::api::Product>),
|
||||
SelectImage(usize),
|
||||
}
|
||||
|
||||
@ -33,10 +35,13 @@ pub fn page_changed(_url: Url, _model: &mut ProductPage) {}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut ProductPage, _orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ProductFetched(Ok(product)) => {
|
||||
Msg::ProductFetched(NetRes::Success(product)) => {
|
||||
model.product = Some(product);
|
||||
}
|
||||
Msg::ProductFetched(Err(e)) => {
|
||||
Msg::ProductFetched(NetRes::Error(e)) => {
|
||||
seed::error!(e);
|
||||
}
|
||||
Msg::ProductFetched(NetRes::Http(e)) => {
|
||||
seed::error!(e);
|
||||
}
|
||||
Msg::SelectImage(selected) => {
|
||||
|
@ -28,18 +28,32 @@ pub fn page_changed(_url: Url, _model: &mut SignInPage) {}
|
||||
pub fn update(_msg: Msg, _model: &mut SignInPage, _orders: &mut impl Orders<Msg>) {}
|
||||
|
||||
pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
||||
let home = Urls::new(&model.url).home();
|
||||
let logo = model
|
||||
.logo
|
||||
.as_deref()
|
||||
.map(|src| {
|
||||
a![
|
||||
attrs![At::Href => home],
|
||||
img![attrs![At::Src => src], C!["m-auto"]]
|
||||
]
|
||||
})
|
||||
.unwrap_or_else(|| a![attrs![At::Href => home], "Home"]);
|
||||
let content = div![
|
||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||
div![
|
||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
||||
h1![C!["text-3xl font-semibold text-center text-indigo-700"], "Logo"],
|
||||
C!["w-full p-6 m-auto bg-white lg:max-w-md"],
|
||||
h1![
|
||||
C!["text-3xl font-semibold text-center text-indigo-700"],
|
||||
logo
|
||||
],
|
||||
sign_in_form(model, page),
|
||||
p![
|
||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||
model.i18n.t("Don't have an account?"),
|
||||
a![
|
||||
C!["font-medium text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).sign_up()],
|
||||
attrs![At::Href => Urls::new(&model.url).sign_up()],
|
||||
" ",
|
||||
model.i18n.t("Sign up")
|
||||
]
|
||||
@ -48,10 +62,8 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
||||
]
|
||||
.map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, None)
|
||||
]
|
||||
// crate::shared::view::public_navbar(model),
|
||||
div![super::layout::view(model, content, None)]
|
||||
}
|
||||
|
||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
||||
@ -87,7 +99,7 @@ fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
||||
],
|
||||
a![
|
||||
C!["text-xs text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).forgot_password()],
|
||||
attrs![At::Href => Urls::new(&model.url).forgot_password()],
|
||||
model.i18n.t("Forget Password?"),
|
||||
],
|
||||
div![
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use model::{Email, Login, Password, PasswordConfirmation};
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
@ -10,7 +11,9 @@ pub enum Msg {
|
||||
LoginChanged(String),
|
||||
EmailChanged(String),
|
||||
PasswordChanged(String),
|
||||
PasswordConfirmationChanged(String),
|
||||
Submit,
|
||||
AccountCreated(crate::api::NetRes<model::api::SessionOutput>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -18,6 +21,7 @@ pub struct SignUpPage {
|
||||
pub login: model::Login,
|
||||
pub email: model::Email,
|
||||
pub password: model::Password,
|
||||
pub password_confirmation: model::PasswordConfirmation,
|
||||
}
|
||||
|
||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
||||
@ -25,37 +29,86 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
||||
login: model::Login::new(""),
|
||||
email: model::Email::invalid_empty(),
|
||||
password: model::Password::new(""),
|
||||
password_confirmation: model::PasswordConfirmation::new(""),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
|
||||
|
||||
pub fn update(_msg: Msg, _model: &mut SignUpPage, _orders: &mut impl Orders<Msg>) {}
|
||||
pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::LoginChanged(value) => {
|
||||
model.login = Login::new(value);
|
||||
}
|
||||
Msg::EmailChanged(value) => {
|
||||
if let Ok(email) = Email::from_str(&value) {
|
||||
model.email = email;
|
||||
}
|
||||
}
|
||||
Msg::PasswordChanged(value) => {
|
||||
model.password = Password::new(value);
|
||||
}
|
||||
Msg::PasswordConfirmationChanged(value) => {
|
||||
model.password_confirmation = PasswordConfirmation::new(value);
|
||||
}
|
||||
Msg::Submit => {
|
||||
let email = model.email.clone();
|
||||
let login = model.login.clone();
|
||||
let password = model.password.clone();
|
||||
let password_confirmation = model.password_confirmation.clone();
|
||||
|
||||
orders.perform_cmd(async move {
|
||||
Msg::AccountCreated(
|
||||
crate::api::public::sign_up(model::api::CreateAccountInput {
|
||||
email,
|
||||
login,
|
||||
password,
|
||||
password_confirmation,
|
||||
})
|
||||
.await,
|
||||
)
|
||||
});
|
||||
}
|
||||
Msg::AccountCreated(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
|
||||
let home = Urls::new(&model.url).home();
|
||||
let logo = model
|
||||
.logo
|
||||
.as_deref()
|
||||
.map(|src| {
|
||||
a![
|
||||
attrs![At::Href => home],
|
||||
img![attrs![At::Src => src], C!["m-auto"]]
|
||||
]
|
||||
})
|
||||
.unwrap_or_else(|| a![attrs![At::Href => home], "Logo"]);
|
||||
|
||||
let content = div![
|
||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||
div![
|
||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
||||
h1![C!["text-3xl font-semibold text-center text-indigo-700"], "Logo"],
|
||||
C!["w-full p-6 m-auto bg-white rounded-md lg:max-w-md"],
|
||||
h1![
|
||||
C!["text-3xl font-semibold text-center text-indigo-700"],
|
||||
logo
|
||||
],
|
||||
sign_up_form(model, page),
|
||||
p![
|
||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||
model.i18n.t("Have an account?"),
|
||||
a![
|
||||
C!["font-medium text-indigo-600 hover:underline"],
|
||||
attrs![At::Href => Urls::new(model.url.clone().set_path(&[] as &[&str])).sign_in()],
|
||||
attrs![At::Href => Urls::new(&model.url).sign_in()],
|
||||
" ",
|
||||
model.i18n.t("Sign in")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
.map_msg(Into::into);
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, None)
|
||||
]
|
||||
.map_msg(Into::into);
|
||||
div![super::layout::view(model, content, None)]
|
||||
}
|
||||
|
||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
@ -73,7 +126,6 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::EmailChanged)
|
||||
})
|
||||
]
|
||||
@ -85,7 +137,6 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged)
|
||||
})
|
||||
]
|
||||
@ -97,10 +148,24 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged)
|
||||
})
|
||||
],
|
||||
div![
|
||||
label![
|
||||
attrs!["for" => "password-confirmation"],
|
||||
C!["block text-sm text-indigo-800"],
|
||||
model.i18n.t("Password confirmation")
|
||||
],
|
||||
input![
|
||||
attrs!["type" => "password", "id" => "password-confirmation"],
|
||||
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]
|
||||
],
|
||||
ev(Ev::Change, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordConfirmationChanged)
|
||||
})
|
||||
],
|
||||
div![
|
||||
C!["mt-6"],
|
||||
button![
|
||||
|
143
web/src/session.rs
Normal file
143
web/src/session.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use chrono::{NaiveDateTime, TimeZone};
|
||||
use model::{AccessTokenString, RefreshTokenString};
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::shared::notification::NotificationMsg;
|
||||
use crate::{Model, Msg};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionMsg {
|
||||
SessionReceived {
|
||||
access_token: AccessTokenString,
|
||||
refresh_token: RefreshTokenString,
|
||||
exp: NaiveDateTime,
|
||||
},
|
||||
SessionExpired,
|
||||
RefreshToken(RefreshTokenString),
|
||||
TokenRefreshed(crate::api::NetRes<model::api::SessionOutput>),
|
||||
CheckSession,
|
||||
}
|
||||
|
||||
pub fn init(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders.stream(streams::interval(500, || {
|
||||
Msg::Session(SessionMsg::CheckSession)
|
||||
}));
|
||||
model.shared.access_token = LocalStorage::get::<_, String>("at")
|
||||
.ok()
|
||||
.map(model::AccessTokenString::new);
|
||||
model.shared.refresh_token = LocalStorage::get::<_, String>("rt")
|
||||
.ok()
|
||||
.map(model::RefreshTokenString::new);
|
||||
model.shared.exp = LocalStorage::get::<_, String>("exp").ok().and_then(|s| {
|
||||
seed::log!("Parsing ", s);
|
||||
chrono::DateTime::parse_from_rfc3339(&s)
|
||||
.ok()
|
||||
.map(|t| t.naive_utc())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(msg: SessionMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
SessionMsg::SessionReceived {
|
||||
access_token,
|
||||
refresh_token,
|
||||
exp,
|
||||
} => {
|
||||
LocalStorage::insert("at", access_token.as_str()).ok();
|
||||
LocalStorage::insert("rt", refresh_token.as_str()).ok();
|
||||
let encoded = {
|
||||
let l: chrono::DateTime<chrono::Local> =
|
||||
chrono::Local.from_local_datetime(&exp).unwrap();
|
||||
l.to_rfc3339()
|
||||
};
|
||||
LocalStorage::insert("exp", &encoded).ok();
|
||||
|
||||
model.shared.access_token = Some(access_token);
|
||||
model.shared.refresh_token = Some(refresh_token);
|
||||
model.shared.exp = Some(exp);
|
||||
}
|
||||
SessionMsg::SessionExpired => {
|
||||
LocalStorage::remove("at").ok();
|
||||
LocalStorage::remove("rt").ok();
|
||||
LocalStorage::remove("exp").ok();
|
||||
|
||||
orders.force_render_now();
|
||||
}
|
||||
SessionMsg::CheckSession => {
|
||||
orders.skip();
|
||||
if model.shared.refresh_token.is_none() {
|
||||
return;
|
||||
}
|
||||
if let Some(exp) = model.shared.exp {
|
||||
if exp > chrono::Utc::now().naive_utc() - chrono::Duration::seconds(1) {
|
||||
return;
|
||||
}
|
||||
if let Some(token) = model.shared.refresh_token.take() {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Session(SessionMsg::RefreshToken(token)));
|
||||
}
|
||||
}
|
||||
}
|
||||
SessionMsg::RefreshToken(token) => {
|
||||
orders.skip().perform_cmd(async {
|
||||
Msg::Session(SessionMsg::TokenRefreshed(
|
||||
crate::api::public::refresh_token(token).await,
|
||||
))
|
||||
});
|
||||
}
|
||||
SessionMsg::TokenRefreshed(crate::api::NetRes::Success(model::api::SessionOutput {
|
||||
access_token,
|
||||
refresh_token,
|
||||
exp,
|
||||
})) => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Session(SessionMsg::SessionReceived {
|
||||
access_token,
|
||||
refresh_token,
|
||||
exp,
|
||||
}));
|
||||
}
|
||||
SessionMsg::TokenRefreshed(crate::api::NetRes::Error(model::api::Failure { errors })) => {
|
||||
errors.into_iter().for_each(|msg| {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Shared(crate::shared::Msg::Notification(
|
||||
NotificationMsg::Error(msg),
|
||||
)));
|
||||
});
|
||||
}
|
||||
SessionMsg::TokenRefreshed(crate::api::NetRes::Http(e)) => match e {
|
||||
FetchError::JsonError(json) => {
|
||||
seed::error!("invalid data in response", json);
|
||||
}
|
||||
FetchError::DomException(dom) => {
|
||||
seed::error!("dom", dom);
|
||||
}
|
||||
FetchError::PromiseError(res) => {
|
||||
seed::error!("promise", res);
|
||||
}
|
||||
FetchError::NetworkError(net) => {
|
||||
seed::error!("net", net);
|
||||
}
|
||||
FetchError::RequestError(e) => {
|
||||
seed::log!(e);
|
||||
}
|
||||
FetchError::StatusError(status) => match status.code {
|
||||
401 | 403 => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Session(SessionMsg::SessionExpired));
|
||||
}
|
||||
_ => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::Shared(crate::shared::Msg::Notification(
|
||||
NotificationMsg::Error("Request failed".into()),
|
||||
)));
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ use seed::app::Orders;
|
||||
pub use crate::shared::msg::Msg;
|
||||
|
||||
pub mod msg;
|
||||
pub mod notification;
|
||||
pub mod view;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -11,6 +12,7 @@ pub struct Model {
|
||||
pub refresh_token: Option<model::RefreshTokenString>,
|
||||
pub exp: Option<chrono::NaiveDateTime>,
|
||||
pub me: Option<model::Account>,
|
||||
pub notifications: Vec<notification::Notification>,
|
||||
}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<crate::Msg>) {
|
||||
@ -22,37 +24,33 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<crate::Msg>)
|
||||
});
|
||||
}
|
||||
}
|
||||
Msg::MeLoaded(Ok(account)) => {
|
||||
Msg::MeLoaded(crate::api::NetRes::Success(account)) => {
|
||||
model.me = Some(account);
|
||||
}
|
||||
Msg::MeLoaded(Err(_err)) => {}
|
||||
Msg::MeLoaded(crate::api::NetRes::Error(_error)) => {}
|
||||
Msg::MeLoaded(crate::api::NetRes::Http(_error)) => {}
|
||||
Msg::SignIn(input) => {
|
||||
orders
|
||||
.skip()
|
||||
.perform_cmd(async { Msg::SignedIn(crate::api::public::sign_in(input).await) });
|
||||
}
|
||||
Msg::SignedIn(Ok(pair)) => {
|
||||
Msg::SignedIn(crate::api::NetRes::Success(pair)) => {
|
||||
handle_auth_pair(pair, model, orders);
|
||||
}
|
||||
Msg::SignedIn(Err(_err)) => {}
|
||||
Msg::RefreshToken(token) => {
|
||||
orders.skip().perform_cmd(async {
|
||||
Msg::TokenRefreshed(crate::api::public::refresh_token(token).await)
|
||||
});
|
||||
Msg::SignedIn(crate::api::NetRes::Error(_err)) => {}
|
||||
Msg::SignedIn(crate::api::NetRes::Http(_err)) => {}
|
||||
Msg::Notification(msg) => {
|
||||
notification::update(msg, model, orders);
|
||||
}
|
||||
Msg::TokenRefreshed(Ok(pair)) => {
|
||||
handle_auth_pair(pair, model, orders);
|
||||
}
|
||||
Msg::TokenRefreshed(Err(_err)) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_auth_pair(
|
||||
pair: model::api::SignInOutput,
|
||||
pair: model::api::SessionOutput,
|
||||
model: &mut Model,
|
||||
_orders: &mut impl Orders<crate::Msg>,
|
||||
) {
|
||||
let model::api::SignInOutput {
|
||||
let model::api::SessionOutput {
|
||||
access_token,
|
||||
refresh_token,
|
||||
exp,
|
||||
|
@ -3,9 +3,8 @@ use seed::fetch::Result;
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
LoadMe,
|
||||
MeLoaded(Result<model::Account>),
|
||||
MeLoaded(crate::api::NetRes<model::Account>),
|
||||
SignIn(model::api::SignInInput),
|
||||
SignedIn(Result<model::api::SignInOutput>),
|
||||
RefreshToken(model::RefreshTokenString),
|
||||
TokenRefreshed(Result<model::api::SignInOutput>),
|
||||
SignedIn(crate::api::NetRes<model::api::SessionOutput>),
|
||||
Notification(crate::shared::notification::NotificationMsg),
|
||||
}
|
||||
|
184
web/src/shared/notification.rs
Normal file
184
web/src/shared/notification.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::{shared, Msg};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NotificationMsg {
|
||||
Success(String),
|
||||
Info(String),
|
||||
Warning(String),
|
||||
Error(String),
|
||||
Close(uuid::Uuid),
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Notification {
|
||||
id: uuid::Uuid,
|
||||
message: String,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Type {
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn into_color(self) -> &'static str {
|
||||
match self {
|
||||
Type::Success => "text-green-600",
|
||||
Type::Info => "text-blue-600",
|
||||
Type::Warning => "text-yellow-600",
|
||||
Type::Error => "text-red-600",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoNodes<Msg> for Type {
|
||||
fn into_nodes(self) -> Vec<Node<Msg>> {
|
||||
match self {
|
||||
Type::Success => vec![success_icon()],
|
||||
Type::Info => vec![],
|
||||
Type::Warning => vec![],
|
||||
Type::Error => vec![error_icon()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new<S: Into<String>>(s: S, ty: Type) -> Self {
|
||||
Self {
|
||||
message: s.into(),
|
||||
id: uuid::Uuid::new_v4(),
|
||||
ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(msg: NotificationMsg, model: &mut shared::Model, _orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
NotificationMsg::Success(message) => {
|
||||
model
|
||||
.notifications
|
||||
.push(Notification::new(message, Type::Success));
|
||||
}
|
||||
NotificationMsg::Info(message) => {
|
||||
model
|
||||
.notifications
|
||||
.push(Notification::new(message, Type::Info));
|
||||
}
|
||||
NotificationMsg::Warning(message) => {
|
||||
model
|
||||
.notifications
|
||||
.push(Notification::new(message, Type::Warning));
|
||||
}
|
||||
NotificationMsg::Error(message) => {
|
||||
model
|
||||
.notifications
|
||||
.push(Notification::new(message, Type::Error));
|
||||
}
|
||||
NotificationMsg::Close(id) => {
|
||||
if let Some(pos) = model.notifications.iter().position(|n| n.id == id) {
|
||||
model.notifications.remove(pos);
|
||||
}
|
||||
}
|
||||
NotificationMsg::Clear => {
|
||||
model.notifications.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &crate::Model) -> Node<Msg> {
|
||||
if model.shared.notifications.is_empty() {
|
||||
return empty![];
|
||||
}
|
||||
let notifications = model.shared.notifications.iter().map(|notification| {
|
||||
message(
|
||||
notification.id,
|
||||
notification.message.as_str(),
|
||||
notification.ty,
|
||||
)
|
||||
});
|
||||
div![
|
||||
C!["absolute inline-block top-0 right-0 bottom-auto left-auto p-2.5 text-xs z-10"],
|
||||
notifications
|
||||
]
|
||||
}
|
||||
|
||||
pub fn message(id: uuid::Uuid, message: &str, icon: Type) -> Node<Msg> {
|
||||
div![
|
||||
C!["flex items-center justify-between max-w-xs p-4 bg-white border rounded-md shadow-sm"],
|
||||
div![
|
||||
C!["flex items-center"],
|
||||
icon.into_nodes(),
|
||||
p![C!["ml-3 text-sm font-bold"], C![icon.into_color()], message],
|
||||
a![
|
||||
C!["ml-4"],
|
||||
close_icon(),
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
Msg::Shared(shared::Msg::Notification(NotificationMsg::Close(id)))
|
||||
})
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
fn success_icon() -> Node<Msg> {
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns"=>"http://www.w3.org/2000/svg",
|
||||
"class" => "w-8 h-8 text-green-500",
|
||||
"viewBox" => "0 0 20 20",
|
||||
"fill" => "currentColor"
|
||||
],
|
||||
path![attrs![
|
||||
"fill-rule" => "evenodd",
|
||||
"d" => "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",
|
||||
"clip-rule" => "evenodd"
|
||||
]]
|
||||
]
|
||||
}
|
||||
|
||||
fn error_icon() -> Node<Msg> {
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns" => "http://www.w3.org/2000/svg",
|
||||
"class" => "w-8 h-8 text-red-600",
|
||||
"viewBox" => "0 0 20 20",
|
||||
"fill" => "currentColor"
|
||||
],
|
||||
path![attrs![
|
||||
"fill-rule" => "evenodd",
|
||||
"d" => "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z",
|
||||
"clip-rule" => "evenodd"
|
||||
]]
|
||||
]
|
||||
}
|
||||
|
||||
fn close_icon() -> Node<Msg> {
|
||||
span![
|
||||
C!["inline-flex items-center cursor-pointer"],
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns" => "http://www.w3.org/2000/svg",
|
||||
"class" => "w-4 h-4 text-gray-600",
|
||||
"fill" => "none",
|
||||
"viewBox" => "0 0 24 24",
|
||||
"stroke" => "currentColor"
|
||||
],
|
||||
path![attrs![
|
||||
"stroke-linecap" => "round",
|
||||
"stroke-linejoin" => "round",
|
||||
"stroke-width" => "2",
|
||||
"d" => "M6 18L18 6M6 6l12 12"
|
||||
]]
|
||||
]
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user