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",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
"validator 0.14.0",
|
"validator 0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -805,7 +805,7 @@ dependencies = [
|
|||||||
"model",
|
"model",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -834,6 +834,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"time 0.1.43",
|
"time 0.1.43",
|
||||||
|
"wasm-bindgen",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1124,7 +1125,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1263,7 +1264,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tinytemplate",
|
"tinytemplate",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1319,7 +1320,7 @@ dependencies = [
|
|||||||
"dummy",
|
"dummy",
|
||||||
"http",
|
"http",
|
||||||
"rand",
|
"rand",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1407,7 +1408,7 @@ dependencies = [
|
|||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1804,7 +1805,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2275,7 +2276,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
"validator 0.15.0",
|
"validator 0.15.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2504,7 +2505,7 @@ dependencies = [
|
|||||||
"model",
|
"model",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2622,7 +2623,7 @@ dependencies = [
|
|||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3151,7 +3152,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sonic-channel",
|
"sonic-channel",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3197,7 +3198,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
"version_check 0.9.4",
|
"version_check 0.9.4",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@ -3223,7 +3224,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
"version_check 0.9.4",
|
"version_check 0.9.4",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@ -3538,7 +3539,7 @@ dependencies = [
|
|||||||
"time 0.2.27",
|
"time 0.2.27",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
"webpki 0.21.4",
|
"webpki 0.21.4",
|
||||||
"webpki-roots 0.21.1",
|
"webpki-roots 0.21.1",
|
||||||
"whoami",
|
"whoami",
|
||||||
@ -3854,7 +3855,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4183,6 +4184,15 @@ dependencies = [
|
|||||||
"sha1",
|
"sha1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "validator"
|
name = "validator"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@ -4360,6 +4370,7 @@ dependencies = [
|
|||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"uuid 1.0.0",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -7,10 +7,11 @@ use std::sync::Arc;
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::web::ServiceConfig;
|
use actix_web::web::ServiceConfig;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
|
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
|
||||||
|
use model::api::Failure;
|
||||||
use model::{AccessTokenString, RecordId, Token};
|
use model::{AccessTokenString, RecordId, Token};
|
||||||
use serde::Serialize;
|
|
||||||
use token_manager::{query_tm, TokenManager};
|
use token_manager::{query_tm, TokenManager};
|
||||||
|
|
||||||
pub use self::admin::Error as AdminError;
|
pub use self::admin::Error as AdminError;
|
||||||
@ -37,6 +38,7 @@ impl RequireLogin for Session {
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[from(ignore)]
|
#[from(ignore)]
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
CriticalFailure,
|
||||||
Admin(routes::admin::Error),
|
Admin(routes::admin::Error),
|
||||||
Public(routes::public::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 {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let msg = match self {
|
let msg = match self {
|
||||||
Error::Unauthorized => String::from("Unauthorized"),
|
Error::Unauthorized => String::from("Unauthorized"),
|
||||||
Error::Admin(e) => format!("{e}"),
|
Error::Admin(e) => format!("{e}"),
|
||||||
Error::Public(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())
|
f.write_str(&serde_json::to_string(&Failure { errors: vec![msg] }).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
impl ResponseError for Error {
|
||||||
struct ReqFailure {
|
fn status_code(&self) -> StatusCode {
|
||||||
success: bool,
|
match self {
|
||||||
msg: String,
|
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 {
|
impl Responder for Error {
|
||||||
type Body = BoxBody;
|
type Body = BoxBody;
|
||||||
|
|
||||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||||
match self {
|
match self {
|
||||||
|
Error::Public(PublicError::DatabaseConnection) | Error::CriticalFailure => {
|
||||||
|
HttpResponse::InternalServerError()
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(Failure {
|
||||||
|
errors: vec![format!("{}", self)],
|
||||||
|
})
|
||||||
|
}
|
||||||
Error::Unauthorized => HttpResponse::Unauthorized()
|
Error::Unauthorized => HttpResponse::Unauthorized()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.json(ReqFailure {
|
.json(Failure {
|
||||||
success: false,
|
errors: vec![format!("{}", self)],
|
||||||
msg: format!("{}", self),
|
|
||||||
}),
|
}),
|
||||||
Error::Public(PublicError::DatabaseConnection)
|
Error::Public(PublicError::Database(..)) | Error::Admin(..) => {
|
||||||
| Error::Public(PublicError::Database(..))
|
HttpResponse::BadRequest()
|
||||||
| Error::Admin(..) => HttpResponse::InternalServerError()
|
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.json(ReqFailure {
|
.json(Failure {
|
||||||
success: false,
|
errors: vec![format!("{}", self)],
|
||||||
msg: format!("{}", self),
|
})
|
||||||
}),
|
}
|
||||||
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
|
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
|
||||||
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
|
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.json(ReqFailure {
|
.json(Failure {
|
||||||
success: false,
|
errors: vec![format!("{}", self)],
|
||||||
msg: format!("{}", self),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Error::Public(PublicError::ApiV1(
|
Error::Public(PublicError::ApiV1(
|
||||||
V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder,
|
V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder,
|
||||||
)) => HttpResponse::BadRequest()
|
)) => HttpResponse::BadRequest()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.json(ReqFailure {
|
.json(Failure {
|
||||||
success: false,
|
errors: vec![format!("{}", self)],
|
||||||
msg: format!("{}", self),
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ async fn refresh_token(
|
|||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
) -> routes::Result<Json<api::SignInOutput>> {
|
) -> routes::Result<Json<api::SessionOutput>> {
|
||||||
let account_id: model::AccountId = credentials
|
let account_id: model::AccountId = credentials
|
||||||
.require_user(tm.clone().into_inner())
|
.require_user(tm.clone().into_inner())
|
||||||
.await?
|
.await?
|
||||||
@ -50,7 +50,7 @@ async fn refresh_token(
|
|||||||
refresh_token_string,
|
refresh_token_string,
|
||||||
} = create_auth_pair(tm, account).await?;
|
} = create_auth_pair(tm, account).await?;
|
||||||
|
|
||||||
Ok(Json(api::SignInOutput {
|
Ok(Json(api::SessionOutput {
|
||||||
access_token: access_token_string,
|
access_token: access_token_string,
|
||||||
refresh_token: refresh_token_string,
|
refresh_token: refresh_token_string,
|
||||||
exp: access_token.expiration_time,
|
exp: access_token.expiration_time,
|
||||||
|
@ -131,7 +131,8 @@ pub async fn create_account(
|
|||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
Json(payload): Json<api::CreateAccountInput>,
|
Json(payload): Json<api::CreateAccountInput>,
|
||||||
config: Data<SharedAppConfig>,
|
config: Data<SharedAppConfig>,
|
||||||
) -> routes::Result<HttpResponse> {
|
tm: Data<Addr<TokenManager>>,
|
||||||
|
) -> routes::Result<Json<model::api::SessionOutput>> {
|
||||||
if payload.password != payload.password_confirmation {
|
if payload.password != payload.password_confirmation {
|
||||||
return Err(routes::Error::Admin(
|
return Err(routes::Error::Admin(
|
||||||
routes::admin::Error::DifferentPasswords,
|
routes::admin::Error::DifferentPasswords,
|
||||||
@ -147,15 +148,30 @@ pub async fn create_account(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public_send_db!(
|
let account: model::FullAccount = query_db!(
|
||||||
db,
|
db,
|
||||||
database_manager::CreateAccount {
|
database_manager::CreateAccount {
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
login: payload.login,
|
login: payload.login,
|
||||||
pass_hash: model::PassHash::from(hash),
|
pass_hash: model::PassHash::from(hash),
|
||||||
role: model::Role::User,
|
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 {
|
pub(crate) struct AuthPair {
|
||||||
@ -205,7 +221,7 @@ async fn sign_in(
|
|||||||
Json(payload): Json<api::SignInInput>,
|
Json(payload): Json<api::SignInInput>,
|
||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
) -> Result<Json<api::SignInOutput>> {
|
) -> Result<Json<api::SessionOutput>> {
|
||||||
let db = db.into_inner();
|
let db = db.into_inner();
|
||||||
|
|
||||||
let account: model::FullAccount = query_db!(
|
let account: model::FullAccount = query_db!(
|
||||||
@ -227,7 +243,7 @@ async fn sign_in(
|
|||||||
refresh_token_string,
|
refresh_token_string,
|
||||||
} = create_auth_pair(tm, account).await?;
|
} = create_auth_pair(tm, account).await?;
|
||||||
|
|
||||||
Ok(Json(api::SignInOutput {
|
Ok(Json(api::SessionOutput {
|
||||||
access_token: access_token_string,
|
access_token: access_token_string,
|
||||||
refresh_token: refresh_token_string,
|
refresh_token: refresh_token_string,
|
||||||
exp: access_token.expiration_time,
|
exp: access_token.expiration_time,
|
||||||
@ -253,5 +269,6 @@ pub(crate) fn configure(config: &mut ServiceConfig) {
|
|||||||
.service(product)
|
.service(product)
|
||||||
.service(products)
|
.service(products)
|
||||||
.service(stocks)
|
.service(stocks)
|
||||||
.service(sign_in);
|
.service(sign_in)
|
||||||
|
.service(create_account);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Failure {
|
||||||
|
pub errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
@ -307,7 +312,7 @@ pub struct SignInInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SignInOutput {
|
pub struct SessionOutput {
|
||||||
pub access_token: AccessTokenString,
|
pub access_token: AccessTokenString,
|
||||||
pub refresh_token: RefreshTokenString,
|
pub refresh_token: RefreshTokenString,
|
||||||
pub exp: NaiveDateTime,
|
pub exp: NaiveDateTime,
|
||||||
|
@ -9,7 +9,7 @@ pub mod encrypt;
|
|||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_more::{Deref, Display, From};
|
use derive_more::{Deref, DerefMut, Display, From};
|
||||||
#[cfg(feature = "dummy")]
|
#[cfg(feature = "dummy")]
|
||||||
use fake::Fake;
|
use fake::Fake;
|
||||||
#[cfg(feature = "dummy")]
|
#[cfg(feature = "dummy")]
|
||||||
@ -305,10 +305,20 @@ impl Login {
|
|||||||
|
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
#[derive(Serialize, Debug, Deref, From, Display)]
|
#[derive(Serialize, Debug, Clone, Deref, DerefMut, From, Display)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Email(String);
|
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 {
|
impl FromStr for Email {
|
||||||
type Err = TransformError;
|
type Err = TransformError;
|
||||||
|
|
||||||
@ -319,10 +329,6 @@ impl FromStr for Email {
|
|||||||
Err(TransformError::NotEmail)
|
Err(TransformError::NotEmail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invalid_empty() -> Self {
|
|
||||||
Self("".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for Email {
|
impl<'de> serde::Deserialize<'de> for Email {
|
||||||
@ -582,7 +588,7 @@ impl ResetToken {
|
|||||||
|
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Deref, From, Display)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Password(String);
|
pub struct Password(String);
|
||||||
|
|
||||||
@ -595,10 +601,16 @@ impl Password {
|
|||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Deref, From, Display)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct PasswordConfirmation(String);
|
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", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
|
||||||
|
@ -12,9 +12,11 @@ model = { path = "../shared/model", features = ["dummy"] }
|
|||||||
seed = { version = "0.9.1", features = [] }
|
seed = { version = "0.9.1", features = [] }
|
||||||
seed_heroicons = { git = "https://github.com/mh84/seed_heroicons.git" }
|
seed_heroicons = { git = "https://github.com/mh84/seed_heroicons.git" }
|
||||||
|
|
||||||
chrono = { version = "*" }
|
chrono = { version = "*", features = ["wasm-bindgen"] }
|
||||||
gloo-timers = { version = "*", features = ["futures"] }
|
gloo-timers = { version = "*", features = ["futures"] }
|
||||||
|
|
||||||
|
uuid = { version = "1.0.0", features = ["v4"] }
|
||||||
|
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.81" }
|
serde_json = { version = "1.0.81" }
|
||||||
serde-wasm-bindgen = { version = "0.4.2" }
|
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;
|
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 model::{AccessTokenString, RefreshTokenString};
|
||||||
use seed::prelude::*;
|
use seed::fetch::{Header, Method, Request};
|
||||||
|
|
||||||
pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
use crate::api::perform;
|
||||||
Request::new("/api/v1/products")
|
|
||||||
.method(Method::Get)
|
pub async fn fetch_products() -> super::NetRes<model::api::Products> {
|
||||||
.fetch()
|
perform(Request::new("/api/v1/products").method(Method::Get)).await
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_product(product_id: model::ProductId) -> fetch::Result<model::api::Product> {
|
pub async fn fetch_product(product_id: model::ProductId) -> super::NetRes<model::api::Product> {
|
||||||
Request::new(format!("/api/v1/product/{}", product_id))
|
perform(Request::new(format!("/api/v1/product/{}", product_id)).method(Method::Get)).await
|
||||||
.method(Method::Get)
|
|
||||||
.fetch()
|
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_me(access_token: AccessTokenString) -> fetch::Result<model::Account> {
|
pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes<model::Account> {
|
||||||
|
perform(
|
||||||
Request::new("/api/v1/me")
|
Request::new("/api/v1/me")
|
||||||
.header(fetch::Header::bearer(access_token.as_str()))
|
.header(Header::bearer(access_token.as_str()))
|
||||||
.method(Method::Get)
|
.method(Method::Get),
|
||||||
.fetch()
|
)
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.json()
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_in(input: model::api::SignInInput) -> fetch::Result<model::api::SignInOutput> {
|
pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes<model::api::SessionOutput> {
|
||||||
|
perform(
|
||||||
Request::new("/api/v1/sign-in")
|
Request::new("/api/v1/sign-in")
|
||||||
.method(Method::Post)
|
.method(Method::Post)
|
||||||
.json(&input)?
|
.json(&input)
|
||||||
.fetch()
|
.map_err(crate::api::NetRes::Http)?,
|
||||||
.await?
|
)
|
||||||
.check_status()?
|
|
||||||
.json()
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_token(access_token: AccessTokenString) -> fetch::Result<String> {
|
pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<String> {
|
||||||
|
perform(
|
||||||
Request::new("/api/v1/token/verify")
|
Request::new("/api/v1/token/verify")
|
||||||
.method(Method::Post)
|
.method(Method::Post)
|
||||||
.header(fetch::Header::bearer(access_token.as_str()))
|
.header(Header::bearer(access_token.as_str())),
|
||||||
.fetch()
|
)
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.json()
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_token(
|
pub async fn refresh_token(
|
||||||
access_token: RefreshTokenString,
|
access_token: RefreshTokenString,
|
||||||
) -> fetch::Result<model::api::SignInOutput> {
|
) -> super::NetRes<model::api::SessionOutput> {
|
||||||
|
perform(
|
||||||
Request::new("/api/v1/token/refresh")
|
Request::new("/api/v1/token/refresh")
|
||||||
.method(Method::Post)
|
.method(Method::Post)
|
||||||
.header(fetch::Header::bearer(access_token.as_str()))
|
.header(Header::bearer(access_token.as_str())),
|
||||||
.fetch()
|
)
|
||||||
.await?
|
.await
|
||||||
.check_status()?
|
}
|
||||||
.json()
|
|
||||||
|
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
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
#![feature(try_trait_v2)]
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
mod model;
|
mod model;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
pub mod session;
|
||||||
pub mod shared;
|
pub mod shared;
|
||||||
|
|
||||||
use seed::empty;
|
use seed::empty;
|
||||||
@ -10,6 +13,7 @@ use seed::prelude::*;
|
|||||||
use crate::i18n::I18n;
|
use crate::i18n::I18n;
|
||||||
use crate::model::Model;
|
use crate::model::Model;
|
||||||
use crate::pages::{Msg, Page, PublicPage};
|
use crate::pages::{Msg, Page, PublicPage};
|
||||||
|
use crate::session::SessionMsg;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fetch_page {
|
macro_rules! fetch_page {
|
||||||
@ -62,11 +66,9 @@ macro_rules! fetch_page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||||
orders
|
orders.subscribe(Msg::UrlChanged);
|
||||||
.stream(streams::interval(500, || Msg::CheckAccessToken))
|
|
||||||
.subscribe(Msg::UrlChanged);
|
|
||||||
|
|
||||||
Model {
|
let mut model = Model {
|
||||||
url: url.clone().set_path(&[] as &[&str]),
|
url: url.clone().set_path(&[] as &[&str]),
|
||||||
token: LocalStorage::get("auth-token").ok(),
|
token: LocalStorage::get("auth-token").ok(),
|
||||||
page: Page::init(url, orders),
|
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")),
|
.and_then(|el: web_sys::Element| el.get_attribute("href")),
|
||||||
shared: shared::Model::default(),
|
shared: shared::Model::default(),
|
||||||
i18n: I18n::load(),
|
i18n: I18n::load(),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
session::init(&mut model, orders);
|
||||||
|
|
||||||
|
model
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
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) => {
|
Msg::Shared(msg) => {
|
||||||
shared::update(msg, &mut model.shared, orders);
|
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::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url, orders),
|
||||||
Msg::Public(pages::public::Msg::Listing(msg)) => {
|
Msg::Public(pages::public::Msg::Listing(msg)) => {
|
||||||
let page = fetch_page!(public model, Listing);
|
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);
|
let page = fetch_page!(public model, SignIn);
|
||||||
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
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)) => {
|
Msg::Public(pages::public::Msg::SignUp(msg)) => {
|
||||||
let page = fetch_page!(public model, SignUp);
|
let page = fetch_page!(public model, SignUp);
|
||||||
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))
|
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))
|
||||||
}
|
}
|
||||||
Msg::Admin(_) => {}
|
Msg::Admin(_) => {}
|
||||||
|
Msg::Session(msg) => {
|
||||||
|
session::update(msg, model, orders);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ pub enum Msg {
|
|||||||
Public(public::Msg),
|
Public(public::Msg),
|
||||||
Admin(admin::Msg),
|
Admin(admin::Msg),
|
||||||
UrlChanged(subs::UrlChanged),
|
UrlChanged(subs::UrlChanged),
|
||||||
CheckAccessToken,
|
|
||||||
Shared(shared::Msg),
|
Shared(shared::Msg),
|
||||||
|
Session(crate::session::SessionMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AdminPage {
|
pub enum AdminPage {
|
||||||
|
@ -69,11 +69,11 @@ pub mod layout {
|
|||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
pub fn view<Msg>(
|
pub fn view(
|
||||||
model: &crate::Model,
|
model: &crate::Model,
|
||||||
content: Node<Msg>,
|
content: Node<crate::Msg>,
|
||||||
categories: Option<&[model::api::Category]>,
|
categories: Option<&[model::api::Category]>,
|
||||||
) -> Node<Msg> {
|
) -> Node<crate::Msg> {
|
||||||
let sidebar = match categories {
|
let sidebar = match categories {
|
||||||
Some(categories) => {
|
Some(categories) => {
|
||||||
let sidebar = super::sidebar::view(model, categories);
|
let sidebar = super::sidebar::view(model, categories);
|
||||||
@ -84,9 +84,11 @@ pub mod layout {
|
|||||||
}
|
}
|
||||||
_ => empty![],
|
_ => empty![],
|
||||||
};
|
};
|
||||||
|
let notifications = crate::shared::notification::view(model);
|
||||||
div![
|
div![
|
||||||
C!["flex"],
|
C!["flex"],
|
||||||
sidebar,
|
sidebar,
|
||||||
|
notifications,
|
||||||
div![C!["w-full h-full p-4 m-8 overflow-y-auto"], content]
|
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> {
|
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()
|
.listing()
|
||||||
.add_path_part(category.key.as_str());
|
.add_path_part(category.key.as_str());
|
||||||
li![
|
li![
|
||||||
|
@ -4,6 +4,7 @@ use seed::app::Orders;
|
|||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
|
use crate::api::NetRes;
|
||||||
use crate::pages::Urls;
|
use crate::pages::Urls;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -19,21 +20,19 @@ pub struct ListingPage {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
FetchProducts,
|
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 {
|
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
||||||
orders.send_msg(Msg::FetchProducts);
|
orders.send_msg(Msg::FetchProducts);
|
||||||
let model = ListingPage {
|
ListingPage {
|
||||||
product_ids: vec![],
|
product_ids: vec![],
|
||||||
filters: url_to_filters(url),
|
filters: url_to_filters(url),
|
||||||
products: Default::default(),
|
products: Default::default(),
|
||||||
errors: vec![],
|
errors: vec![],
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
visible_products: vec![],
|
visible_products: vec![],
|
||||||
};
|
}
|
||||||
seed::log!(&model);
|
|
||||||
model
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url_to_filters(mut url: Url) -> HashSet<String> {
|
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) }
|
async { Msg::ProductFetched(crate::api::public::fetch_products().await) }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Msg::ProductFetched(Ok(products)) => {
|
Msg::ProductFetched(NetRes::Success(products)) => {
|
||||||
model.categories = products
|
model.categories = products
|
||||||
.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
@ -103,7 +102,7 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>)
|
|||||||
};
|
};
|
||||||
filter_products(model);
|
filter_products(model);
|
||||||
}
|
}
|
||||||
Msg::ProductFetched(Err(_e)) => {
|
Msg::ProductFetched(NetRes::Error(_)) | Msg::ProductFetched(NetRes::Http(_)) => {
|
||||||
model.errors.push("Failed to load products".into());
|
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())
|
.map(|photo| photo.url.as_str())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let url = Urls::new(model.url.clone())
|
let url = Urls::new(&model.url)
|
||||||
.product()
|
.product()
|
||||||
.add_path_part((*product.id as i32).to_string());
|
.add_path_part((*product.id as i32).to_string());
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
|
use crate::api::NetRes;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
ProductFetched(fetch::Result<model::api::Product>),
|
ProductFetched(crate::api::NetRes<model::api::Product>),
|
||||||
SelectImage(usize),
|
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>) {
|
pub fn update(msg: Msg, model: &mut ProductPage, _orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::ProductFetched(Ok(product)) => {
|
Msg::ProductFetched(NetRes::Success(product)) => {
|
||||||
model.product = Some(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);
|
seed::error!(e);
|
||||||
}
|
}
|
||||||
Msg::SelectImage(selected) => {
|
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 update(_msg: Msg, _model: &mut SignInPage, _orders: &mut impl Orders<Msg>) {}
|
||||||
|
|
||||||
pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::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![
|
let content = div![
|
||||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||||
div![
|
div![
|
||||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
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"],
|
h1![
|
||||||
|
C!["text-3xl font-semibold text-center text-indigo-700"],
|
||||||
|
logo
|
||||||
|
],
|
||||||
sign_in_form(model, page),
|
sign_in_form(model, page),
|
||||||
p![
|
p![
|
||||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||||
model.i18n.t("Don't have an account?"),
|
model.i18n.t("Don't have an account?"),
|
||||||
a![
|
a![
|
||||||
C!["font-medium text-indigo-600 hover:underline"],
|
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")
|
model.i18n.t("Sign up")
|
||||||
]
|
]
|
||||||
@ -48,10 +62,8 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
|||||||
]
|
]
|
||||||
.map_msg(Into::into);
|
.map_msg(Into::into);
|
||||||
|
|
||||||
div![
|
// crate::shared::view::public_navbar(model),
|
||||||
crate::shared::view::public_navbar(model),
|
div![super::layout::view(model, content, None)]
|
||||||
super::layout::view(model, content, None)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
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![
|
a![
|
||||||
C!["text-xs text-indigo-600 hover:underline"],
|
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?"),
|
model.i18n.t("Forget Password?"),
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use model::{Email, Login, Password, PasswordConfirmation};
|
||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
@ -10,7 +11,9 @@ pub enum Msg {
|
|||||||
LoginChanged(String),
|
LoginChanged(String),
|
||||||
EmailChanged(String),
|
EmailChanged(String),
|
||||||
PasswordChanged(String),
|
PasswordChanged(String),
|
||||||
|
PasswordConfirmationChanged(String),
|
||||||
Submit,
|
Submit,
|
||||||
|
AccountCreated(crate::api::NetRes<model::api::SessionOutput>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -18,6 +21,7 @@ pub struct SignUpPage {
|
|||||||
pub login: model::Login,
|
pub login: model::Login,
|
||||||
pub email: model::Email,
|
pub email: model::Email,
|
||||||
pub password: model::Password,
|
pub password: model::Password,
|
||||||
|
pub password_confirmation: model::PasswordConfirmation,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
||||||
@ -25,26 +29,78 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
|||||||
login: model::Login::new(""),
|
login: model::Login::new(""),
|
||||||
email: model::Email::invalid_empty(),
|
email: model::Email::invalid_empty(),
|
||||||
password: model::Password::new(""),
|
password: model::Password::new(""),
|
||||||
|
password_confirmation: model::PasswordConfirmation::new(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
|
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> {
|
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![
|
let content = div![
|
||||||
C!["relative flex flex-col justify-center overflow-hidden"],
|
C!["relative flex flex-col justify-center overflow-hidden"],
|
||||||
div![
|
div![
|
||||||
C!["w-full p-6 m-auto bg-white border-t-4 rounded-md shadow-md border-top lg:max-w-md"],
|
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"],
|
h1![
|
||||||
|
C!["text-3xl font-semibold text-center text-indigo-700"],
|
||||||
|
logo
|
||||||
|
],
|
||||||
sign_up_form(model, page),
|
sign_up_form(model, page),
|
||||||
p![
|
p![
|
||||||
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
C!["mt-8 text-xs font-light text-center text-indigo-700"],
|
||||||
model.i18n.t("Have an account?"),
|
model.i18n.t("Have an account?"),
|
||||||
a![
|
a![
|
||||||
C!["font-medium text-indigo-600 hover:underline"],
|
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")
|
model.i18n.t("Sign in")
|
||||||
]
|
]
|
||||||
@ -52,10 +108,7 @@ pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
.map_msg(Into::into);
|
.map_msg(Into::into);
|
||||||
div![
|
div![super::layout::view(model, content, None)]
|
||||||
crate::shared::view::public_navbar(model),
|
|
||||||
super::layout::view(model, content, None)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
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"],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
|
||||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::EmailChanged)
|
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"],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
|
||||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged)
|
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"]],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
|
||||||
ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged)
|
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![
|
div![
|
||||||
C!["mt-6"],
|
C!["mt-6"],
|
||||||
button![
|
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 use crate::shared::msg::Msg;
|
||||||
|
|
||||||
pub mod msg;
|
pub mod msg;
|
||||||
|
pub mod notification;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@ -11,6 +12,7 @@ pub struct Model {
|
|||||||
pub refresh_token: Option<model::RefreshTokenString>,
|
pub refresh_token: Option<model::RefreshTokenString>,
|
||||||
pub exp: Option<chrono::NaiveDateTime>,
|
pub exp: Option<chrono::NaiveDateTime>,
|
||||||
pub me: Option<model::Account>,
|
pub me: Option<model::Account>,
|
||||||
|
pub notifications: Vec<notification::Notification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<crate::Msg>) {
|
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);
|
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) => {
|
Msg::SignIn(input) => {
|
||||||
orders
|
orders
|
||||||
.skip()
|
.skip()
|
||||||
.perform_cmd(async { Msg::SignedIn(crate::api::public::sign_in(input).await) });
|
.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);
|
handle_auth_pair(pair, model, orders);
|
||||||
}
|
}
|
||||||
Msg::SignedIn(Err(_err)) => {}
|
Msg::SignedIn(crate::api::NetRes::Error(_err)) => {}
|
||||||
Msg::RefreshToken(token) => {
|
Msg::SignedIn(crate::api::NetRes::Http(_err)) => {}
|
||||||
orders.skip().perform_cmd(async {
|
Msg::Notification(msg) => {
|
||||||
Msg::TokenRefreshed(crate::api::public::refresh_token(token).await)
|
notification::update(msg, model, orders);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Msg::TokenRefreshed(Ok(pair)) => {
|
|
||||||
handle_auth_pair(pair, model, orders);
|
|
||||||
}
|
|
||||||
Msg::TokenRefreshed(Err(_err)) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_auth_pair(
|
fn handle_auth_pair(
|
||||||
pair: model::api::SignInOutput,
|
pair: model::api::SessionOutput,
|
||||||
model: &mut Model,
|
model: &mut Model,
|
||||||
_orders: &mut impl Orders<crate::Msg>,
|
_orders: &mut impl Orders<crate::Msg>,
|
||||||
) {
|
) {
|
||||||
let model::api::SignInOutput {
|
let model::api::SessionOutput {
|
||||||
access_token,
|
access_token,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
exp,
|
exp,
|
||||||
|
@ -3,9 +3,8 @@ use seed::fetch::Result;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
LoadMe,
|
LoadMe,
|
||||||
MeLoaded(Result<model::Account>),
|
MeLoaded(crate::api::NetRes<model::Account>),
|
||||||
SignIn(model::api::SignInInput),
|
SignIn(model::api::SignInInput),
|
||||||
SignedIn(Result<model::api::SignInOutput>),
|
SignedIn(crate::api::NetRes<model::api::SessionOutput>),
|
||||||
RefreshToken(model::RefreshTokenString),
|
Notification(crate::shared::notification::NotificationMsg),
|
||||||
TokenRefreshed(Result<model::api::SignInOutput>),
|
|
||||||
}
|
}
|
||||||
|
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