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)]
|
||||
pub enum Error {
|
||||
#[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#"
|
||||
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
||||
FROM tokens
|
||||
WHERE jwt_id = $1
|
||||
WHERE jwt_id = $1 AND expiration_time > now()
|
||||
"#)
|
||||
.bind(msg.jti)
|
||||
.fetch_one(&pool)
|
||||
@ -69,3 +69,41 @@ RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not
|
||||
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) => {
|
||||
$crate::query_db!($fs, $msg, $fail, $fail)
|
||||
$crate::query_fs!($fs, $msg, $fail, $fail)
|
||||
};
|
||||
|
||||
($fs: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
|
||||
|
@ -7,7 +7,7 @@ use config::SharedAppConfig;
|
||||
use database_manager::{query_db, Database};
|
||||
use hmac::digest::KeyInit;
|
||||
use hmac::Hmac;
|
||||
use model::{AccountId, Audience, Role, Token, TokenString};
|
||||
use model::{AccessTokenString, AccountId, Audience, Role, Token};
|
||||
use sha2::Sha256;
|
||||
|
||||
#[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 {
|
||||
/// cti (customer id): Customer uuid identifier used by payment service
|
||||
pub cti: uuid::Uuid,
|
||||
@ -80,30 +136,33 @@ impl TokenManager {
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(Token, TokenString)>")]
|
||||
#[rtype(result = "Result<(Token, AccessTokenString)>")]
|
||||
pub struct CreateToken {
|
||||
pub customer_id: uuid::Uuid,
|
||||
pub role: Role,
|
||||
pub subject: AccountId,
|
||||
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(
|
||||
msg: CreateToken,
|
||||
db: Addr<Database>,
|
||||
config: SharedAppConfig,
|
||||
) -> Result<(Token, TokenString)> {
|
||||
) -> Result<(Token, AccessTokenString)> {
|
||||
let CreateToken {
|
||||
customer_id,
|
||||
role,
|
||||
subject,
|
||||
audience,
|
||||
exp,
|
||||
} = msg;
|
||||
let audience = audience.unwrap_or_default();
|
||||
|
||||
let token: Token = query_db!(
|
||||
let token: Token = match exp {
|
||||
None => query_db!(
|
||||
db,
|
||||
database_manager::CreateToken {
|
||||
customer_id,
|
||||
@ -113,7 +172,20 @@ pub(crate) async fn create_token(
|
||||
},
|
||||
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 = {
|
||||
use jwt::SignWithKey;
|
||||
@ -169,7 +241,7 @@ pub(crate) async fn create_token(
|
||||
return Err(Error::SaveInternal);
|
||||
}
|
||||
};
|
||||
TokenString::from(s)
|
||||
AccessTokenString::new(s)
|
||||
};
|
||||
Ok((token, token_string))
|
||||
}
|
||||
@ -177,7 +249,7 @@ pub(crate) async fn create_token(
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(Token, bool)>")]
|
||||
pub struct Validate {
|
||||
pub token: TokenString,
|
||||
pub token: AccessTokenString,
|
||||
}
|
||||
|
||||
token_async_handler!(Validate, validate, (Token, bool));
|
||||
@ -214,6 +286,10 @@ pub(crate) async fn validate(
|
||||
Error::ValidateInternal
|
||||
);
|
||||
|
||||
if token.expiration_time < Utc::now().naive_utc() {
|
||||
return Err(Error::Validate);
|
||||
}
|
||||
|
||||
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
|
||||
return Ok((token, false));
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||
use actix_web::{delete, get, post, HttpResponse};
|
||||
use config::SharedAppConfig;
|
||||
use database_manager::{query_db, Database};
|
||||
use model::{Account, Email, Encrypt, Login, PassHash, Password, PasswordConfirmation, Role};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use model::{Encrypt, PassHash};
|
||||
|
||||
use crate::routes;
|
||||
use crate::routes::{RequireLogin, Result};
|
||||
@ -43,29 +42,19 @@ pub enum Error {
|
||||
Database(#[from] database_manager::Error),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LogoutResponse {}
|
||||
|
||||
#[delete("logout")]
|
||||
async fn logout(session: Session) -> Result<HttpResponse> {
|
||||
async fn logout(session: Session) -> Result<Json<model::api::admin::LogoutResponse>> {
|
||||
session.require_admin()?;
|
||||
session.clear();
|
||||
|
||||
Ok(HttpResponse::NotImplemented().body(""))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SignInInput {
|
||||
login: Option<Login>,
|
||||
email: Option<Email>,
|
||||
password: Password,
|
||||
Ok(Json(model::api::admin::LogoutResponse {}))
|
||||
}
|
||||
|
||||
#[post("/sign-in")]
|
||||
async fn sign_in(
|
||||
session: Session,
|
||||
db: Data<Addr<Database>>,
|
||||
Json(payload): Json<SignInInput>,
|
||||
Json(payload): Json<model::api::admin::SignInInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
log::debug!("{:?}", payload);
|
||||
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
|
||||
#[post("/register")]
|
||||
async fn register(
|
||||
session: Session,
|
||||
Json(input): Json<RegisterInput>,
|
||||
Json(input): Json<model::api::admin::RegisterInput>,
|
||||
db: Data<Addr<Database>>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut response = RegisterResponse::default();
|
||||
let mut response = model::api::admin::RegisterResponse::default();
|
||||
session.require_admin()?;
|
||||
|
||||
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()) {
|
||||
|
@ -9,9 +9,9 @@ use actix_session::Session;
|
||||
use actix_web::body::BoxBody;
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
|
||||
use model::{RecordId, Token, TokenString};
|
||||
use model::{AccessTokenString, RecordId, Token};
|
||||
use serde::Serialize;
|
||||
use token_manager::TokenManager;
|
||||
use token_manager::{query_tm, TokenManager};
|
||||
|
||||
pub use self::admin::Error as AdminError;
|
||||
pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
|
||||
@ -132,15 +132,12 @@ pub trait RequireUser {
|
||||
#[async_trait::async_trait]
|
||||
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
|
||||
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)> {
|
||||
match tm
|
||||
.send(token_manager::Validate {
|
||||
token: TokenString::from(String::from(self.token())),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(res)) => Ok(res),
|
||||
Ok(Err(_e)) => Err(Error::Unauthorized),
|
||||
Err(_) => Err(Error::Unauthorized),
|
||||
}
|
||||
Ok(query_tm!(
|
||||
tm,
|
||||
token_manager::Validate {
|
||||
token: AccessTokenString::new(self.token()),
|
||||
},
|
||||
Error::Unauthorized
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,60 @@ use actix::Addr;
|
||||
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||
use actix_web::{delete, get, post, HttpRequest, HttpResponse};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use cart_manager::CartManager;
|
||||
use cart_manager::{query_cart, CartManager};
|
||||
use database_manager::{query_db, Database};
|
||||
use model::{api, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartItemId};
|
||||
use model::api;
|
||||
use payment_manager::{query_pay, PaymentManager};
|
||||
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::Error as PublicError;
|
||||
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")]
|
||||
async fn shopping_cart(
|
||||
@ -23,7 +67,7 @@ async fn shopping_cart(
|
||||
let cart: model::ShoppingCart = query_db!(
|
||||
db,
|
||||
database_manager::EnsureActiveShoppingCart {
|
||||
buyer_id: AccountId::from(token.subject),
|
||||
buyer_id: token.subject.into(),
|
||||
},
|
||||
routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart(
|
||||
ShoppingCartError::Ensure
|
||||
@ -43,60 +87,30 @@ async fn shopping_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")]
|
||||
async fn create_cart_item(
|
||||
cart: Data<Addr<CartManager>>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
credentials: BearerAuth,
|
||||
Json(payload): Json<CreateItemInput>,
|
||||
) -> Result<Json<CreateItemOutput>> {
|
||||
Json(payload): Json<api::CreateItemInput>,
|
||||
) -> Result<Json<api::CreateItemOutput>> {
|
||||
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
||||
|
||||
match cart
|
||||
.send(cart_manager::AddItem {
|
||||
buyer_id: AccountId::from(token.subject),
|
||||
let item: model::ShoppingCartItem = query_cart!(
|
||||
cart,
|
||||
cart_manager::AddItem {
|
||||
buyer_id: token.subject.into(),
|
||||
product_id: payload.product_id,
|
||||
quantity: payload.quantity,
|
||||
quantity_unit: payload.quantity_unit,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(item)) => Ok(Json(CreateItemOutput {
|
||||
},
|
||||
routes::Error::Public(super::Error::AddItem.into()),
|
||||
routes::Error::Public(PublicError::DatabaseConnection)
|
||||
);
|
||||
Ok(Json(api::CreateItemOutput {
|
||||
success: true,
|
||||
shopping_cart_item: item,
|
||||
})),
|
||||
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,
|
||||
shopping_cart_item: item.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[delete("/shopping-cart-item")]
|
||||
@ -105,14 +119,14 @@ async fn delete_cart_item(
|
||||
cart: Data<Addr<CartManager>>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
credentials: BearerAuth,
|
||||
Json(payload): Json<DeleteItemInput>,
|
||||
Json(payload): Json<api::DeleteItemInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (token, _) = credentials.require_user(tm.into_inner()).await?;
|
||||
|
||||
let sc: model::ShoppingCart = query_db!(
|
||||
db,
|
||||
database_manager::EnsureActiveShoppingCart {
|
||||
buyer_id: AccountId::from(token.subject),
|
||||
buyer_id: token.subject.into(),
|
||||
},
|
||||
routes::Error::Public(super::Error::RemoveItem.into()),
|
||||
routes::Error::Public(PublicError::DatabaseConnection)
|
||||
@ -126,10 +140,10 @@ async fn delete_cart_item(
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Ok(HttpResponse::Ok().json(DeleteItemOutput { success: true })),
|
||||
Ok(Ok(_)) => Ok(HttpResponse::Ok().json(api::DeleteItemOutput { success: true })),
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{e}");
|
||||
Ok(HttpResponse::BadRequest().json(DeleteItemOutput { success: false }))
|
||||
Ok(HttpResponse::BadRequest().json(api::DeleteItemOutput { success: false }))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
@ -138,51 +152,34 @@ async fn delete_cart_item(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
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,
|
||||
#[get("/me")]
|
||||
pub(crate) async fn me(
|
||||
db: Data<Addr<Database>>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
credentials: BearerAuth,
|
||||
) -> routes::Result<Json<model::Account>> {
|
||||
let account_id: model::AccountId = credentials
|
||||
.require_user(tm.into_inner())
|
||||
.await?
|
||||
.0
|
||||
.subject
|
||||
.into();
|
||||
let account: model::FullAccount =
|
||||
public_send_db!(owned, db, database_manager::FindAccount { account_id });
|
||||
Ok(Json(account.into()))
|
||||
}
|
||||
|
||||
#[post("/order")]
|
||||
pub(crate) async fn create_order(
|
||||
req: HttpRequest,
|
||||
Json(payload): Json<CreateOrderInput>,
|
||||
Json(payload): Json<api::CreateOrderInput>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
credentials: BearerAuth,
|
||||
payment: Data<Addr<PaymentManager>>,
|
||||
) -> routes::Result<HttpResponse> {
|
||||
let (
|
||||
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 subject = credentials.require_user(tm.into_inner()).await?.0.subject;
|
||||
|
||||
let buyer_id = model::AccountId::from(subject);
|
||||
let CreateOrderInput {
|
||||
let api::CreateOrderInput {
|
||||
email,
|
||||
phone,
|
||||
first_name,
|
||||
@ -208,7 +205,7 @@ pub(crate) async fn create_order(
|
||||
language,
|
||||
},
|
||||
customer_ip: ip.to_string(),
|
||||
buyer_id,
|
||||
buyer_id: subject.into(),
|
||||
charge_client
|
||||
},
|
||||
routes::Error::Public(PublicError::DatabaseConnection)
|
||||
@ -222,11 +219,15 @@ pub(crate) async fn create_order(
|
||||
}
|
||||
|
||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||
config.service(scope("")
|
||||
let scoped = scope("")
|
||||
.app_data(actix_web_httpauth::extractors::bearer::Config::default()
|
||||
.realm("user api")
|
||||
.scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
|
||||
.service(shopping_cart)
|
||||
.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 config::SharedAppConfig;
|
||||
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 token_manager::TokenManager;
|
||||
use token_manager::{query_tm, TokenManager};
|
||||
|
||||
use crate::public_send_db;
|
||||
use crate::routes::public::Error as PublicError;
|
||||
use crate::routes::{self, Result};
|
||||
use crate::{public_send_db, Login, Password};
|
||||
|
||||
#[get("/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)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateAccountInput {
|
||||
pub email: model::Email,
|
||||
pub login: Login,
|
||||
pub password: Password,
|
||||
pub password_confirmation: model::PasswordConfirmation,
|
||||
}
|
||||
|
||||
#[post("/register")]
|
||||
pub async fn create_account(
|
||||
db: Data<Addr<Database>>,
|
||||
Json(payload): Json<CreateAccountInput>,
|
||||
Json(payload): Json<api::CreateAccountInput>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> routes::Result<HttpResponse> {
|
||||
if payload.password != payload.password_confirmation {
|
||||
@ -57,12 +49,14 @@ pub async fn create_account(
|
||||
routes::admin::Error::DifferentPasswords,
|
||||
));
|
||||
}
|
||||
let hash = match payload.password.encrypt(&config.lock().web().pass_salt()) {
|
||||
let hash = {
|
||||
match payload.password.encrypt(&config.lock().web().pass_salt()) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public_send_db!(
|
||||
@ -76,63 +70,78 @@ pub async fn create_account(
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SignInInput {
|
||||
pub login: String,
|
||||
pub password: String,
|
||||
pub(crate) struct AuthPair {
|
||||
pub access_token: Token,
|
||||
pub access_token_string: AccessTokenString,
|
||||
pub _refresh_token: Token,
|
||||
pub refresh_token_string: RefreshTokenString,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct SignInOutput {
|
||||
pub token: TokenString,
|
||||
}
|
||||
|
||||
#[post("/sign-in")]
|
||||
async fn sign_in(
|
||||
Json(payload): Json<SignInInput>,
|
||||
db: Data<Addr<Database>>,
|
||||
pub(crate) async fn create_auth_pair(
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let db = db.into_inner();
|
||||
let tm = tm.into_inner();
|
||||
|
||||
let account: FullAccount = query_db!(
|
||||
db,
|
||||
database_manager::AccountByIdentity {
|
||||
login: Some(Login::from(payload.login)),
|
||||
email: None,
|
||||
},
|
||||
account: FullAccount,
|
||||
) -> routes::Result<AuthPair> {
|
||||
let (access_token, refresh_token) = query_tm!(
|
||||
multi,
|
||||
tm,
|
||||
routes::Error::Public(PublicError::DatabaseConnection),
|
||||
routes::Error::Public(PublicError::DatabaseConnection)
|
||||
);
|
||||
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 {
|
||||
token_manager::CreateToken {
|
||||
customer_id: account.customer_id,
|
||||
role: account.role,
|
||||
subject: account.id,
|
||||
audience: Some(Audience::Web),
|
||||
exp: None
|
||||
},
|
||||
token_manager::CreateToken {
|
||||
customer_id: account.customer_id,
|
||||
role: account.role,
|
||||
subject: account.id,
|
||||
audience: Some(Audience::Web),
|
||||
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
|
||||
}
|
||||
);
|
||||
let (access_token, access_token_string): (Token, AccessTokenString) = access_token?;
|
||||
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(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(token)) => token,
|
||||
Ok(Err(token_err)) => {
|
||||
log::error!("{token_err}");
|
||||
return Err(routes::Error::Public(PublicError::DatabaseConnection));
|
||||
}
|
||||
Err(db_err) => {
|
||||
log::error!("{db_err}");
|
||||
return Err(routes::Error::Public(PublicError::DatabaseConnection));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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")]
|
||||
@ -145,7 +154,7 @@ async fn handle_notification(
|
||||
notification: notify,
|
||||
});
|
||||
}
|
||||
HttpResponse::Ok().body("")
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||
|
@ -1,9 +1,10 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use derive_more::Deref;
|
||||
#[cfg(feature = "dummy")]
|
||||
use fake::Fake;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ProductLinkedPhoto;
|
||||
use crate::*;
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -75,20 +76,40 @@ pub struct AccountOrder {
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ShoppingCartItem {
|
||||
pub id: crate::ShoppingCartId,
|
||||
pub product_id: crate::ProductId,
|
||||
pub shopping_cart_id: crate::ShoppingCartId,
|
||||
pub quantity: crate::Quantity,
|
||||
pub quantity_unit: crate::QuantityUnit,
|
||||
pub id: ShoppingCartId,
|
||||
pub product_id: ProductId,
|
||||
pub shopping_cart_id: ShoppingCartId,
|
||||
pub quantity: Quantity,
|
||||
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))]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ShoppingCart {
|
||||
pub id: crate::ShoppingCartId,
|
||||
pub buyer_id: crate::AccountId,
|
||||
pub payment_method: crate::PaymentMethod,
|
||||
pub state: crate::ShoppingCartState,
|
||||
pub id: ShoppingCartId,
|
||||
pub buyer_id: AccountId,
|
||||
pub payment_method: PaymentMethod,
|
||||
pub state: ShoppingCartState,
|
||||
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 = "db", derive(sqlx::FromRow))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Account {
|
||||
pub id: AccountId,
|
||||
pub email: Email,
|
||||
@ -744,10 +744,34 @@ pub struct Token {
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
|
||||
pub struct TokenString(String);
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
|
||||
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 {
|
||||
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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link id="logo" data-trunk rel="icon" href="assets/logo.png" >
|
||||
<title>Bazzar</title>
|
||||
<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/>
|
||||
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<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::*;
|
||||
|
||||
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()
|
||||
.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 {
|
||||
orders.stream(streams::interval(500, || Msg::CheckAccessToken));
|
||||
|
||||
Model {
|
||||
token: LocalStorage::get("auth-token").ok(),
|
||||
page: Page::Public(PublicPage::Listing(pages::public::listing::init(
|
||||
url,
|
||||
&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>) {
|
||||
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::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)) => {
|
||||
let page = fetch_page!(public model, 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> {
|
||||
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))),
|
||||
_ => empty![],
|
||||
}
|
||||
|
@ -3,4 +3,6 @@ use crate::Page;
|
||||
pub struct Model {
|
||||
pub token: Option<String>,
|
||||
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::{struct_urls, Url};
|
||||
|
||||
use crate::shared;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Public(public::Msg),
|
||||
UrlChanged(subs::UrlChanged),
|
||||
CheckAccessToken,
|
||||
Shared(shared::Msg),
|
||||
}
|
||||
|
||||
pub enum AdminPage {
|
||||
|
@ -12,6 +12,7 @@ pub struct Model {
|
||||
pub enum Msg {
|
||||
FetchProducts,
|
||||
ProductFetched(fetch::Result<model::api::Products>),
|
||||
Shared(crate::shared::Msg)
|
||||
}
|
||||
|
||||
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)) => {
|
||||
model.errors.push("Failed to load products".into());
|
||||
}
|
||||
Msg::Shared(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let products = model.products.iter().map(product);
|
||||
pub fn view(model: &crate::Model, page: &Model) -> Node<Msg> {
|
||||
let products = page.products.iter().map(product);
|
||||
|
||||
div![
|
||||
crate::shared::public_navbar(),
|
||||
crate::shared::view::public_navbar(model),
|
||||
div![
|
||||
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
||||
products
|
||||
|
@ -1,35 +1,64 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::app::Orders;
|
||||
|
||||
pub fn public_navbar<Msg>() -> Node<Msg> {
|
||||
header![
|
||||
C!["sticky top-0 z-30 w-full px-2 py-4 bg-white sm:px-4 shadow-xl"],
|
||||
div![
|
||||
C!["flex items-center justify-between mx-auto max-w-7xl"],
|
||||
logo(),
|
||||
div![
|
||||
C!["flex items-center space-x-1"],
|
||||
ul![
|
||||
C!["hidden space-x-2 md:inline-flex"],
|
||||
navbar_item("Home", "/"),
|
||||
navbar_item("Sign In", "/sign-in"),
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
pub use crate::shared::msg::Msg;
|
||||
|
||||
pub mod msg;
|
||||
pub mod view;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Model {
|
||||
pub access_token: Option<model::AccessTokenString>,
|
||||
pub refresh_token: Option<model::RefreshTokenString>,
|
||||
pub exp: Option<chrono::NaiveDateTime>,
|
||||
pub me: Option<model::Account>,
|
||||
}
|
||||
|
||||
fn navbar_item<Msg>(name: &str, path: &str) -> Node<Msg> {
|
||||
li![a![
|
||||
attrs!["href"=>path],
|
||||
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
||||
name
|
||||
]]
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<crate::Msg>) {
|
||||
match msg {
|
||||
Msg::LoadMe => {
|
||||
if let Some(token) = model.access_token.as_ref().cloned() {
|
||||
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> {
|
||||
a![
|
||||
attrs!["a" => "#"],
|
||||
span![C!["text-2xl font-extrabold text-blue-600"], "Logo"]
|
||||
]
|
||||
fn handle_auth_pair(
|
||||
pair: model::api::SignInOutput,
|
||||
model: &mut Model,
|
||||
_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