From da218adcbda97eebaa489cbbc052acfe1dc2f8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Tue, 10 May 2022 16:20:37 +0200 Subject: [PATCH] Refresh tokens, sign in and so on --- actors/cart_manager/src/lib.rs | 35 ++++ actors/database_manager/src/tokens.rs | 40 +++- actors/fs_manager/src/lib.rs | 2 +- actors/token_manager/src/lib.rs | 110 +++++++++-- api/src/routes/admin.rs | 48 +---- api/src/routes/mod.rs | 21 +-- api/src/routes/public/api_v1/restricted.rs | 181 ++++++++++--------- api/src/routes/public/api_v1/unrestricted.rs | 139 +++++++------- shared/model/src/api.rs | 141 ++++++++++++++- shared/model/src/lib.rs | 32 +++- web/assets/logo.png | Bin 0 -> 16639 bytes web/index.html | 4 +- web/public/index.css | 54 ------ web/src/api/public.rs | 47 +++++ web/src/lib.rs | 27 ++- web/src/model.rs | 2 + web/src/pages.rs | 4 + web/src/pages/public/listing.rs | 8 +- web/src/shared.rs | 87 ++++++--- web/src/shared/msg.rs | 11 ++ web/src/shared/view.rs | 75 ++++++++ 21 files changed, 741 insertions(+), 327 deletions(-) create mode 100644 web/assets/logo.png delete mode 100644 web/public/index.css create mode 100644 web/src/shared/msg.rs create mode 100644 web/src/shared/view.rs diff --git a/actors/cart_manager/src/lib.rs b/actors/cart_manager/src/lib.rs index e4e9ed4..d3f71cc 100644 --- a/actors/cart_manager/src/lib.rs +++ b/actors/cart_manager/src/lib.rs @@ -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")] diff --git a/actors/database_manager/src/tokens.rs b/actors/database_manager/src/tokens.rs index 6d29aba..e15ac98 100644 --- a/actors/database_manager/src/tokens.rs +++ b/actors/database_manager/src/tokens.rs @@ -24,7 +24,7 @@ pub(crate) async fn token_by_jti(msg: TokenByJti, pool: PgPool) -> Result 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")] +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 { + 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) + }) +} diff --git a/actors/fs_manager/src/lib.rs b/actors/fs_manager/src/lib.rs index 8e336e1..3ec9d08 100644 --- a/actors/fs_manager/src/lib.rs +++ b/actors/fs_manager/src/lib.rs @@ -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) => { diff --git a/actors/token_manager/src/lib.rs b/actors/token_manager/src/lib.rs index 7e5b6ab..e9cf762 100644 --- a/actors/token_manager/src/lib.rs +++ b/actors/token_manager/src/lib.rs @@ -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,40 +136,56 @@ 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, + pub exp: Option, } -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, 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!( - db, - database_manager::CreateToken { - customer_id, - role, - subject, - audience, - }, - Error::Save, - Error::SaveInternal - ); + let token: Token = match exp { + None => query_db!( + db, + database_manager::CreateToken { + customer_id, + role, + subject, + audience, + }, + 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)); } diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 86c1933..0936be5 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -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 { +async fn logout(session: Session) -> Result> { session.require_admin()?; session.clear(); - Ok(HttpResponse::NotImplemented().body("")) -} - -#[derive(Deserialize, Debug)] -pub struct SignInInput { - login: Option, - email: Option, - password: Password, + Ok(Json(model::api::admin::LogoutResponse {})) } #[post("/sign-in")] async fn sign_in( session: Session, db: Data>, - Json(payload): Json, + Json(payload): Json, ) -> Result { 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, - pub account: Option, -} - -#[derive(Serialize)] -pub enum RegisterError { - PasswordDiffer, -} - // login_required #[post("/register")] async fn register( session: Session, - Json(input): Json, + Json(input): Json, db: Data>, config: Data, ) -> Result { - 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()) { diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index ff28d19..8cea125 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -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>) -> 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 + )) } } diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index eaf7680..8ff1da6 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -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>, + credentials: BearerAuth, +) -> routes::Result { + 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>, + db: Data>, + credentials: BearerAuth, +) -> routes::Result> { + 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>, tm: Data>, credentials: BearerAuth, - Json(payload): Json, -) -> Result> { + Json(payload): Json, +) -> Result> { 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 { - 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, + }, + routes::Error::Public(super::Error::AddItem.into()), + routes::Error::Public(PublicError::DatabaseConnection) + ); + Ok(Json(api::CreateItemOutput { + success: true, + shopping_cart_item: item.into(), + })) } #[delete("/shopping-cart-item")] @@ -105,14 +119,14 @@ async fn delete_cart_item( cart: Data>, tm: Data>, credentials: BearerAuth, - Json(payload): Json, + Json(payload): Json, ) -> Result { 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>, + tm: Data>, + credentials: BearerAuth, +) -> routes::Result> { + 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, + Json(payload): Json, tm: Data>, credentials: BearerAuth, payment: Data>, ) -> routes::Result { - 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); } diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index b57978f..6d5a222 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -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>) -> Result { 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>, - Json(payload): Json, + Json(payload): Json, config: Data, ) -> routes::Result { if payload.password != payload.password_confirmation { @@ -57,11 +49,13 @@ pub async fn create_account( routes::admin::Error::DifferentPasswords, )); } - 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)); + 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)); + } } }; @@ -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, - db: Data>, +pub(crate) async fn create_auth_pair( tm: Data>, -) -> Result { - 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 { + 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), - }) - .await - { - Ok(Ok(token)) => token, - Ok(Err(token_err)) => { - log::error!("{token_err}"); - return Err(routes::Error::Public(PublicError::DatabaseConnection)); + 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()) } - Err(db_err) => { - log::error!("{db_err}"); - return Err(routes::Error::Public(PublicError::DatabaseConnection)); - } - }; + ); + 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(), + }) +} - Ok(HttpResponse::Created().json(SignInOutput { token: string })) +#[post("/sign-in")] +async fn sign_in( + Json(payload): Json, + db: Data>, + tm: Data>, +) -> Result> { + 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) { diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index 11651e7..8a315cc 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -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 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, } @@ -215,3 +236,103 @@ impl From<(Vec, Vec, 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, + pub account: Option, + } + + #[derive(Serialize, Deserialize, Debug)] + pub enum RegisterError { + PasswordDiffer, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct SignInInput { + pub login: Option, + pub email: Option, + pub password: Password, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct LogoutResponse {} +} diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 3c33300..f03f345 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -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: S) -> Self { + Self(s.into()) + } +} + +impl From 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 for RefreshTokenString { + fn from(r: AccessTokenString) -> Self { + Self(r.0) + } +} + +impl RefreshTokenString { pub fn new>(s: S) -> Self { Self(s.into()) } diff --git a/web/assets/logo.png b/web/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8fef885cee7ac38bb447daca3d29f1bf47de3f43 GIT binary patch literal 16639 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 L6>z1LS@8b=#8H{{ literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index 9a59519..d5480cb 100644 --- a/web/index.html +++ b/web/index.html @@ -3,10 +3,12 @@ + Bazzar + + -
diff --git a/web/public/index.css b/web/public/index.css deleted file mode 100644 index 71365e9..0000000 --- a/web/public/index.css +++ /dev/null @@ -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; -} diff --git a/web/src/api/public.rs b/web/src/api/public.rs index 00a7895..b3f43f2 100644 --- a/web/src/api/public.rs +++ b/web/src/api/public.rs @@ -1,3 +1,4 @@ +use model::{AccessTokenString, RefreshTokenString}; use seed::prelude::*; pub async fn fetch_products() -> fetch::Result { @@ -9,3 +10,49 @@ pub async fn fetch_products() -> fetch::Result { .json() .await } + +pub async fn fetch_me(access_token: AccessTokenString) -> fetch::Result { + 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 { + 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 { + 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 { + Request::new("/api/v1/token/refresh") + .method(Method::Post) + .header(fetch::Header::bearer(access_token.as_str())) + .fetch() + .await? + .check_status()? + .json() + .await +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 2b98479..f8b40ea 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -24,18 +24,43 @@ macro_rules! fetch_page { } fn init(url: Url, orders: &mut impl Orders) -> 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) { 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) { fn view(model: &Model) -> Node { 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![], } diff --git a/web/src/model.rs b/web/src/model.rs index 8492e28..84003e6 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -3,4 +3,6 @@ use crate::Page; pub struct Model { pub token: Option, pub page: Page, + pub logo: Option, + pub shared: crate::shared::Model, } diff --git a/web/src/pages.rs b/web/src/pages.rs index 6b52daa..a2d516d 100644 --- a/web/src/pages.rs +++ b/web/src/pages.rs @@ -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 { diff --git a/web/src/pages/public/listing.rs b/web/src/pages/public/listing.rs index 55c0923..6930814 100644 --- a/web/src/pages/public/listing.rs +++ b/web/src/pages/public/listing.rs @@ -12,6 +12,7 @@ pub struct Model { pub enum Msg { FetchProducts, ProductFetched(fetch::Result), + Shared(crate::shared::Msg) } pub fn init(_url: Url, orders: &mut impl Orders) -> Model { @@ -35,14 +36,15 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { Msg::ProductFetched(Err(_e)) => { model.errors.push("Failed to load products".into()); } + Msg::Shared(_) => {} } } -pub fn view(model: &Model) -> Node { - let products = model.products.iter().map(product); +pub fn view(model: &crate::Model, page: &Model) -> Node { + 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 diff --git a/web/src/shared.rs b/web/src/shared.rs index 4db1001..76d3f52 100644 --- a/web/src/shared.rs +++ b/web/src/shared.rs @@ -1,35 +1,64 @@ -use seed::prelude::*; -use seed::*; +use seed::app::Orders; -pub fn public_navbar() -> Node { - 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, + pub refresh_token: Option, + pub exp: Option, + pub me: Option, } -fn navbar_item(name: &str, path: &str) -> Node { - 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) { + 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() -> Node { - 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, +) { + 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); } diff --git a/web/src/shared/msg.rs b/web/src/shared/msg.rs new file mode 100644 index 0000000..11cead6 --- /dev/null +++ b/web/src/shared/msg.rs @@ -0,0 +1,11 @@ +use seed::fetch::Result; + +#[derive(Debug)] +pub enum Msg { + LoadMe, + MeLoaded(Result), + SignIn(model::api::SignInInput), + SignedIn(Result), + RefreshToken(model::RefreshTokenString), + TokenRefreshed(Result), +} diff --git a/web/src/shared/view.rs b/web/src/shared/view.rs new file mode 100644 index 0000000..4d464e6 --- /dev/null +++ b/web/src/shared/view.rs @@ -0,0 +1,75 @@ +use seed::prelude::*; +use seed::*; + +pub fn public_navbar(model: &crate::Model) -> Node { + 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(name: Node, path: &str) -> Node { + a![ + attrs!["href" => path], + C!["px-4 py-2 font-semibold text-gray-600 rounded"], + name + ] +} + +fn logo(model: &crate::Model) -> Node { + 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() -> Node { + 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() -> Node { + 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" + ]], + ] +}