From f0d01a973525ba141406f7396138cdfbc71ae822 Mon Sep 17 00:00:00 2001 From: eraden Date: Tue, 17 May 2022 08:23:39 +0200 Subject: [PATCH] Migrate admin endpoint to JWT --- Cargo.toml | 4 + actors/token_manager/src/lib.rs | 26 +++--- api/src/routes/admin.rs | 95 +------------------- api/src/routes/admin/api_v1.rs | 63 ++++++++++++- api/src/routes/admin/api_v1/accounts.rs | 53 +++++------ api/src/routes/admin/api_v1/orders.rs | 13 ++- api/src/routes/admin/api_v1/products.rs | 25 +++--- api/src/routes/admin/api_v1/stocks.rs | 38 ++++---- api/src/routes/admin/api_v1/uploads.rs | 12 ++- api/src/routes/mod.rs | 77 +++++++++++++--- api/src/routes/public/api_v1/restricted.rs | 34 +++---- api/src/routes/public/api_v1/unrestricted.rs | 73 ++++----------- shared/model/src/api.rs | 2 +- shared/model/src/lib.rs | 12 +++ web/Cargo.toml | 3 - web/src/api.rs | 7 ++ web/src/api/admin.rs | 35 ++++++++ 17 files changed, 325 insertions(+), 247 deletions(-) create mode 100644 web/src/api/admin.rs diff --git a/Cargo.toml b/Cargo.toml index 6ac2308..57dc606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,7 @@ members = [ "actors/fs_manager", "db-seed" ] + +[profile.release] +lto = true +opt-level = 's' diff --git a/actors/token_manager/src/lib.rs b/actors/token_manager/src/lib.rs index e9cf762..618924b 100644 --- a/actors/token_manager/src/lib.rs +++ b/actors/token_manager/src/lib.rs @@ -116,6 +116,8 @@ pub enum Error { Validate, #[error("Unable to validate token. Can't connect to database")] ValidateInternal, + #[error("Token does not exists or some fields are incorrect")] + Invalid, } pub type Result = std::result::Result; @@ -247,18 +249,18 @@ pub(crate) async fn create_token( } #[derive(Message)] -#[rtype(result = "Result<(Token, bool)>")] +#[rtype(result = "Result")] pub struct Validate { pub token: AccessTokenString, } -token_async_handler!(Validate, validate, (Token, bool)); +token_async_handler!(Validate, validate, Token); pub(crate) async fn validate( msg: Validate, db: Addr, config: SharedAppConfig, -) -> Result<(Token, bool)> { +) -> Result { use jwt::VerifyWithKey; log::info!("Validating token {:?}", msg.token); @@ -291,32 +293,32 @@ pub(crate) async fn validate( } if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "arl", token.role, |left, right| right == left) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "sub", token.subject, validate_num) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "nbt", &token.not_before_time, validate_time) { - return Ok((token, false)); + return Err(Error::Invalid); } if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) { - return Ok((token, false)); + return Err(Error::Invalid); } log::info!("JWT token valid"); - Ok((token, true)) + Ok(token) } fn build_key(secret: String) -> Result> { diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 0936be5..5d72fba 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -1,15 +1,9 @@ mod api_v1; -use actix::Addr; -use actix_session::Session; -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::{Encrypt, PassHash}; +use actix_web::web::{scope, ServiceConfig}; +use actix_web::{get, HttpResponse}; -use crate::routes; -use crate::routes::{RequireLogin, Result}; +use crate::routes::Result; #[macro_export] macro_rules! admin_send_db { @@ -42,86 +36,6 @@ pub enum Error { Database(#[from] database_manager::Error), } -#[delete("logout")] -async fn logout(session: Session) -> Result> { - session.require_admin()?; - session.clear(); - - Ok(Json(model::api::admin::LogoutResponse {})) -} - -#[post("/sign-in")] -async fn sign_in( - session: Session, - db: Data>, - Json(payload): Json, -) -> Result { - log::debug!("{:?}", payload); - let db = db.into_inner(); - let user: model::FullAccount = query_db!( - db, - database_manager::AccountByIdentity { - email: payload.email, - login: payload.login, - }, - routes::Error::Unauthorized - ); - if let Err(e) = payload.password.validate(&user.pass_hash) { - log::error!("Password validation failed. {}", e); - Err(routes::Error::Unauthorized) - } else { - if let Err(e) = session.insert("admin_id", *user.id) { - log::error!("{:?}", e); - } - Ok(HttpResponse::Ok().json(model::Account::from(user))) - } -} - -// login_required -#[post("/register")] -async fn register( - session: Session, - Json(input): Json, - db: Data>, - config: Data, -) -> Result { - let mut response = model::api::admin::RegisterResponse::default(); - session.require_admin()?; - - if input.password != input.password_confirmation { - response - .errors - .push(model::api::admin::RegisterError::PasswordDiffer); - } - - let hash = match input.password.encrypt(&config.lock().web().pass_salt()) { - Ok(s) => s, - Err(e) => { - log::error!("{e:?}"); - return Err(routes::Error::Admin(Error::HashPass)); - } - }; - - query_db!( - db, - database_manager::CreateAccount { - email: input.email, - login: input.login, - pass_hash: PassHash::from(hash), - role: input.role, - }, - super::Error::Admin(Error::Register), - super::Error::Admin(Error::Register) - ); - - response.success = response.errors.is_empty(); - Ok(if response.success { - HttpResponse::Ok().json(response) - } else { - HttpResponse::BadRequest().json(response) - }) -} - #[get("")] async fn landing() -> Result { Ok(HttpResponse::NotImplemented() @@ -132,9 +46,6 @@ async fn landing() -> Result { pub fn configure(config: &mut ServiceConfig) { config.service( scope("/admin") - .service(sign_in) - .service(logout) - .service(register) .service(landing) .configure(api_v1::configure), ); diff --git a/api/src/routes/admin/api_v1.rs b/api/src/routes/admin/api_v1.rs index 2d0d882..1943c7f 100644 --- a/api/src/routes/admin/api_v1.rs +++ b/api/src/routes/admin/api_v1.rs @@ -4,7 +4,64 @@ mod products; mod stocks; mod uploads; -use actix_web::web::{scope, ServiceConfig}; +use actix::Addr; +use actix_web::web::{scope, Data, Json, ServiceConfig}; +use actix_web::{delete, post}; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use database_manager::{query_db, Database}; +use model::Encrypt; +use token_manager::TokenManager; + +use crate::routes; +use crate::routes::{create_auth_pair, AdminError, AuthPair, RequireUser}; + +#[delete("/logout")] +async fn logout( + credentials: BearerAuth, + tm: Data>, +) -> routes::Result> { + credentials.require_admin(tm.into_inner()).await?; + + todo!("Destroy token") + // Ok(Json(model::api::admin::LogoutResponse {})) +} + +#[post("/sign-in")] +async fn sign_in( + tm: Data>, + db: Data>, + Json(payload): Json, +) -> routes::Result> { + let db = db.into_inner(); + + let account: model::FullAccount = query_db!( + db, + database_manager::AccountByIdentity { + login: payload.login, + email: payload.email, + }, + routes::Error::Admin(AdminError::DatabaseConnection) + ); + if payload.password.validate(&account.pass_hash).is_err() { + return Err(routes::Error::Unauthorized); + } + + let role = account.role; + + let AuthPair { + access_token, + access_token_string, + _refresh_token: _, + refresh_token_string, + } = create_auth_pair(tm, account).await?; + + Ok(Json(model::api::SessionOutput { + access_token: access_token_string, + refresh_token: refresh_token_string, + exp: access_token.expiration_time, + role, + })) +} pub fn configure(config: &mut ServiceConfig) { config.service( @@ -13,6 +70,8 @@ pub fn configure(config: &mut ServiceConfig) { .configure(stocks::configure) .configure(accounts::configure) .configure(orders::configure) - .configure(uploads::configure), + .configure(uploads::configure) + .service(sign_in) + .service(logout), ); } diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs index 22d60d9..54154a4 100644 --- a/api/src/routes/admin/api_v1/accounts.rs +++ b/api/src/routes/admin/api_v1/accounts.rs @@ -1,18 +1,24 @@ use actix::Addr; -use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{get, patch, post, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use config::SharedAppConfig; use database_manager::Database; use model::{AccountId, AccountState, Encrypt, PasswordConfirmation}; +use token_manager::TokenManager; use crate::routes::admin::Error; -use crate::routes::RequireLogin; +use crate::routes::RequireUser; use crate::{admin_send_db, routes, Email, Login, PassHash, Password, Role}; #[get("/accounts")] -pub async fn accounts(session: Session, db: Data>) -> routes::Result { - session.require_admin()?; +pub async fn accounts( + credentials: BearerAuth, + tm: Data>, + db: Data>, +) -> routes::Result { + credentials.require_admin(tm.into_inner()).await?; + let accounts = admin_send_db!(db, database_manager::AllAccounts); Ok(HttpResponse::Ok().json(accounts)) } @@ -30,12 +36,13 @@ pub struct UpdateAccountInput { #[patch("/account")] pub async fn update_account( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, config: Data, ) -> routes::Result { - session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let hash = match (payload.password, payload.password_confirmation) { (None, None) => None, @@ -52,7 +59,7 @@ pub async fn update_account( return Err(routes::Error::Admin(routes::admin::Error::HashPass)); } }; - Some(PassHash::from(hash)) + Some(PassHash::new(hash)) } _ => { return Err(routes::Error::Admin( @@ -75,37 +82,28 @@ pub async fn update_account( Ok(HttpResponse::Ok().json(account)) } -#[derive(serde::Deserialize)] -pub struct CreateAccountInput { - pub email: Email, - pub login: Login, - pub password: Password, - pub password_confirmation: PasswordConfirmation, - pub role: Role, -} - #[post("/account")] pub async fn create_account( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, - Json(payload): Json, + Json(payload): Json, config: Data, -) -> routes::Result { - session.require_admin()?; +) -> routes::Result> { + credentials.require_admin(tm.into_inner()).await?; + if payload.password != payload.password_confirmation { - return Err(routes::Error::Admin( - routes::admin::Error::DifferentPasswords, - )); + return Err(routes::Error::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)); + return Err(routes::Error::Admin(Error::HashPass)); } }; - let account = admin_send_db!( + let account: model::FullAccount = admin_send_db!( db, database_manager::CreateAccount { email: payload.email, @@ -114,7 +112,10 @@ pub async fn create_account( role: payload.role, } ); - Ok(HttpResponse::Ok().json(account)) + Ok(Json(model::api::admin::RegisterResponse { + errors: vec![], + account: Some(account.into()), + })) } pub fn configure(config: &mut ServiceConfig) { diff --git a/api/src/routes/admin/api_v1/orders.rs b/api/src/routes/admin/api_v1/orders.rs index fd95368..a8e8d73 100644 --- a/api/src/routes/admin/api_v1/orders.rs +++ b/api/src/routes/admin/api_v1/orders.rs @@ -1,17 +1,22 @@ use actix::Addr; -use actix_session::Session; use actix_web::get; use actix_web::web::{Data, Json, ServiceConfig}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use database_manager::Database; use model::api::AccountOrders; +use token_manager::TokenManager; use crate::routes::admin::Error; -use crate::routes::RequireLogin; +use crate::routes::RequireUser; use crate::{admin_send_db, routes}; #[get("/orders")] -async fn orders(session: Session, db: Data>) -> routes::Result> { - session.require_admin()?; +async fn orders( + credentials: BearerAuth, + tm: Data>, + db: Data>, +) -> routes::Result> { + credentials.require_admin(tm.into_inner()).await?; let orders: Vec = admin_send_db!(&db, database_manager::AllAccountOrders); let items: Vec = admin_send_db!(db, database_manager::AllOrderItems); diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs index b2e88e7..6cfbe0d 100644 --- a/api/src/routes/admin/api_v1/products.rs +++ b/api/src/routes/admin/api_v1/products.rs @@ -1,7 +1,7 @@ use actix::Addr; -use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{delete, get, patch, post, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use config::SharedAppConfig; use database_manager::Database; use model::{ @@ -10,18 +10,20 @@ use model::{ }; use search_manager::SearchManager; use serde::Deserialize; +use token_manager::TokenManager; use crate::routes::admin::Error; -use crate::routes::RequireLogin; +use crate::routes::RequireUser; use crate::{admin_send_db, routes}; #[get("/products")] async fn products( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, config: Data, ) -> routes::Result> { - session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let public_path = { let l = config.lock(); @@ -55,11 +57,12 @@ pub struct UpdateProduct { #[patch("/product")] async fn update_product( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, ) -> routes::Result { - session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let product = admin_send_db!( db, @@ -89,12 +92,13 @@ pub struct CreateProduct { #[post("/product")] async fn create_product( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, search: Data>, Json(payload): Json, ) -> routes::Result { - session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let product: model::Product = admin_send_db!( db.clone(), @@ -138,11 +142,12 @@ pub struct DeleteProduct { #[delete("/product")] async fn delete_product( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, ) -> routes::Result { - let _ = session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let product = admin_send_db!( db, diff --git a/api/src/routes/admin/api_v1/stocks.rs b/api/src/routes/admin/api_v1/stocks.rs index 22f9b56..819613b 100644 --- a/api/src/routes/admin/api_v1/stocks.rs +++ b/api/src/routes/admin/api_v1/stocks.rs @@ -1,21 +1,26 @@ use actix::Addr; -use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{delete, get, patch, post, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use database_manager::Database; use model::{ProductId, Quantity, QuantityUnit, StockId}; use serde::Deserialize; +use token_manager::TokenManager; use crate::routes::admin::Error; -use crate::routes::RequireLogin; +use crate::routes::RequireUser; use crate::{admin_send_db, routes}; #[get("/stocks")] -async fn stocks(session: Session, db: Data>) -> routes::Result { - session.require_admin()?; +async fn stocks( + credentials: BearerAuth, + tm: Data>, + db: Data>, +) -> routes::Result>> { + credentials.require_admin(tm.into_inner()).await?; let stocks = admin_send_db!(db, database_manager::AllStocks); - Ok(HttpResponse::Created().json(stocks)) + Ok(Json(stocks)) } #[derive(Deserialize)] @@ -28,11 +33,12 @@ pub struct UpdateStock { #[patch("/stock")] async fn update_stock( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, -) -> routes::Result { - session.require_admin()?; +) -> routes::Result> { + credentials.require_admin(tm.into_inner()).await?; let stock = admin_send_db!( db, @@ -43,7 +49,7 @@ async fn update_stock( quantity_unit: payload.quantity_unit } ); - Ok(HttpResponse::Created().json(stock)) + Ok(Json(stock)) } #[derive(Deserialize)] @@ -55,11 +61,12 @@ pub struct CreateStock { #[post("/stock")] async fn create_stock( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, ) -> routes::Result { - session.require_admin()?; + credentials.require_admin(tm.into_inner()).await?; let stock = admin_send_db!( db, @@ -79,11 +86,12 @@ pub struct DeleteStock { #[delete("/stock")] async fn delete_stock( - session: Session, + credentials: BearerAuth, + tm: Data>, db: Data>, Json(payload): Json, -) -> routes::Result { - session.require_admin()?; +) -> routes::Result>> { + credentials.require_admin(tm.into_inner()).await?; let stock = admin_send_db!( db, @@ -91,7 +99,7 @@ async fn delete_stock( stock_id: payload.id } ); - Ok(HttpResponse::Created().json(stock)) + Ok(Json(stock)) } pub fn configure(config: &mut ServiceConfig) { diff --git a/api/src/routes/admin/api_v1/uploads.rs b/api/src/routes/admin/api_v1/uploads.rs index f388aad..6734eb9 100644 --- a/api/src/routes/admin/api_v1/uploads.rs +++ b/api/src/routes/admin/api_v1/uploads.rs @@ -2,9 +2,13 @@ use actix::Addr; use actix_multipart::Multipart; use actix_web::web::{Data, ServiceConfig}; use actix_web::{post, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use database_manager::{query_db, Database}; use fs_manager::FsManager; use futures_util::StreamExt; +use token_manager::TokenManager; + +use crate::routes::RequireUser; #[derive(Debug, thiserror::Error)] pub enum UploadError { @@ -21,7 +25,13 @@ async fn upload_product_image( mut payload: Multipart, fs: Data>, db: Data>, + credentials: BearerAuth, + tm: Data>, ) -> HttpResponse { + if credentials.require_admin(tm.into_inner()).await.is_err() { + return HttpResponse::Unauthorized().finish(); + } + let mut name = None; while let Some(Ok(mut field)) = payload.next().await { let field_name = field.name(); @@ -106,7 +116,7 @@ async fn upload_product_image( _ => {} } } - HttpResponse::NotImplemented().finish() + HttpResponse::Ok().finish() } pub fn configure(config: &mut ServiceConfig) { diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index 1f287c9..4509051 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -8,26 +8,24 @@ use actix::Addr; use actix_session::Session; use actix_web::body::BoxBody; use actix_web::http::StatusCode; -use actix_web::web::ServiceConfig; +use actix_web::web::{Data, ServiceConfig}; use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError}; use model::api::Failure; -use model::{AccessTokenString, RecordId, Token}; use token_manager::{query_tm, TokenManager}; pub use self::admin::Error as AdminError; pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError}; -use crate::routes; pub trait RequireLogin { - fn require_admin(&self) -> Result; + fn require_admin(&self) -> Result; } impl RequireLogin for Session { - fn require_admin(&self) -> Result { + fn require_admin(&self) -> Result { match self.get("admin_id") { Ok(Some(id)) => Ok(id), _ => { - log::debug!("User is not logged as admin"); + log::debug!("User is not logged as an admin"); Err(Error::Unauthorized) } } @@ -39,8 +37,8 @@ pub enum Error { #[from(ignore)] Unauthorized, CriticalFailure, - Admin(routes::admin::Error), - Public(routes::public::Error), + Admin(admin::Error), + Public(public::Error), } impl From for Error { @@ -132,18 +130,75 @@ pub fn configure(config: &mut ServiceConfig) { #[async_trait::async_trait] pub trait RequireUser { - async fn require_user(&self, tm: Arc>) -> Result<(Token, bool)>; + async fn require_user(&self, tm: Arc>) -> Result; + async fn require_admin(&self, tm: Arc>) -> Result; } #[async_trait::async_trait] impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth { - async fn require_user(&self, tm: Arc>) -> Result<(Token, bool)> { + async fn require_user(&self, tm: Arc>) -> Result { Ok(query_tm!( tm, token_manager::Validate { - token: AccessTokenString::new(self.token()), + token: model::AccessTokenString::new(self.token()), }, Error::Unauthorized )) } + async fn require_admin(&self, tm: Arc>) -> Result { + let token: model::Token = query_tm!( + tm, + token_manager::Validate { + token: model::AccessTokenString::new(self.token()), + }, + Error::Unauthorized + ); + if token.role == model::Role::Admin { + Err(Error::Unauthorized) + } else { + Ok(token) + } + } +} + +pub struct AuthPair { + pub access_token: model::Token, + pub access_token_string: model::AccessTokenString, + pub _refresh_token: model::Token, + pub refresh_token_string: model::RefreshTokenString, +} + +pub async fn create_auth_pair( + tm: Data>, + account: model::FullAccount, +) -> Result { + let (access_token, refresh_token) = query_tm!( + multi, + tm, + Error::Public(PublicError::DatabaseConnection), + token_manager::CreateToken { + customer_id: account.customer_id, + role: account.role, + subject: account.id, + audience: Some(model::Audience::Web), + exp: None + }, + token_manager::CreateToken { + customer_id: account.customer_id, + role: account.role, + subject: account.id, + audience: Some(model::Audience::Web), + exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()) + } + ); + let (access_token, access_token_string): (model::Token, model::AccessTokenString) = + access_token?; + let (refresh_token, refresh_token_string): (model::Token, model::AccessTokenString) = + refresh_token?; + Ok(AuthPair { + access_token, + access_token_string, + _refresh_token: refresh_token, + refresh_token_string: refresh_token_string.into(), + }) } diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index cfd2a5b..f9277b1 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -8,10 +8,9 @@ use model::api; use payment_manager::{query_pay, PaymentManager}; use token_manager::TokenManager; -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::routes::{create_auth_pair, AuthPair, RequireUser, Result}; use crate::{public_send_db, routes}; /// This requires [model::AccessTokenString] to be set as bearer @@ -20,7 +19,7 @@ async fn verify_token( tm: Data>, credentials: BearerAuth, ) -> routes::Result { - let _token = credentials.require_user(tm.into_inner()).await?.0; + let _token = credentials.require_user(tm.into_inner()).await?; Ok("".into()) } @@ -34,7 +33,6 @@ async fn refresh_token( let account_id: model::AccountId = credentials .require_user(tm.clone().into_inner()) .await? - .0 .subject .into(); let account: model::FullAccount = query_db!( @@ -43,6 +41,8 @@ async fn refresh_token( routes::Error::Unauthorized ); + let role = account.role; + let AuthPair { access_token, access_token_string, @@ -50,10 +50,11 @@ async fn refresh_token( refresh_token_string, } = create_auth_pair(tm, account).await?; - Ok(Json(api::SessionOutput { + Ok(Json(model::api::SessionOutput { access_token: access_token_string, refresh_token: refresh_token_string, exp: access_token.expiration_time, + role, })) } @@ -63,11 +64,11 @@ async fn shopping_cart( tm: Data>, credentials: BearerAuth, ) -> Result> { - let (token, _) = credentials.require_user(tm.into_inner()).await?; + let token = credentials.require_user(tm.into_inner()).await?; let cart: model::ShoppingCart = query_db!( db, database_manager::EnsureActiveShoppingCart { - buyer_id: token.subject.into(), + buyer_id: token.account_id(), }, routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart( ShoppingCartError::Ensure @@ -94,12 +95,12 @@ async fn create_cart_item( credentials: BearerAuth, Json(payload): Json, ) -> Result> { - let (token, _) = credentials.require_user(tm.into_inner()).await?; + let token = credentials.require_user(tm.into_inner()).await?; let item: model::ShoppingCartItem = query_cart!( cart, cart_manager::AddItem { - buyer_id: token.subject.into(), + buyer_id: token.account_id(), product_id: payload.product_id, quantity: payload.quantity, quantity_unit: payload.quantity_unit, @@ -121,12 +122,12 @@ async fn delete_cart_item( credentials: BearerAuth, Json(payload): Json, ) -> Result { - let (token, _) = credentials.require_user(tm.into_inner()).await?; + let token = credentials.require_user(tm.into_inner()).await?; let sc: model::ShoppingCart = query_db!( db, database_manager::EnsureActiveShoppingCart { - buyer_id: token.subject.into(), + buyer_id: token.account_id(), }, routes::Error::Public(super::Error::RemoveItem.into()), routes::Error::Public(PublicError::DatabaseConnection) @@ -161,9 +162,7 @@ pub(crate) async fn me( let account_id: model::AccountId = credentials .require_user(tm.into_inner()) .await? - .0 - .subject - .into(); + .account_id(); let account: model::FullAccount = public_send_db!(owned, db, database_manager::FindAccount { account_id }); Ok(Json(account.into())) @@ -177,7 +176,10 @@ pub(crate) async fn create_order( credentials: BearerAuth, payment: Data>, ) -> routes::Result { - let subject = credentials.require_user(tm.into_inner()).await?.0.subject; + let account_id = credentials + .require_user(tm.into_inner()) + .await? + .account_id(); let api::CreateOrderInput { email, @@ -205,7 +207,7 @@ pub(crate) async fn create_order( language, }, customer_ip: ip.to_string(), - buyer_id: subject.into(), + buyer_id: account_id, charge_client }, routes::Error::Public(PublicError::DatabaseConnection) diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index 8c26ff7..f08382e 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -3,14 +3,14 @@ use actix_web::web::{Data, Json, Path, Query, ServiceConfig}; use actix_web::{get, post, HttpResponse}; use config::SharedAppConfig; use database_manager::{query_db, Database}; -use model::{api, Encrypt}; +use model::Encrypt; use payment_manager::{PaymentManager, PaymentNotification}; use search_manager::SearchManager; -use token_manager::{query_tm, TokenManager}; +use token_manager::TokenManager; use crate::public_send_db; use crate::routes::public::Error as PublicError; -use crate::routes::{self, Result}; +use crate::routes::{self, create_auth_pair, AuthPair}; #[get("/search")] async fn search( @@ -57,7 +57,7 @@ async fn search( async fn products( db: Data>, config: Data, -) -> Result> { +) -> routes::Result> { let db = db.into_inner(); let public_path = { let l = config.lock(); @@ -86,7 +86,7 @@ async fn product( path: Path, db: Data>, config: Data, -) -> Result> { +) -> routes::Result> { let product_id: model::ProductId = path.into_inner().into(); let db = db.into_inner(); let public_path = { @@ -122,14 +122,15 @@ async fn product( } #[get("/stocks")] -async fn stocks(db: Data>) -> Result { - public_send_db!(db.into_inner(), database_manager::AllStocks) +async fn stocks(db: Data>) -> routes::Result>> { + let stocks = public_send_db!(owned, db.into_inner(), database_manager::AllStocks); + Ok(Json(stocks)) } #[post("/register")] pub async fn create_account( db: Data>, - Json(payload): Json, + Json(payload): Json, config: Data, tm: Data>, ) -> routes::Result> { @@ -160,6 +161,8 @@ pub async fn create_account( routes::Error::CriticalFailure ); + let role = account.role; + let AuthPair { access_token, access_token_string, @@ -167,61 +170,20 @@ pub async fn create_account( refresh_token_string, } = create_auth_pair(tm, account).await?; - Ok(Json(api::SessionOutput { + Ok(Json(model::api::SessionOutput { access_token: access_token_string, refresh_token: refresh_token_string, exp: access_token.expiration_time, + role, })) } -pub(crate) struct AuthPair { - pub access_token: model::Token, - pub access_token_string: model::AccessTokenString, - pub _refresh_token: model::Token, - pub refresh_token_string: model::RefreshTokenString, -} - -pub(crate) async fn create_auth_pair( - tm: Data>, - account: model::FullAccount, -) -> routes::Result { - let (access_token, refresh_token) = query_tm!( - multi, - tm, - routes::Error::Public(PublicError::DatabaseConnection), - token_manager::CreateToken { - customer_id: account.customer_id, - role: account.role, - subject: account.id, - audience: Some(model::Audience::Web), - exp: None - }, - token_manager::CreateToken { - customer_id: account.customer_id, - role: account.role, - subject: account.id, - audience: Some(model::Audience::Web), - exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()) - } - ); - let (access_token, access_token_string): (model::Token, model::AccessTokenString) = - access_token?; - let (refresh_token, refresh_token_string): (model::Token, model::AccessTokenString) = - refresh_token?; - Ok(AuthPair { - access_token, - access_token_string, - _refresh_token: refresh_token, - refresh_token_string: refresh_token_string.into(), - }) -} - #[post("/sign-in")] async fn sign_in( - Json(payload): Json, + Json(payload): Json, db: Data>, tm: Data>, -) -> Result> { +) -> routes::Result> { let db = db.into_inner(); let account: model::FullAccount = query_db!( @@ -236,6 +198,8 @@ async fn sign_in( return Err(routes::Error::Unauthorized); } + let role = account.role; + let AuthPair { access_token, access_token_string, @@ -243,10 +207,11 @@ async fn sign_in( refresh_token_string, } = create_auth_pair(tm, account).await?; - Ok(Json(api::SessionOutput { + Ok(Json(model::api::SessionOutput { access_token: access_token_string, refresh_token: refresh_token_string, exp: access_token.expiration_time, + role, })) } diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index e62d315..e7eb056 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -316,6 +316,7 @@ pub struct SessionOutput { pub access_token: AccessTokenString, pub refresh_token: RefreshTokenString, pub exp: NaiveDateTime, + pub role: Role, } #[derive(Serialize, Deserialize, Debug)] @@ -391,7 +392,6 @@ pub mod admin { #[derive(Serialize, Deserialize, Debug, Default)] pub struct RegisterResponse { - pub success: bool, pub errors: Vec, pub account: Option, } diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 1c017a2..4273d84 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -664,6 +664,12 @@ impl PasswordConfirmation { #[serde(transparent)] pub struct PassHash(String); +impl PassHash { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + impl PartialEq for Password { fn eq(&self, other: &PasswordConfirmation) -> bool { self.0 == other.0 @@ -977,6 +983,12 @@ pub struct Token { pub jwt_id: uuid::Uuid, } +impl Token { + pub fn account_id(&self) -> AccountId { + AccountId(self.subject) + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] diff --git a/web/Cargo.toml b/web/Cargo.toml index 33ef72f..eff4c88 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -31,6 +31,3 @@ rusty-money = { version = "0.4.1", features = ["iso"] } thiserror = { version = "1.0.31" } -[profile.release] -lto = true -opt-level = 's' diff --git a/web/src/api.rs b/web/src/api.rs index 8d387a5..54b0eb8 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -3,6 +3,7 @@ use std::ops::FromResidual; use seed::fetch::{FetchError, Request}; +pub mod admin; pub mod public; #[derive(Debug)] @@ -12,6 +13,12 @@ pub enum NetRes { Http(FetchError), } +impl From for NetRes { + fn from(e: FetchError) -> Self { + Self::Http(e) + } +} + impl FromResidual>> for NetRes { fn from_residual(residual: Result>) -> Self { match residual { diff --git a/web/src/api/admin.rs b/web/src/api/admin.rs new file mode 100644 index 0000000..6f21789 --- /dev/null +++ b/web/src/api/admin.rs @@ -0,0 +1,35 @@ +use seed::fetch::{Header, Method, Request}; + +use crate::api::{perform, NetRes}; + +pub async fn sign_in(identity: String, password: model::Password) -> NetRes { + use model::api::admin::SignInInput; + + let input = if identity.contains('@') { + SignInInput { + login: None, + email: Some(model::Email::new(identity)), + password, + } + } else { + SignInInput { + login: Some(model::Login::new(identity)), + email: None, + password, + } + }; + perform( + Request::new("/api/v1/sign-in") + .method(Method::Post) + .json(&input) + .map_err(NetRes::Http)?, + ) + .await +} + +/// This request validates if session is still active +/// It should be run from time to time just to check if user should be +/// redirected to sign-in page +pub async fn check_session() -> NetRes { + perform(Request::new("/api/v1/check").method(Method::Get)).await +}