Refresh tokens, sign in and so on
This commit is contained in:
parent
9072819de6
commit
da218adcbd
@ -19,6 +19,41 @@ macro_rules! cart_async_handler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! query_cart {
|
||||||
|
($cart: expr, $msg: expr, default $fail: expr) => {
|
||||||
|
match $cart.send($msg).await {
|
||||||
|
Ok(Ok(r)) => r,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
$fail
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
$fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($cart: expr, $msg: expr, $fail: expr) => {
|
||||||
|
$crate::query_cart!($cart, $msg, $fail, $fail)
|
||||||
|
};
|
||||||
|
|
||||||
|
($cart: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
|
||||||
|
match $cart.send($msg).await {
|
||||||
|
Ok(Ok(r)) => r,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
return Err($db_fail);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
return Err($act_fail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("System can't ensure shopping cart existence")]
|
#[error("System can't ensure shopping cart existence")]
|
||||||
|
@ -24,7 +24,7 @@ pub(crate) async fn token_by_jti(msg: TokenByJti, pool: PgPool) -> Result<Token>
|
|||||||
sqlx::query_as(r#"
|
sqlx::query_as(r#"
|
||||||
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
||||||
FROM tokens
|
FROM tokens
|
||||||
WHERE jwt_id = $1
|
WHERE jwt_id = $1 AND expiration_time > now()
|
||||||
"#)
|
"#)
|
||||||
.bind(msg.jti)
|
.bind(msg.jti)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
@ -69,3 +69,41 @@ RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not
|
|||||||
crate::Error::Token(Error::Create)
|
crate::Error::Token(Error::Create)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<Token>")]
|
||||||
|
pub struct CreateExtendedToken {
|
||||||
|
pub customer_id: uuid::Uuid,
|
||||||
|
pub role: model::Role,
|
||||||
|
pub subject: AccountId,
|
||||||
|
pub audience: Audience,
|
||||||
|
pub expiration_time: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
db_async_handler!(CreateExtendedToken, create_extended_token, Token);
|
||||||
|
|
||||||
|
pub(crate) async fn create_extended_token(msg: CreateExtendedToken, pool: PgPool) -> Result<Token> {
|
||||||
|
let CreateExtendedToken {
|
||||||
|
customer_id,
|
||||||
|
role,
|
||||||
|
subject,
|
||||||
|
audience,
|
||||||
|
expiration_time,
|
||||||
|
} = msg;
|
||||||
|
sqlx::query_as(r#"
|
||||||
|
INSERT INTO tokens (customer_id, role, subject, audience, expiration_time)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
||||||
|
"#)
|
||||||
|
.bind(customer_id)
|
||||||
|
.bind(role)
|
||||||
|
.bind(subject)
|
||||||
|
.bind(audience)
|
||||||
|
.bind(expiration_time)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
crate::Error::Token(Error::Create)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -51,7 +51,7 @@ macro_rules! query_fs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
($fs: expr, $msg: expr, $fail: expr) => {
|
($fs: expr, $msg: expr, $fail: expr) => {
|
||||||
$crate::query_db!($fs, $msg, $fail, $fail)
|
$crate::query_fs!($fs, $msg, $fail, $fail)
|
||||||
};
|
};
|
||||||
|
|
||||||
($fs: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
|
($fs: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
|
||||||
|
@ -7,7 +7,7 @@ use config::SharedAppConfig;
|
|||||||
use database_manager::{query_db, Database};
|
use database_manager::{query_db, Database};
|
||||||
use hmac::digest::KeyInit;
|
use hmac::digest::KeyInit;
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use model::{AccountId, Audience, Role, Token, TokenString};
|
use model::{AccessTokenString, AccountId, Audience, Role, Token};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
@ -26,6 +26,62 @@ macro_rules! token_async_handler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! query_tm {
|
||||||
|
($tm: expr, $msg: expr, default $fail: expr) => {
|
||||||
|
match $tm.send($msg).await {
|
||||||
|
Ok(Ok(r)) => r,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
$fail
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
$fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(multi, $tm: expr, $fail: expr, $($msg: expr),*) => {{
|
||||||
|
use futures_util::TryFutureExt;
|
||||||
|
tokio::join!(
|
||||||
|
$(
|
||||||
|
$tm.send($msg).map_ok_or_else(
|
||||||
|
|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
Err($fail)
|
||||||
|
},
|
||||||
|
|res| match res {
|
||||||
|
Ok(rec) => Ok(rec),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
Err($fail)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
),*
|
||||||
|
)
|
||||||
|
}};
|
||||||
|
|
||||||
|
($tm: expr, $msg: expr, $fail: expr) => {
|
||||||
|
$crate::query_tm!($tm, $msg, $fail, $fail)
|
||||||
|
};
|
||||||
|
|
||||||
|
($tm: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
|
||||||
|
match $tm.send($msg).await {
|
||||||
|
Ok(Ok(r)) => r,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{e}");
|
||||||
|
return Err($db_fail);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
return Err($act_fail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/*struct Jwt {
|
/*struct Jwt {
|
||||||
/// cti (customer id): Customer uuid identifier used by payment service
|
/// cti (customer id): Customer uuid identifier used by payment service
|
||||||
pub cti: uuid::Uuid,
|
pub cti: uuid::Uuid,
|
||||||
@ -80,40 +136,56 @@ impl TokenManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "Result<(Token, TokenString)>")]
|
#[rtype(result = "Result<(Token, AccessTokenString)>")]
|
||||||
pub struct CreateToken {
|
pub struct CreateToken {
|
||||||
pub customer_id: uuid::Uuid,
|
pub customer_id: uuid::Uuid,
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub subject: AccountId,
|
pub subject: AccountId,
|
||||||
pub audience: Option<Audience>,
|
pub audience: Option<Audience>,
|
||||||
|
pub exp: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
token_async_handler!(CreateToken, create_token, (Token, TokenString));
|
token_async_handler!(CreateToken, create_token, (Token, AccessTokenString));
|
||||||
|
|
||||||
pub(crate) async fn create_token(
|
pub(crate) async fn create_token(
|
||||||
msg: CreateToken,
|
msg: CreateToken,
|
||||||
db: Addr<Database>,
|
db: Addr<Database>,
|
||||||
config: SharedAppConfig,
|
config: SharedAppConfig,
|
||||||
) -> Result<(Token, TokenString)> {
|
) -> Result<(Token, AccessTokenString)> {
|
||||||
let CreateToken {
|
let CreateToken {
|
||||||
customer_id,
|
customer_id,
|
||||||
role,
|
role,
|
||||||
subject,
|
subject,
|
||||||
audience,
|
audience,
|
||||||
|
exp,
|
||||||
} = msg;
|
} = msg;
|
||||||
let audience = audience.unwrap_or_default();
|
let audience = audience.unwrap_or_default();
|
||||||
|
|
||||||
let token: Token = query_db!(
|
let token: Token = match exp {
|
||||||
db,
|
None => query_db!(
|
||||||
database_manager::CreateToken {
|
db,
|
||||||
customer_id,
|
database_manager::CreateToken {
|
||||||
role,
|
customer_id,
|
||||||
subject,
|
role,
|
||||||
audience,
|
subject,
|
||||||
},
|
audience,
|
||||||
Error::Save,
|
},
|
||||||
Error::SaveInternal
|
Error::Save,
|
||||||
);
|
Error::SaveInternal
|
||||||
|
),
|
||||||
|
Some(exp) => query_db!(
|
||||||
|
db,
|
||||||
|
database_manager::CreateExtendedToken {
|
||||||
|
customer_id,
|
||||||
|
role,
|
||||||
|
subject,
|
||||||
|
audience,
|
||||||
|
expiration_time: exp
|
||||||
|
},
|
||||||
|
Error::Save,
|
||||||
|
Error::SaveInternal
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
let token_string = {
|
let token_string = {
|
||||||
use jwt::SignWithKey;
|
use jwt::SignWithKey;
|
||||||
@ -169,7 +241,7 @@ pub(crate) async fn create_token(
|
|||||||
return Err(Error::SaveInternal);
|
return Err(Error::SaveInternal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
TokenString::from(s)
|
AccessTokenString::new(s)
|
||||||
};
|
};
|
||||||
Ok((token, token_string))
|
Ok((token, token_string))
|
||||||
}
|
}
|
||||||
@ -177,7 +249,7 @@ pub(crate) async fn create_token(
|
|||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "Result<(Token, bool)>")]
|
#[rtype(result = "Result<(Token, bool)>")]
|
||||||
pub struct Validate {
|
pub struct Validate {
|
||||||
pub token: TokenString,
|
pub token: AccessTokenString,
|
||||||
}
|
}
|
||||||
|
|
||||||
token_async_handler!(Validate, validate, (Token, bool));
|
token_async_handler!(Validate, validate, (Token, bool));
|
||||||
@ -214,6 +286,10 @@ pub(crate) async fn validate(
|
|||||||
Error::ValidateInternal
|
Error::ValidateInternal
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if token.expiration_time < Utc::now().naive_utc() {
|
||||||
|
return Err(Error::Validate);
|
||||||
|
}
|
||||||
|
|
||||||
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
|
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
|
||||||
return Ok((token, false));
|
return Ok((token, false));
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,7 @@ use actix_web::web::{scope, Data, Json, ServiceConfig};
|
|||||||
use actix_web::{delete, get, post, HttpResponse};
|
use actix_web::{delete, get, post, HttpResponse};
|
||||||
use config::SharedAppConfig;
|
use config::SharedAppConfig;
|
||||||
use database_manager::{query_db, Database};
|
use database_manager::{query_db, Database};
|
||||||
use model::{Account, Email, Encrypt, Login, PassHash, Password, PasswordConfirmation, Role};
|
use model::{Encrypt, PassHash};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
use crate::routes::{RequireLogin, Result};
|
use crate::routes::{RequireLogin, Result};
|
||||||
@ -43,29 +42,19 @@ pub enum Error {
|
|||||||
Database(#[from] database_manager::Error),
|
Database(#[from] database_manager::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct LogoutResponse {}
|
|
||||||
|
|
||||||
#[delete("logout")]
|
#[delete("logout")]
|
||||||
async fn logout(session: Session) -> Result<HttpResponse> {
|
async fn logout(session: Session) -> Result<Json<model::api::admin::LogoutResponse>> {
|
||||||
session.require_admin()?;
|
session.require_admin()?;
|
||||||
session.clear();
|
session.clear();
|
||||||
|
|
||||||
Ok(HttpResponse::NotImplemented().body(""))
|
Ok(Json(model::api::admin::LogoutResponse {}))
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct SignInInput {
|
|
||||||
login: Option<Login>,
|
|
||||||
email: Option<Email>,
|
|
||||||
password: Password,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sign-in")]
|
#[post("/sign-in")]
|
||||||
async fn sign_in(
|
async fn sign_in(
|
||||||
session: Session,
|
session: Session,
|
||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
Json(payload): Json<SignInInput>,
|
Json(payload): Json<model::api::admin::SignInInput>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
log::debug!("{:?}", payload);
|
log::debug!("{:?}", payload);
|
||||||
let db = db.into_inner();
|
let db = db.into_inner();
|
||||||
@ -88,40 +77,21 @@ async fn sign_in(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct RegisterInput {
|
|
||||||
pub login: Login,
|
|
||||||
pub email: Email,
|
|
||||||
pub password: Password,
|
|
||||||
pub password_confirmation: PasswordConfirmation,
|
|
||||||
pub role: Role,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Default)]
|
|
||||||
pub struct RegisterResponse {
|
|
||||||
pub success: bool,
|
|
||||||
pub errors: Vec<RegisterError>,
|
|
||||||
pub account: Option<Account>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub enum RegisterError {
|
|
||||||
PasswordDiffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
// login_required
|
// login_required
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(
|
async fn register(
|
||||||
session: Session,
|
session: Session,
|
||||||
Json(input): Json<RegisterInput>,
|
Json(input): Json<model::api::admin::RegisterInput>,
|
||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
config: Data<SharedAppConfig>,
|
config: Data<SharedAppConfig>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let mut response = RegisterResponse::default();
|
let mut response = model::api::admin::RegisterResponse::default();
|
||||||
session.require_admin()?;
|
session.require_admin()?;
|
||||||
|
|
||||||
if input.password != input.password_confirmation {
|
if input.password != input.password_confirmation {
|
||||||
response.errors.push(RegisterError::PasswordDiffer);
|
response
|
||||||
|
.errors
|
||||||
|
.push(model::api::admin::RegisterError::PasswordDiffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = match input.password.encrypt(&config.lock().web().pass_salt()) {
|
let hash = match input.password.encrypt(&config.lock().web().pass_salt()) {
|
||||||
|
@ -9,9 +9,9 @@ use actix_session::Session;
|
|||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
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::{RecordId, Token, TokenString};
|
use model::{AccessTokenString, RecordId, Token};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use token_manager::TokenManager;
|
use token_manager::{query_tm, TokenManager};
|
||||||
|
|
||||||
pub use self::admin::Error as AdminError;
|
pub use self::admin::Error as AdminError;
|
||||||
pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
|
pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
|
||||||
@ -132,15 +132,12 @@ pub trait RequireUser {
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
|
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
|
||||||
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)> {
|
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)> {
|
||||||
match tm
|
Ok(query_tm!(
|
||||||
.send(token_manager::Validate {
|
tm,
|
||||||
token: TokenString::from(String::from(self.token())),
|
token_manager::Validate {
|
||||||
})
|
token: AccessTokenString::new(self.token()),
|
||||||
.await
|
},
|
||||||
{
|
Error::Unauthorized
|
||||||
Ok(Ok(res)) => Ok(res),
|
))
|
||||||
Ok(Err(_e)) => Err(Error::Unauthorized),
|
|
||||||
Err(_) => Err(Error::Unauthorized),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,60 @@ use actix::Addr;
|
|||||||
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||||
use actix_web::{delete, get, post, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, post, HttpRequest, HttpResponse};
|
||||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
use cart_manager::CartManager;
|
use cart_manager::{query_cart, CartManager};
|
||||||
use database_manager::{query_db, Database};
|
use database_manager::{query_db, Database};
|
||||||
use model::{api, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartItemId};
|
use model::api;
|
||||||
use payment_manager::{query_pay, PaymentManager};
|
use payment_manager::{query_pay, PaymentManager};
|
||||||
use token_manager::TokenManager;
|
use token_manager::TokenManager;
|
||||||
|
|
||||||
use crate::routes;
|
use crate::routes::public::api_v1::unrestricted::{create_auth_pair, AuthPair};
|
||||||
use crate::routes::public::api_v1::{Error as ApiV1Error, ShoppingCartError};
|
use crate::routes::public::api_v1::{Error as ApiV1Error, ShoppingCartError};
|
||||||
use crate::routes::public::Error as PublicError;
|
use crate::routes::public::Error as PublicError;
|
||||||
use crate::routes::{RequireUser, Result};
|
use crate::routes::{RequireUser, Result};
|
||||||
|
use crate::{public_send_db, routes};
|
||||||
|
|
||||||
|
/// This requires [model::AccessTokenString] to be set as bearer
|
||||||
|
#[post("/token/verify")]
|
||||||
|
async fn verify_token(
|
||||||
|
tm: Data<Addr<TokenManager>>,
|
||||||
|
credentials: BearerAuth,
|
||||||
|
) -> routes::Result<String> {
|
||||||
|
let _token = credentials.require_user(tm.into_inner()).await?.0;
|
||||||
|
Ok("".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This requires [model::RefreshTokenString] to be set as bearer
|
||||||
|
#[post("/token/refresh")]
|
||||||
|
async fn refresh_token(
|
||||||
|
tm: Data<Addr<TokenManager>>,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
credentials: BearerAuth,
|
||||||
|
) -> routes::Result<Json<api::SignInOutput>> {
|
||||||
|
let account_id: model::AccountId = credentials
|
||||||
|
.require_user(tm.clone().into_inner())
|
||||||
|
.await?
|
||||||
|
.0
|
||||||
|
.subject
|
||||||
|
.into();
|
||||||
|
let account: model::FullAccount = query_db!(
|
||||||
|
db,
|
||||||
|
database_manager::FindAccount { account_id },
|
||||||
|
routes::Error::Unauthorized
|
||||||
|
);
|
||||||
|
|
||||||
|
let AuthPair {
|
||||||
|
access_token,
|
||||||
|
access_token_string,
|
||||||
|
_refresh_token: _,
|
||||||
|
refresh_token_string,
|
||||||
|
} = create_auth_pair(tm, account).await?;
|
||||||
|
|
||||||
|
Ok(Json(api::SignInOutput {
|
||||||
|
access_token: access_token_string,
|
||||||
|
refresh_token: refresh_token_string,
|
||||||
|
exp: access_token.expiration_time,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/shopping-cart")]
|
#[get("/shopping-cart")]
|
||||||
async fn shopping_cart(
|
async fn shopping_cart(
|
||||||
@ -23,7 +67,7 @@ async fn shopping_cart(
|
|||||||
let cart: model::ShoppingCart = query_db!(
|
let cart: model::ShoppingCart = query_db!(
|
||||||
db,
|
db,
|
||||||
database_manager::EnsureActiveShoppingCart {
|
database_manager::EnsureActiveShoppingCart {
|
||||||
buyer_id: AccountId::from(token.subject),
|
buyer_id: token.subject.into(),
|
||||||
},
|
},
|
||||||
routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart(
|
routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart(
|
||||||
ShoppingCartError::Ensure
|
ShoppingCartError::Ensure
|
||||||
@ -43,60 +87,30 @@ async fn shopping_cart(
|
|||||||
Ok(Json(cart))
|
Ok(Json(cart))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct CreateItemInput {
|
|
||||||
pub product_id: ProductId,
|
|
||||||
pub quantity: Quantity,
|
|
||||||
pub quantity_unit: QuantityUnit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct CreateItemOutput {
|
|
||||||
pub success: bool,
|
|
||||||
pub shopping_cart_item: model::ShoppingCartItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/shopping-cart-item")]
|
#[post("/shopping-cart-item")]
|
||||||
async fn create_cart_item(
|
async fn create_cart_item(
|
||||||
cart: Data<Addr<CartManager>>,
|
cart: Data<Addr<CartManager>>,
|
||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
Json(payload): Json<CreateItemInput>,
|
Json(payload): Json<api::CreateItemInput>,
|
||||||
) -> Result<Json<CreateItemOutput>> {
|
) -> Result<Json<api::CreateItemOutput>> {
|
||||||
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
||||||
|
|
||||||
match cart
|
let item: model::ShoppingCartItem = query_cart!(
|
||||||
.send(cart_manager::AddItem {
|
cart,
|
||||||
buyer_id: AccountId::from(token.subject),
|
cart_manager::AddItem {
|
||||||
|
buyer_id: token.subject.into(),
|
||||||
product_id: payload.product_id,
|
product_id: payload.product_id,
|
||||||
quantity: payload.quantity,
|
quantity: payload.quantity,
|
||||||
quantity_unit: payload.quantity_unit,
|
quantity_unit: payload.quantity_unit,
|
||||||
})
|
},
|
||||||
.await
|
routes::Error::Public(super::Error::AddItem.into()),
|
||||||
{
|
routes::Error::Public(PublicError::DatabaseConnection)
|
||||||
Ok(Ok(item)) => Ok(Json(CreateItemOutput {
|
);
|
||||||
success: true,
|
Ok(Json(api::CreateItemOutput {
|
||||||
shopping_cart_item: item,
|
success: true,
|
||||||
})),
|
shopping_cart_item: item.into(),
|
||||||
Ok(Err(e)) => {
|
}))
|
||||||
log::error!("{e:}");
|
|
||||||
Err(routes::Error::Public(super::Error::AddItem.into()))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{e:?}");
|
|
||||||
Err(routes::Error::Public(PublicError::DatabaseConnection))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct DeleteItemInput {
|
|
||||||
pub shopping_cart_item_id: ShoppingCartItemId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct DeleteItemOutput {
|
|
||||||
pub success: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/shopping-cart-item")]
|
#[delete("/shopping-cart-item")]
|
||||||
@ -105,14 +119,14 @@ async fn delete_cart_item(
|
|||||||
cart: Data<Addr<CartManager>>,
|
cart: Data<Addr<CartManager>>,
|
||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
Json(payload): Json<DeleteItemInput>,
|
Json(payload): Json<api::DeleteItemInput>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
||||||
|
|
||||||
let sc: model::ShoppingCart = query_db!(
|
let sc: model::ShoppingCart = query_db!(
|
||||||
db,
|
db,
|
||||||
database_manager::EnsureActiveShoppingCart {
|
database_manager::EnsureActiveShoppingCart {
|
||||||
buyer_id: AccountId::from(token.subject),
|
buyer_id: token.subject.into(),
|
||||||
},
|
},
|
||||||
routes::Error::Public(super::Error::RemoveItem.into()),
|
routes::Error::Public(super::Error::RemoveItem.into()),
|
||||||
routes::Error::Public(PublicError::DatabaseConnection)
|
routes::Error::Public(PublicError::DatabaseConnection)
|
||||||
@ -126,10 +140,10 @@ async fn delete_cart_item(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(_)) => Ok(HttpResponse::Ok().json(DeleteItemOutput { success: true })),
|
Ok(Ok(_)) => Ok(HttpResponse::Ok().json(api::DeleteItemOutput { success: true })),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
log::error!("{e}");
|
log::error!("{e}");
|
||||||
Ok(HttpResponse::BadRequest().json(DeleteItemOutput { success: false }))
|
Ok(HttpResponse::BadRequest().json(api::DeleteItemOutput { success: false }))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
@ -138,51 +152,34 @@ async fn delete_cart_item(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[get("/me")]
|
||||||
pub struct CreateOrderInput {
|
pub(crate) async fn me(
|
||||||
/// Required customer e-mail
|
db: Data<Addr<Database>>,
|
||||||
pub email: String,
|
tm: Data<Addr<TokenManager>>,
|
||||||
/// Required customer phone number
|
credentials: BearerAuth,
|
||||||
pub phone: String,
|
) -> routes::Result<Json<model::Account>> {
|
||||||
/// Required customer first name
|
let account_id: model::AccountId = credentials
|
||||||
pub first_name: String,
|
.require_user(tm.into_inner())
|
||||||
/// Required customer last name
|
.await?
|
||||||
pub last_name: String,
|
.0
|
||||||
/// Required customer language
|
.subject
|
||||||
pub language: String,
|
.into();
|
||||||
/// False if customer is allowed to be charged on site.
|
let account: model::FullAccount =
|
||||||
/// Otherwise it should be true to use payment service for charging
|
public_send_db!(owned, db, database_manager::FindAccount { account_id });
|
||||||
pub charge_client: bool,
|
Ok(Json(account.into()))
|
||||||
/// User currency
|
|
||||||
pub currency: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/order")]
|
#[post("/order")]
|
||||||
pub(crate) async fn create_order(
|
pub(crate) async fn create_order(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
Json(payload): Json<CreateOrderInput>,
|
Json(payload): Json<api::CreateOrderInput>,
|
||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
payment: Data<Addr<PaymentManager>>,
|
payment: Data<Addr<PaymentManager>>,
|
||||||
) -> routes::Result<HttpResponse> {
|
) -> routes::Result<HttpResponse> {
|
||||||
let (
|
let subject = credentials.require_user(tm.into_inner()).await?.0.subject;
|
||||||
model::Token {
|
|
||||||
id: _,
|
|
||||||
customer_id: _,
|
|
||||||
role: _,
|
|
||||||
issuer: _,
|
|
||||||
subject,
|
|
||||||
audience: _,
|
|
||||||
expiration_time: _,
|
|
||||||
not_before_time: _,
|
|
||||||
issued_at_time: _,
|
|
||||||
jwt_id: _,
|
|
||||||
},
|
|
||||||
_,
|
|
||||||
) = credentials.require_user(tm.into_inner()).await?;
|
|
||||||
|
|
||||||
let buyer_id = model::AccountId::from(subject);
|
let api::CreateOrderInput {
|
||||||
let CreateOrderInput {
|
|
||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
first_name,
|
first_name,
|
||||||
@ -208,7 +205,7 @@ pub(crate) async fn create_order(
|
|||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
customer_ip: ip.to_string(),
|
customer_ip: ip.to_string(),
|
||||||
buyer_id,
|
buyer_id: subject.into(),
|
||||||
charge_client
|
charge_client
|
||||||
},
|
},
|
||||||
routes::Error::Public(PublicError::DatabaseConnection)
|
routes::Error::Public(PublicError::DatabaseConnection)
|
||||||
@ -222,11 +219,15 @@ pub(crate) async fn create_order(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||||
config.service(scope("")
|
let scoped = scope("")
|
||||||
.app_data(actix_web_httpauth::extractors::bearer::Config::default()
|
.app_data(actix_web_httpauth::extractors::bearer::Config::default()
|
||||||
.realm("user api")
|
.realm("user api")
|
||||||
.scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
|
.scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
|
||||||
.service(shopping_cart)
|
.service(shopping_cart)
|
||||||
.service(delete_cart_item)
|
.service(delete_cart_item)
|
||||||
.service(create_order));
|
.service(create_order)
|
||||||
|
.service(me)
|
||||||
|
.service(verify_token)
|
||||||
|
.service(refresh_token);
|
||||||
|
config.service(scoped);
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ use actix_web::web::{Data, Json, ServiceConfig};
|
|||||||
use actix_web::{get, post, HttpResponse};
|
use actix_web::{get, post, HttpResponse};
|
||||||
use config::SharedAppConfig;
|
use config::SharedAppConfig;
|
||||||
use database_manager::{query_db, Database};
|
use database_manager::{query_db, Database};
|
||||||
use model::{api, Audience, Encrypt, FullAccount, Token, TokenString};
|
use model::{api, AccessTokenString, Audience, Encrypt, FullAccount, RefreshTokenString, Token};
|
||||||
use payment_manager::{PaymentManager, PaymentNotification};
|
use payment_manager::{PaymentManager, PaymentNotification};
|
||||||
use token_manager::TokenManager;
|
use token_manager::{query_tm, TokenManager};
|
||||||
|
|
||||||
|
use crate::public_send_db;
|
||||||
use crate::routes::public::Error as PublicError;
|
use crate::routes::public::Error as PublicError;
|
||||||
use crate::routes::{self, Result};
|
use crate::routes::{self, Result};
|
||||||
use crate::{public_send_db, Login, Password};
|
|
||||||
|
|
||||||
#[get("/products")]
|
#[get("/products")]
|
||||||
async fn products(
|
async fn products(
|
||||||
@ -38,18 +38,10 @@ async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
|
|||||||
public_send_db!(db.into_inner(), database_manager::AllStocks)
|
public_send_db!(db.into_inner(), database_manager::AllStocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct CreateAccountInput {
|
|
||||||
pub email: model::Email,
|
|
||||||
pub login: Login,
|
|
||||||
pub password: Password,
|
|
||||||
pub password_confirmation: model::PasswordConfirmation,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
pub async fn create_account(
|
pub async fn create_account(
|
||||||
db: Data<Addr<Database>>,
|
db: Data<Addr<Database>>,
|
||||||
Json(payload): Json<CreateAccountInput>,
|
Json(payload): Json<api::CreateAccountInput>,
|
||||||
config: Data<SharedAppConfig>,
|
config: Data<SharedAppConfig>,
|
||||||
) -> routes::Result<HttpResponse> {
|
) -> routes::Result<HttpResponse> {
|
||||||
if payload.password != payload.password_confirmation {
|
if payload.password != payload.password_confirmation {
|
||||||
@ -57,11 +49,13 @@ pub async fn create_account(
|
|||||||
routes::admin::Error::DifferentPasswords,
|
routes::admin::Error::DifferentPasswords,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let hash = match payload.password.encrypt(&config.lock().web().pass_salt()) {
|
let hash = {
|
||||||
Ok(hash) => hash,
|
match payload.password.encrypt(&config.lock().web().pass_salt()) {
|
||||||
Err(e) => {
|
Ok(hash) => hash,
|
||||||
log::error!("{e:?}");
|
Err(e) => {
|
||||||
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
|
log::error!("{e:?}");
|
||||||
|
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,63 +70,78 @@ pub async fn create_account(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
pub(crate) struct AuthPair {
|
||||||
pub struct SignInInput {
|
pub access_token: Token,
|
||||||
pub login: String,
|
pub access_token_string: AccessTokenString,
|
||||||
pub password: String,
|
pub _refresh_token: Token,
|
||||||
|
pub refresh_token_string: RefreshTokenString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
pub(crate) async fn create_auth_pair(
|
||||||
pub struct SignInOutput {
|
|
||||||
pub token: TokenString,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/sign-in")]
|
|
||||||
async fn sign_in(
|
|
||||||
Json(payload): Json<SignInInput>,
|
|
||||||
db: Data<Addr<Database>>,
|
|
||||||
tm: Data<Addr<TokenManager>>,
|
tm: Data<Addr<TokenManager>>,
|
||||||
) -> Result<HttpResponse> {
|
account: FullAccount,
|
||||||
let db = db.into_inner();
|
) -> routes::Result<AuthPair> {
|
||||||
let tm = tm.into_inner();
|
let (access_token, refresh_token) = query_tm!(
|
||||||
|
multi,
|
||||||
let account: FullAccount = query_db!(
|
tm,
|
||||||
db,
|
|
||||||
database_manager::AccountByIdentity {
|
|
||||||
login: Some(Login::from(payload.login)),
|
|
||||||
email: None,
|
|
||||||
},
|
|
||||||
routes::Error::Public(PublicError::DatabaseConnection),
|
routes::Error::Public(PublicError::DatabaseConnection),
|
||||||
routes::Error::Public(PublicError::DatabaseConnection)
|
token_manager::CreateToken {
|
||||||
);
|
|
||||||
if Password::from(payload.password)
|
|
||||||
.validate(&account.pass_hash)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Err(routes::Error::Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_token, string): (Token, TokenString) = match tm
|
|
||||||
.send(token_manager::CreateToken {
|
|
||||||
customer_id: account.customer_id,
|
customer_id: account.customer_id,
|
||||||
role: account.role,
|
role: account.role,
|
||||||
subject: account.id,
|
subject: account.id,
|
||||||
audience: Some(Audience::Web),
|
audience: Some(Audience::Web),
|
||||||
})
|
exp: None
|
||||||
.await
|
},
|
||||||
{
|
token_manager::CreateToken {
|
||||||
Ok(Ok(token)) => token,
|
customer_id: account.customer_id,
|
||||||
Ok(Err(token_err)) => {
|
role: account.role,
|
||||||
log::error!("{token_err}");
|
subject: account.id,
|
||||||
return Err(routes::Error::Public(PublicError::DatabaseConnection));
|
audience: Some(Audience::Web),
|
||||||
|
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
|
||||||
}
|
}
|
||||||
Err(db_err) => {
|
);
|
||||||
log::error!("{db_err}");
|
let (access_token, access_token_string): (Token, AccessTokenString) = access_token?;
|
||||||
return Err(routes::Error::Public(PublicError::DatabaseConnection));
|
let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?;
|
||||||
}
|
Ok(AuthPair {
|
||||||
};
|
access_token,
|
||||||
|
access_token_string,
|
||||||
|
_refresh_token: refresh_token,
|
||||||
|
refresh_token_string: refresh_token_string.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Created().json(SignInOutput { token: string }))
|
#[post("/sign-in")]
|
||||||
|
async fn sign_in(
|
||||||
|
Json(payload): Json<api::SignInInput>,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
tm: Data<Addr<TokenManager>>,
|
||||||
|
) -> Result<Json<api::SignInOutput>> {
|
||||||
|
let db = db.into_inner();
|
||||||
|
|
||||||
|
let account: FullAccount = query_db!(
|
||||||
|
db,
|
||||||
|
database_manager::AccountByIdentity {
|
||||||
|
login: Some(payload.login),
|
||||||
|
email: None,
|
||||||
|
},
|
||||||
|
routes::Error::Public(PublicError::DatabaseConnection)
|
||||||
|
);
|
||||||
|
if payload.password.validate(&account.pass_hash).is_err() {
|
||||||
|
return Err(routes::Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let AuthPair {
|
||||||
|
access_token,
|
||||||
|
access_token_string,
|
||||||
|
_refresh_token: _,
|
||||||
|
refresh_token_string,
|
||||||
|
} = create_auth_pair(tm, account).await?;
|
||||||
|
|
||||||
|
Ok(Json(api::SignInOutput {
|
||||||
|
access_token: access_token_string,
|
||||||
|
refresh_token: refresh_token_string,
|
||||||
|
exp: access_token.expiration_time,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/payment/notify")]
|
#[post("/payment/notify")]
|
||||||
@ -145,7 +154,7 @@ async fn handle_notification(
|
|||||||
notification: notify,
|
notification: notify,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HttpResponse::Ok().body("")
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
#[cfg(feature = "dummy")]
|
#[cfg(feature = "dummy")]
|
||||||
use fake::Fake;
|
use fake::Fake;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::ProductLinkedPhoto;
|
use crate::*;
|
||||||
|
|
||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@ -75,20 +76,40 @@ pub struct AccountOrder {
|
|||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ShoppingCartItem {
|
pub struct ShoppingCartItem {
|
||||||
pub id: crate::ShoppingCartId,
|
pub id: ShoppingCartId,
|
||||||
pub product_id: crate::ProductId,
|
pub product_id: ProductId,
|
||||||
pub shopping_cart_id: crate::ShoppingCartId,
|
pub shopping_cart_id: ShoppingCartId,
|
||||||
pub quantity: crate::Quantity,
|
pub quantity: Quantity,
|
||||||
pub quantity_unit: crate::QuantityUnit,
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::ShoppingCartItem> for ShoppingCartItem {
|
||||||
|
fn from(
|
||||||
|
crate::ShoppingCartItem {
|
||||||
|
id,
|
||||||
|
product_id,
|
||||||
|
shopping_cart_id,
|
||||||
|
quantity,
|
||||||
|
quantity_unit,
|
||||||
|
}: crate::ShoppingCartItem,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
product_id,
|
||||||
|
shopping_cart_id,
|
||||||
|
quantity,
|
||||||
|
quantity_unit,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ShoppingCart {
|
pub struct ShoppingCart {
|
||||||
pub id: crate::ShoppingCartId,
|
pub id: ShoppingCartId,
|
||||||
pub buyer_id: crate::AccountId,
|
pub buyer_id: AccountId,
|
||||||
pub payment_method: crate::PaymentMethod,
|
pub payment_method: PaymentMethod,
|
||||||
pub state: crate::ShoppingCartState,
|
pub state: ShoppingCartState,
|
||||||
pub items: Vec<ShoppingCartItem>,
|
pub items: Vec<ShoppingCartItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,3 +236,103 @@ impl From<(Vec<crate::Product>, Vec<ProductLinkedPhoto>, String)> for Products {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SignInInput {
|
||||||
|
pub login: Login,
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SignInOutput {
|
||||||
|
pub access_token: AccessTokenString,
|
||||||
|
pub refresh_token: RefreshTokenString,
|
||||||
|
pub exp: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateAccountInput {
|
||||||
|
pub email: Email,
|
||||||
|
pub login: Login,
|
||||||
|
pub password: Password,
|
||||||
|
pub password_confirmation: PasswordConfirmation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateOrderInput {
|
||||||
|
/// Required customer e-mail
|
||||||
|
pub email: String,
|
||||||
|
/// Required customer phone number
|
||||||
|
pub phone: String,
|
||||||
|
/// Required customer first name
|
||||||
|
pub first_name: String,
|
||||||
|
/// Required customer last name
|
||||||
|
pub last_name: String,
|
||||||
|
/// Required customer language
|
||||||
|
pub language: String,
|
||||||
|
/// False if customer is allowed to be charged on site.
|
||||||
|
/// Otherwise it should be true to use payment service for charging
|
||||||
|
pub charge_client: bool,
|
||||||
|
/// User currency
|
||||||
|
pub currency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DeleteItemInput {
|
||||||
|
pub shopping_cart_item_id: ShoppingCartItemId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DeleteItemOutput {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateItemInput {
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateItemOutput {
|
||||||
|
pub success: bool,
|
||||||
|
pub shopping_cart_item: ShoppingCartItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod admin {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct RegisterInput {
|
||||||
|
pub login: Login,
|
||||||
|
pub email: Email,
|
||||||
|
pub password: Password,
|
||||||
|
pub password_confirmation: PasswordConfirmation,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
pub struct RegisterResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub errors: Vec<RegisterError>,
|
||||||
|
pub account: Option<Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum RegisterError {
|
||||||
|
PasswordDiffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SignInInput {
|
||||||
|
pub login: Option<Login>,
|
||||||
|
pub email: Option<Email>,
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LogoutResponse {}
|
||||||
|
}
|
||||||
|
@ -494,7 +494,7 @@ pub struct FullAccount {
|
|||||||
|
|
||||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
|
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub id: AccountId,
|
pub id: AccountId,
|
||||||
pub email: Email,
|
pub email: Email,
|
||||||
@ -744,10 +744,34 @@ pub struct Token {
|
|||||||
#[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, Display, From)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
|
||||||
pub struct TokenString(String);
|
pub struct AccessTokenString(String);
|
||||||
|
|
||||||
impl TokenString {
|
impl AccessTokenString {
|
||||||
|
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||||
|
Self(s.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RefreshTokenString> for AccessTokenString {
|
||||||
|
fn from(r: RefreshTokenString) -> Self {
|
||||||
|
Self(r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||||
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
|
||||||
|
pub struct RefreshTokenString(String);
|
||||||
|
|
||||||
|
impl From<AccessTokenString> for RefreshTokenString {
|
||||||
|
fn from(r: AccessTokenString) -> Self {
|
||||||
|
Self(r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RefreshTokenString {
|
||||||
pub fn new<S: Into<String>>(s: S) -> Self {
|
pub fn new<S: Into<String>>(s: S) -> Self {
|
||||||
Self(s.into())
|
Self(s.into())
|
||||||
}
|
}
|
||||||
|
BIN
web/assets/logo.png
Normal file
BIN
web/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -3,10 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link id="logo" data-trunk rel="icon" href="assets/logo.png" >
|
||||||
<title>Bazzar</title>
|
<title>Bazzar</title>
|
||||||
<link data-trunk rel="css" href="tmp/tailwind.css"/>
|
<link data-trunk rel="css" href="tmp/tailwind.css"/>
|
||||||
|
<link rel="copy-file" href="assets/logo.png">
|
||||||
|
<link rel="copy-file" href="tmp/tailwind.css">
|
||||||
<base data-trunk-public-url/>
|
<base data-trunk-public-url/>
|
||||||
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
/* http://meyerweb.com/eric/tools/css/reset/
|
|
||||||
v2.0 | 20110126
|
|
||||||
License: none (public domain)
|
|
||||||
*/
|
|
||||||
|
|
||||||
html, body, div, span, applet, object, iframe,
|
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
|
||||||
a, abbr, acronym, address, big, cite, code,
|
|
||||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
|
||||||
small, strike, strong, sub, sup, tt, var,
|
|
||||||
b, u, i, center,
|
|
||||||
dl, dt, dd, ol, ul, li,
|
|
||||||
fieldset, form, label, legend,
|
|
||||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
|
||||||
article, aside, canvas, details, embed,
|
|
||||||
figure, figcaption, footer, header, hgroup,
|
|
||||||
menu, nav, output, ruby, section, summary,
|
|
||||||
time, mark, audio, video {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
font-size: 100%;
|
|
||||||
font: inherit;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HTML5 display-role reset for older browsers */
|
|
||||||
article, aside, details, figcaption, figure,
|
|
||||||
footer, header, hgroup, menu, nav, section {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol, ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote, q {
|
|
||||||
quotes: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote:before, blockquote:after,
|
|
||||||
q:before, q:after {
|
|
||||||
content: '';
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
use model::{AccessTokenString, RefreshTokenString};
|
||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
|
|
||||||
pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
||||||
@ -9,3 +10,49 @@ pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
|||||||
.json()
|
.json()
|
||||||
.await
|
.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 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 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 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
|
||||||
|
}
|
||||||
|
@ -24,18 +24,43 @@ 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.stream(streams::interval(500, || Msg::CheckAccessToken));
|
||||||
|
|
||||||
Model {
|
Model {
|
||||||
token: LocalStorage::get("auth-token").ok(),
|
token: LocalStorage::get("auth-token").ok(),
|
||||||
page: Page::Public(PublicPage::Listing(pages::public::listing::init(
|
page: Page::Public(PublicPage::Listing(pages::public::listing::init(
|
||||||
url,
|
url,
|
||||||
&mut orders.proxy(proxy_public_listing),
|
&mut orders.proxy(proxy_public_listing),
|
||||||
))),
|
))),
|
||||||
|
logo: seed::document()
|
||||||
|
.query_selector("link[rel=icon]")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|el: web_sys::Element| el.get_attribute("href")),
|
||||||
|
shared: shared::Model::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
|
Msg::Shared(msg) => {
|
||||||
|
shared::update(msg, &mut model.shared, orders);
|
||||||
|
}
|
||||||
|
Msg::CheckAccessToken => {
|
||||||
|
orders.skip();
|
||||||
|
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.as_ref().cloned() {
|
||||||
|
orders.send_msg(Msg::Shared(shared::Msg::RefreshToken(token)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Msg::UrlChanged(subs::UrlChanged(url)) => model.page = Page::init(url, orders),
|
Msg::UrlChanged(subs::UrlChanged(url)) => model.page = Page::init(url, orders),
|
||||||
|
Msg::Public(pages::public::Msg::Listing(pages::public::listing::Msg::Shared(msg))) => {
|
||||||
|
shared::update(msg, &mut model.shared, 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, ());
|
||||||
pages::public::listing::update(msg, page, &mut orders.proxy(proxy_public_listing));
|
pages::public::listing::update(msg, page, &mut orders.proxy(proxy_public_listing));
|
||||||
@ -45,7 +70,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
|
|
||||||
fn view(model: &Model) -> Node<Msg> {
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
match &model.page {
|
match &model.page {
|
||||||
Page::Public(PublicPage::Listing(page)) => pages::public::listing::view(&page)
|
Page::Public(PublicPage::Listing(page)) => pages::public::listing::view(model, page)
|
||||||
.map_msg(|msg| Msg::Public(pages::public::Msg::Listing(msg))),
|
.map_msg(|msg| Msg::Public(pages::public::Msg::Listing(msg))),
|
||||||
_ => empty![],
|
_ => empty![],
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,6 @@ use crate::Page;
|
|||||||
pub struct Model {
|
pub struct Model {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub page: Page,
|
pub page: Page,
|
||||||
|
pub logo: Option<String>,
|
||||||
|
pub shared: crate::shared::Model,
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ pub mod public;
|
|||||||
use seed::app::{subs, Orders};
|
use seed::app::{subs, Orders};
|
||||||
use seed::{struct_urls, Url};
|
use seed::{struct_urls, Url};
|
||||||
|
|
||||||
|
use crate::shared;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Public(public::Msg),
|
Public(public::Msg),
|
||||||
UrlChanged(subs::UrlChanged),
|
UrlChanged(subs::UrlChanged),
|
||||||
|
CheckAccessToken,
|
||||||
|
Shared(shared::Msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AdminPage {
|
pub enum AdminPage {
|
||||||
|
@ -12,6 +12,7 @@ pub struct Model {
|
|||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
FetchProducts,
|
FetchProducts,
|
||||||
ProductFetched(fetch::Result<model::api::Products>),
|
ProductFetched(fetch::Result<model::api::Products>),
|
||||||
|
Shared(crate::shared::Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||||
@ -35,14 +36,15 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::ProductFetched(Err(_e)) => {
|
Msg::ProductFetched(Err(_e)) => {
|
||||||
model.errors.push("Failed to load products".into());
|
model.errors.push("Failed to load products".into());
|
||||||
}
|
}
|
||||||
|
Msg::Shared(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &crate::Model, page: &Model) -> Node<Msg> {
|
||||||
let products = model.products.iter().map(product);
|
let products = page.products.iter().map(product);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
crate::shared::public_navbar(),
|
crate::shared::view::public_navbar(model),
|
||||||
div![
|
div![
|
||||||
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
||||||
products
|
products
|
||||||
|
@ -1,35 +1,64 @@
|
|||||||
use seed::prelude::*;
|
use seed::app::Orders;
|
||||||
use seed::*;
|
|
||||||
|
|
||||||
pub fn public_navbar<Msg>() -> Node<Msg> {
|
pub use crate::shared::msg::Msg;
|
||||||
header![
|
|
||||||
C!["sticky top-0 z-30 w-full px-2 py-4 bg-white sm:px-4 shadow-xl"],
|
pub mod msg;
|
||||||
div![
|
pub mod view;
|
||||||
C!["flex items-center justify-between mx-auto max-w-7xl"],
|
|
||||||
logo(),
|
#[derive(Debug, Default)]
|
||||||
div![
|
pub struct Model {
|
||||||
C!["flex items-center space-x-1"],
|
pub access_token: Option<model::AccessTokenString>,
|
||||||
ul![
|
pub refresh_token: Option<model::RefreshTokenString>,
|
||||||
C!["hidden space-x-2 md:inline-flex"],
|
pub exp: Option<chrono::NaiveDateTime>,
|
||||||
navbar_item("Home", "/"),
|
pub me: Option<model::Account>,
|
||||||
navbar_item("Sign In", "/sign-in"),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navbar_item<Msg>(name: &str, path: &str) -> Node<Msg> {
|
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<crate::Msg>) {
|
||||||
li![a![
|
match msg {
|
||||||
attrs!["href"=>path],
|
Msg::LoadMe => {
|
||||||
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
if let Some(token) = model.access_token.as_ref().cloned() {
|
||||||
name
|
orders.skip().perform_cmd(async {
|
||||||
]]
|
Msg::MeLoaded(crate::api::public::fetch_me(token).await)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::MeLoaded(Ok(account)) => {
|
||||||
|
model.me = Some(account);
|
||||||
|
}
|
||||||
|
Msg::MeLoaded(Err(_err)) => {}
|
||||||
|
Msg::SignIn(input) => {
|
||||||
|
orders
|
||||||
|
.skip()
|
||||||
|
.perform_cmd(async { Msg::SignedIn(crate::api::public::sign_in(input).await) });
|
||||||
|
}
|
||||||
|
Msg::SignedIn(Ok(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::TokenRefreshed(Ok(pair)) => {
|
||||||
|
handle_auth_pair(pair, model, orders);
|
||||||
|
}
|
||||||
|
Msg::TokenRefreshed(Err(_err)) => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logo<Msg>() -> Node<Msg> {
|
fn handle_auth_pair(
|
||||||
a![
|
pair: model::api::SignInOutput,
|
||||||
attrs!["a" => "#"],
|
model: &mut Model,
|
||||||
span![C!["text-2xl font-extrabold text-blue-600"], "Logo"]
|
_orders: &mut impl Orders<crate::Msg>,
|
||||||
]
|
) {
|
||||||
|
let model::api::SignInOutput {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
exp,
|
||||||
|
} = pair;
|
||||||
|
|
||||||
|
model.access_token = Some(access_token);
|
||||||
|
model.refresh_token = Some(refresh_token);
|
||||||
|
model.exp = Some(exp);
|
||||||
}
|
}
|
||||||
|
11
web/src/shared/msg.rs
Normal file
11
web/src/shared/msg.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use seed::fetch::Result;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Msg {
|
||||||
|
LoadMe,
|
||||||
|
MeLoaded(Result<model::Account>),
|
||||||
|
SignIn(model::api::SignInInput),
|
||||||
|
SignedIn(Result<model::api::SignInOutput>),
|
||||||
|
RefreshToken(model::RefreshTokenString),
|
||||||
|
TokenRefreshed(Result<model::api::SignInOutput>),
|
||||||
|
}
|
75
web/src/shared/view.rs
Normal file
75
web/src/shared/view.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use seed::prelude::*;
|
||||||
|
use seed::*;
|
||||||
|
|
||||||
|
pub fn public_navbar<Msg>(model: &crate::Model) -> Node<Msg> {
|
||||||
|
header![
|
||||||
|
C!["container flex justify-around py-8 mx-auto bg-white"],
|
||||||
|
div![C!["flex items-center"], logo(model),],
|
||||||
|
div![
|
||||||
|
C!["items-center hidden space-x-8 lg:flex"],
|
||||||
|
navbar_item(div![C![""], "Home"], "/"),
|
||||||
|
],
|
||||||
|
div![
|
||||||
|
C!["flex items-center space-x-2"],
|
||||||
|
navbar_item(account(), "/sign-in"),
|
||||||
|
navbar_item(bag(), "/cart")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navbar_item<Msg>(name: Node<Msg>, path: &str) -> Node<Msg> {
|
||||||
|
a![
|
||||||
|
attrs!["href" => path],
|
||||||
|
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
||||||
|
name
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logo<Msg>(model: &crate::Model) -> Node<Msg> {
|
||||||
|
a![
|
||||||
|
attrs!["href" => "/"],
|
||||||
|
match model.logo.as_deref() {
|
||||||
|
Some(url) => img![
|
||||||
|
C!["text-2xl font-extrabold text-blue-600"],
|
||||||
|
attrs!["alt" => "logo", "src" => url, "height" => "32", "style" => "height: 64px;"]
|
||||||
|
],
|
||||||
|
_ => span![C!["text-2xl font-extrabold text-blue-600"], "logo"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bag<Msg>() -> Node<Msg> {
|
||||||
|
svg![
|
||||||
|
attrs![
|
||||||
|
"width" => "32px",
|
||||||
|
"height" => "32px",
|
||||||
|
"viewBox" => "0 0 32 32",
|
||||||
|
"xmlns" => "http://www.w3.org/2000/svg",
|
||||||
|
"class"=>"w-6 h-6",
|
||||||
|
"fill" => "none",
|
||||||
|
"stroke" => "currentColor",
|
||||||
|
"stroke-linecap" => "round",
|
||||||
|
"stroke-linejoin" => "round",
|
||||||
|
"stroke-width" => "2"
|
||||||
|
],
|
||||||
|
path![attrs!["d" => "M5 9 L5 29 27 29 27 9 Z M10 9 C10 9 10 3 16 3 22 3 22 9 22 9"]]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn account<Msg>() -> Node<Msg> {
|
||||||
|
svg![
|
||||||
|
attrs![
|
||||||
|
"xmlns" => "http://www.w3.org/2000/svg",
|
||||||
|
"class" => "w-6 h-6",
|
||||||
|
"fill" => "none",
|
||||||
|
"viewBox" => "0 0 24 24",
|
||||||
|
"stroke" => "currentColor",
|
||||||
|
],
|
||||||
|
path![attrs![
|
||||||
|
"stroke-linecap" => "round",
|
||||||
|
"stroke-linejoin" => "round",
|
||||||
|
"stroke-width" => "2",
|
||||||
|
"d" => "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
]],
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user