diff --git a/Cargo.lock b/Cargo.lock index 01ce6b2..267cc9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -609,37 +609,40 @@ dependencies = [ "actix-web-opentelemetry", "argon2", "async-trait", + "cart_manager", "chrono", + "config", + "database_manager", "derive_more", "dotenv", + "email_manager", "futures", "futures-util", "gumdrop", - "hmac", "jemallocator", - "jwt", "log", "messagebus", + "model", "oauth2", + "order_manager", "parking_lot 0.12.0", "password-hash", - "pay_u", + "payment_manager", "pretty_env_logger", "rand_core", - "sendgrid", + "search_manager", "serde", "serde_json", - "sha2", - "sonic-channel", "sqlx", "sqlx-core", "tera", "thiserror", + "token_manager", "tokio", "toml", "tracing", "uuid", - "validator", + "validator 0.14.0", ] [[package]] @@ -767,6 +770,22 @@ dependencies = [ "bytes", ] +[[package]] +name = "cart_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "database_manager", + "log", + "model", + "pretty_env_logger", + "thiserror", + "uuid", +] + [[package]] name = "cc" version = "1.0.73" @@ -827,6 +846,21 @@ dependencies = [ "generic-array 0.14.5", ] +[[package]] +name = "config" +version = "0.1.0" +dependencies = [ + "actix-web", + "log", + "parking_lot 0.12.0", + "password-hash", + "pay_u", + "serde", + "serde_json", + "thiserror", + "toml", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1016,6 +1050,23 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +[[package]] +name = "database_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "log", + "model", + "pretty_env_logger", + "sqlx", + "sqlx-core", + "thiserror", + "uuid", +] + [[package]] name = "dbg" version = "1.0.4" @@ -1102,6 +1153,22 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "email_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "log", + "model", + "pretty_env_logger", + "sendgrid", + "thiserror", + "uuid", +] + [[package]] name = "enclose" version = "1.1.8" @@ -1215,6 +1282,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" +[[package]] +name = "fs_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "log", + "model", + "pretty_env_logger", + "thiserror", + "tokio", + "uuid", +] + [[package]] name = "futures" version = "0.3.21" @@ -1866,9 +1949,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] @@ -2006,6 +2089,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "model" +version = "0.1.0" +dependencies = [ + "chrono", + "derive_more", + "serde", + "sqlx", + "sqlx-core", + "thiserror", + "uuid", + "validator 0.15.0", +] + [[package]] name = "native-tls" version = "0.2.10" @@ -2206,6 +2303,22 @@ dependencies = [ "opentelemetry", ] +[[package]] +name = "order_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "database_manager", + "log", + "model", + "pretty_env_logger", + "thiserror", + "uuid", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2295,6 +2408,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "payment_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "database_manager", + "derive_more", + "log", + "model", + "parking_lot 0.12.0", + "pay_u", + "pretty_env_logger", + "serde", + "thiserror", + "uuid", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2770,6 +2903,25 @@ dependencies = [ "untrusted", ] +[[package]] +name = "search_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "derive_more", + "log", + "model", + "parking_lot 0.12.0", + "pretty_env_logger", + "serde", + "sonic-channel", + "thiserror", + "uuid", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -3288,18 +3440,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -3396,11 +3548,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "token_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "argon2", + "chrono", + "config", + "database_manager", + "derive_more", + "hmac", + "jwt", + "log", + "model", + "parking_lot 0.12.0", + "password-hash", + "pretty_env_logger", + "rand_core", + "serde", + "sha2", + "thiserror", + "uuid", +] + [[package]] name = "tokio" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f48b6d60512a392e34dbf7fd456249fd2de3c83669ab642e021903f4015185b" +checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc" dependencies = [ "bytes", "libc", @@ -3764,6 +3941,21 @@ dependencies = [ "validator_types", ] +[[package]] +name = "validator" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07b0a1390e01c0fc35ebb26b28ced33c9a3808f7f9fbe94d3cc01e233bfeed5" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "validator_types" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index b16a531..1fcde3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,14 @@ [workspace] -members = ["api", "web"] +members = [ + "api", + "web", + "shared/model", + "actors/cart_manager", + "actors/database_manager", + "actors/email_manager", + "actors/order_manager", + "actors/payment_manager", + "actors/search_manager", + "actors/token_manager", + "actors/fs_manager", +] diff --git a/actors/cart_manager/Cargo.toml b/actors/cart_manager/Cargo.toml new file mode 100644 index 0000000..86bbb91 --- /dev/null +++ b/actors/cart_manager/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cart_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } +database_manager = { path = "../database_manager" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } diff --git a/api/src/actors/cart_manager.rs b/actors/cart_manager/src/lib.rs similarity index 74% rename from api/src/actors/cart_manager.rs rename to actors/cart_manager/src/lib.rs index cd400a6..e4e9ed4 100644 --- a/api/src/actors/cart_manager.rs +++ b/actors/cart_manager/src/lib.rs @@ -1,7 +1,5 @@ -use actix::{Actor, Addr, Context, Message}; - -use crate::database::{self, Database}; -use crate::model::{ +use database_manager::{query_db, Database}; +use model::{ AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartId, ShoppingCartItem, ShoppingCartItemId, ShoppingCartState, }; @@ -30,7 +28,7 @@ pub enum Error { #[error("Failed to add item to cart")] CantAddItem, #[error("{0}")] - Db(#[from] database::Error), + Db(#[from] database_manager::Error), #[error("Unable to update cart item")] UpdateFailed, #[error("Failed to change quantity")] @@ -42,20 +40,20 @@ pub enum Error { pub type Result = std::result::Result; pub struct CartManager { - db: Addr, + db: actix::Addr, } -impl Actor for CartManager { - type Context = Context; +impl actix::Actor for CartManager { + type Context = actix::Context; } impl CartManager { - pub fn new(db: Addr) -> Self { + pub fn new(db: actix::Addr) -> Self { Self { db } } } -#[derive(Message)] +#[derive(actix::Message)] #[rtype(result = "Result")] pub struct AddItem { pub buyer_id: AccountId, @@ -66,9 +64,9 @@ pub struct AddItem { cart_async_handler!(AddItem, add_item, ShoppingCartItem); -async fn add_item(msg: AddItem, db: Addr) -> Result { +async fn add_item(msg: AddItem, db: actix::Addr) -> Result { match db - .send(database::EnsureActiveShoppingCart { + .send(database_manager::EnsureActiveShoppingCart { buyer_id: msg.buyer_id, }) .await @@ -77,7 +75,7 @@ async fn add_item(msg: AddItem, db: Addr) -> Result _ => return Err(Error::ShoppingCartFailed), }; let cart = match db - .send(database::AccountShoppingCarts { + .send(database_manager::AccountShoppingCarts { account_id: msg.buyer_id, state: Some(ShoppingCartState::Active), }) @@ -95,7 +93,7 @@ async fn add_item(msg: AddItem, db: Addr) -> Result _ => return Err(Error::CartNotAvailable), }; match db - .send(database::CreateShoppingCartItem { + .send(database_manager::CreateShoppingCartItem { product_id: msg.product_id, shopping_cart_id: cart.id, quantity: msg.quantity, @@ -108,7 +106,7 @@ async fn add_item(msg: AddItem, db: Addr) -> Result } } -#[derive(Message)] +#[derive(actix::Message)] #[rtype(result = "Result>")] pub struct RemoveProduct { pub shopping_cart_id: ShoppingCartId, @@ -119,10 +117,10 @@ cart_async_handler!(RemoveProduct, remove_product, Option); pub(crate) async fn remove_product( msg: RemoveProduct, - db: Addr, + db: actix::Addr, ) -> Result> { match db - .send(database::RemoveCartItem { + .send(database_manager::RemoveCartItem { shopping_cart_id: msg.shopping_cart_id, shopping_cart_item_id: Some(msg.shopping_cart_item_id), product_id: None, @@ -141,7 +139,7 @@ pub(crate) async fn remove_product( } } -#[derive(Message)] +#[derive(actix::Message)] #[rtype(result = "Result>")] pub struct ChangeQuantity { pub shopping_cart_id: ShoppingCartId, @@ -154,7 +152,7 @@ cart_async_handler!(ChangeQuantity, change_quantity, Option); pub(crate) async fn change_quantity( msg: ChangeQuantity, - db: Addr, + db: actix::Addr, ) -> Result> { if **msg.quantity == 0 { return remove_product( @@ -166,41 +164,23 @@ pub(crate) async fn change_quantity( ) .await; } - let item: ShoppingCartItem = match db - .send(database::FindShoppingCartItem { + let item: ShoppingCartItem = query_db!( + db, + database_manager::FindShoppingCartItem { id: msg.shopping_cart_item_id, - }) - .await - { - Ok(Ok(row)) => row, - Ok(Err(db_err)) => { - log::error!("{db_err}"); - return Err(Error::NotExists(msg.shopping_cart_item_id)); - } - Err(act_err) => { - log::error!("{act_err:?}"); - return Err(Error::NotExists(msg.shopping_cart_item_id)); - } - }; + }, + Error::NotExists(msg.shopping_cart_item_id) + ); - match db - .send(database::UpdateShoppingCartItem { + Ok(Some(query_db!( + db, + database_manager::UpdateShoppingCartItem { id: msg.shopping_cart_item_id, product_id: item.product_id, shopping_cart_id: item.shopping_cart_id, quantity: msg.quantity, quantity_unit: msg.quantity_unit, - }) - .await - { - Ok(Ok(row)) => Ok(Some(row)), - Ok(Err(db_err)) => { - log::error!("{db_err}"); - Err(Error::ChangeQuantity) - } - Err(act_err) => { - log::error!("{act_err:?}"); - Err(Error::ChangeQuantity) - } - } + }, + Error::ChangeQuantity + ))) } diff --git a/actors/database_manager/Cargo.toml b/actors/database_manager/Cargo.toml new file mode 100644 index 0000000..3aeaa9c --- /dev/null +++ b/actors/database_manager/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "database_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +sqlx = { version = "0.5", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } +sqlx-core = { version = "0.5", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } diff --git a/api/src/actors/database/account_orders.rs b/actors/database_manager/src/account_orders.rs similarity index 96% rename from api/src/actors/database/account_orders.rs rename to actors/database_manager/src/account_orders.rs index 5954c5a..c58f4dc 100644 --- a/api/src/actors/database/account_orders.rs +++ b/actors/database_manager/src/account_orders.rs @@ -1,11 +1,11 @@ +use model::*; use sqlx::PgPool; use super::Result; -use crate::database::{ - create_order_item, shopping_cart_set_state, CreateOrderItem, Database, ShoppingCartSetState, +use crate::{ + create_order_item, db_async_handler, shopping_cart_set_state, CreateOrderItem, Database, + ShoppingCartSetState, }; -use crate::db_async_handler; -use crate::model::*; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -45,7 +45,7 @@ ORDER BY id DESC } pub mod create_order { - use crate::model::{ProductId, Quantity, QuantityUnit}; + use model::{ProductId, Quantity, QuantityUnit}; pub struct OrderItem { pub product_id: ProductId, diff --git a/api/src/actors/database/accounts.rs b/actors/database_manager/src/accounts.rs similarity index 96% rename from api/src/actors/database/accounts.rs rename to actors/database_manager/src/accounts.rs index 756a8bb..84c322a 100644 --- a/api/src/actors/database/accounts.rs +++ b/actors/database_manager/src/accounts.rs @@ -1,9 +1,8 @@ +use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; use sqlx::PgPool; use super::Result; -use crate::database::Database; -use crate::db_async_handler; -use crate::model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; +use crate::{db_async_handler, Database}; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/api/src/actors/database.rs b/actors/database_manager/src/lib.rs similarity index 97% rename from api/src/actors/database.rs rename to actors/database_manager/src/lib.rs index 13e2a03..3f8b0a0 100644 --- a/api/src/actors/database.rs +++ b/actors/database_manager/src/lib.rs @@ -1,6 +1,7 @@ pub use account_orders::*; pub use accounts::*; use actix::{Actor, Context}; +use config::SharedAppConfig; pub use order_items::*; pub use products::*; pub use shopping_cart_items::*; @@ -9,8 +10,6 @@ use sqlx::PgPool; pub use stocks::*; pub use tokens::*; -use crate::config::SharedAppConfig; - pub mod account_orders; pub mod accounts; pub mod order_items; @@ -134,7 +133,7 @@ impl Clone for Database { } impl Database { - pub(crate) async fn build(config: SharedAppConfig) -> Result { + pub async fn build(config: SharedAppConfig) -> Result { let url = config.lock().database().url(); let pool = sqlx::PgPool::connect(&url).await.map_err(Error::Connect)?; Ok(Database { pool }) diff --git a/api/src/actors/database/order_items.rs b/actors/database_manager/src/order_items.rs similarity index 97% rename from api/src/actors/database/order_items.rs rename to actors/database_manager/src/order_items.rs index bdfe4c9..39d8946 100644 --- a/api/src/actors/database/order_items.rs +++ b/actors/database_manager/src/order_items.rs @@ -1,9 +1,8 @@ +use model::*; use sqlx::PgPool; use super::Result; -use crate::database::Database; -use crate::model::*; -use crate::{db_async_handler, model}; +use crate::{db_async_handler, Database}; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/api/src/actors/database/products.rs b/actors/database_manager/src/products.rs similarity index 93% rename from api/src/actors/database/products.rs rename to actors/database_manager/src/products.rs index 45c07e6..1b9d217 100644 --- a/api/src/actors/database/products.rs +++ b/actors/database_manager/src/products.rs @@ -1,12 +1,11 @@ use actix::Message; - -use super::Result; -use crate::database::Database; -use crate::model::{ +use model::{ Days, Price, Product, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, }; -use crate::{database, model}; + +use super::Result; +use crate::Database; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -48,7 +47,7 @@ FROM products .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Product(Error::All) + crate::Error::Product(Error::All) }) } @@ -92,7 +91,7 @@ RETURNING id, .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Product(Error::Create) + crate::Error::Product(Error::Create) }) } @@ -144,7 +143,7 @@ RETURNING id, .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Product(Error::Update) + crate::Error::Product(Error::Update) }) } @@ -183,7 +182,7 @@ RETURNING id, .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Product(Error::Delete) + crate::Error::Product(Error::Delete) }) } @@ -226,6 +225,6 @@ WHERE shopping_cart_id = $1 .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Product(Error::ShoppingCartProducts) + crate::Error::Product(Error::ShoppingCartProducts) }) } diff --git a/api/src/actors/database/shopping_cart_items.rs b/actors/database_manager/src/shopping_cart_items.rs similarity index 96% rename from api/src/actors/database/shopping_cart_items.rs rename to actors/database_manager/src/shopping_cart_items.rs index 3840754..fc810ce 100644 --- a/api/src/actors/database/shopping_cart_items.rs +++ b/actors/database_manager/src/shopping_cart_items.rs @@ -1,9 +1,8 @@ +use model::*; use sqlx::PgPool; use super::Result; -use crate::database::Database; -use crate::model::*; -use crate::{database, db_async_handler}; +use crate::{db_async_handler, Database}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -253,7 +252,7 @@ WHERE shopping_cart_id = $1 .await .map_err(|e| { log::error!("{e:?}"); - super::Error::ShoppingCartItem(Error::CartItems(shopping_cart_id)) + crate::Error::ShoppingCartItem(Error::CartItems(shopping_cart_id)) }) } @@ -300,13 +299,13 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit ) .bind(msg.shopping_cart_id) .bind(product_id), - _ => return Err(database::Error::ShoppingCartItem(Error::NoIdentity)), + _ => return Err(crate::Error::ShoppingCartItem(Error::NoIdentity)), } .fetch_optional(&pool) .await .map_err(|e| { log::error!("{e:?}"); - database::Error::ShoppingCartItem(Error::Update { + crate::Error::ShoppingCartItem(Error::Update { shopping_cart_item_id: msg.shopping_cart_item_id, product_id: msg.product_id, }) diff --git a/api/src/actors/database/shopping_carts.rs b/actors/database_manager/src/shopping_carts.rs similarity index 98% rename from api/src/actors/database/shopping_carts.rs rename to actors/database_manager/src/shopping_carts.rs index 20ca43d..23a2f59 100644 --- a/api/src/actors/database/shopping_carts.rs +++ b/actors/database_manager/src/shopping_carts.rs @@ -1,9 +1,8 @@ +use model::*; use sqlx::PgPool; use super::Result; -use crate::database::Database; -use crate::db_async_handler; -use crate::model::*; +use crate::{db_async_handler, Database}; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/api/src/actors/database/stocks.rs b/actors/database_manager/src/stocks.rs similarity index 88% rename from api/src/actors/database/stocks.rs rename to actors/database_manager/src/stocks.rs index 9e7cf59..a3d11fc 100644 --- a/api/src/actors/database/stocks.rs +++ b/actors/database_manager/src/stocks.rs @@ -1,10 +1,8 @@ use actix::Message; +use model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; use sqlx::PgPool; -use super::Result; -use crate::database::Database; -use crate::model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; -use crate::{database, model}; +use crate::{Database, Result}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -35,7 +33,7 @@ FROM stocks .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Stock(Error::All) + crate::Error::Stock(Error::All) }) } @@ -64,7 +62,7 @@ RETURNING id, product_id, quantity, quantity_unit .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Stock(Error::Create) + crate::Error::Stock(Error::Create) }) } @@ -98,7 +96,7 @@ RETURNING id, product_id, quantity, quantity_unit .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Stock(Error::Update) + crate::Error::Stock(Error::Update) }) } @@ -123,6 +121,6 @@ RETURNING id, product_id, quantity, quantity_unit .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Stock(Error::Delete) + crate::Error::Stock(Error::Delete) }) } diff --git a/api/src/actors/database/tokens.rs b/actors/database_manager/src/tokens.rs similarity index 85% rename from api/src/actors/database/tokens.rs rename to actors/database_manager/src/tokens.rs index 445d35d..fbb2dae 100644 --- a/api/src/actors/database/tokens.rs +++ b/actors/database_manager/src/tokens.rs @@ -1,10 +1,8 @@ use actix::Message; +use model::{AccountId, Audience, Token}; use sqlx::PgPool; -use super::Result; -use crate::database::Database; -use crate::model::{AccountId, Audience, Token}; -use crate::{database, db_async_handler, Role}; +use crate::{db_async_handler, Database, Result}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -33,7 +31,7 @@ WHERE jwt_id = $1 .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Token(Error::Jti) + crate::Error::Token(Error::Jti) }) } @@ -41,7 +39,7 @@ WHERE jwt_id = $1 #[rtype(result = "Result")] pub struct CreateToken { pub customer_id: uuid::Uuid, - pub role: Role, + pub role: model::Role, pub subject: AccountId, pub audience: Audience, } @@ -68,6 +66,6 @@ RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not .await .map_err(|e| { log::error!("{e:?}"); - database::Error::Token(Error::Create) + crate::Error::Token(Error::Create) }) } diff --git a/actors/email_manager/Cargo.toml b/actors/email_manager/Cargo.toml new file mode 100644 index 0000000..181cf23 --- /dev/null +++ b/actors/email_manager/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "email_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +sendgrid = { version = "0.17", features = ["async"] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } diff --git a/api/src/actors/email_manager.rs b/actors/email_manager/src/lib.rs similarity index 95% rename from api/src/actors/email_manager.rs rename to actors/email_manager/src/lib.rs index 1e9bc4a..5642fb7 100644 --- a/api/src/actors/email_manager.rs +++ b/actors/email_manager/src/lib.rs @@ -1,7 +1,6 @@ use std::sync::Arc; -use crate::config::SharedAppConfig; -use crate::Email; +use config::SharedAppConfig; #[macro_export] macro_rules! mail_async_handler { @@ -50,7 +49,7 @@ impl EmailManager { #[derive(actix::Message)] #[rtype(result = "Result")] pub struct TestMail { - pub receiver: Email, + pub receiver: model::Email, } mail_async_handler!(TestMail, test_mail, SendState); diff --git a/actors/fs_manager/Cargo.toml b/actors/fs_manager/Cargo.toml new file mode 100644 index 0000000..accce94 --- /dev/null +++ b/actors/fs_manager/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fs_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } + +tokio = { version = "1.18.1", features = ["full"] } diff --git a/actors/fs_manager/src/lib.rs b/actors/fs_manager/src/lib.rs new file mode 100644 index 0000000..83e718c --- /dev/null +++ b/actors/fs_manager/src/lib.rs @@ -0,0 +1,145 @@ +#![feature(io_error_more)] + +use std::ffi::OsStr; +use std::io::Write; + +use config::SharedAppConfig; +use model::FileName; + +#[macro_export] +macro_rules! fs_async_handler { + ($msg: ty, $async: ident, $res: ty) => { + impl actix::Handler<$msg> for FsManager { + type Result = actix::ResponseActFuture>; + + fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { + use actix::WrapFuture; + let config = self.config.clone(); + Box::pin(async { $async(msg, config).await }.into_actor(self)) + } + } + }; +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Can't access file system. Please check privileges")] + StorageUnavailable, + #[error("Can't write to file. Please check privileges. {0:?}")] + CantWrite(std::io::Error), + #[error("Can't write to file. There's no more space on disk")] + NoSpace, + #[error("Can't remove file. Please check privileges. {0:?}")] + CantRemove(std::io::Error), + #[error("Can't write to file. Invalid path, no filename")] + InvalidPath, +} + +pub type Result = std::result::Result; + +pub struct FsManager { + config: SharedAppConfig, +} + +impl actix::Actor for FsManager { + type Context = actix::Context; +} + +impl FsManager { + pub async fn build(config: SharedAppConfig) -> Result { + Self::validate_fs(config.clone()).await?; + Ok(Self { config }) + } + + async fn validate_fs(config: SharedAppConfig) -> Result<()> { + let l = config.lock(); + let output_path = l.files().local_path(); + let test_file = std::path::PathBuf::new() + .join(output_path) + .join(format!("{}", uuid::Uuid::new_v4())); + tokio::fs::remove_file(test_file.clone()).await.ok(); + tokio::fs::write(test_file.clone(), b"1") + .await + .map_err(|_| Error::StorageUnavailable)?; + tokio::fs::remove_file(test_file.clone()) + .await + .map_err(|_| Error::StorageUnavailable) + } +} + +#[derive(actix::Message)] +#[rtype(result = "Result<()>")] +pub struct RemoveFile { + pub file_name: String, +} + +fs_async_handler!(RemoveFile, remove_file, ()); + +pub(crate) async fn remove_file(msg: RemoveFile, config: SharedAppConfig) -> Result<()> { + let output_path = { + let l = config.lock(); + l.files().local_path() + }; + match tokio::fs::remove_file( + std::path::PathBuf::new() + .join(output_path) + .join(msg.file_name), + ) + .await + { + Ok(_) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(Error::CantRemove(e)), + } +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct WriteFile { + pub file_name: String, + pub stream: tokio::sync::mpsc::UnboundedReceiver, +} + +fs_async_handler!(WriteFile, write_file, FileName); + +pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Result { + let WriteFile { + file_name, + mut stream, + } = msg; + + let p = std::path::Path::new(&file_name); + let ext = p + .file_name() + .and_then(OsStr::to_str) + .map(std::path::Path::new) + .and_then(std::path::Path::extension) + .and_then(OsStr::to_str) + .map(String::from); + + let file_name = format!( + "{}{}", + uuid::Uuid::new_v4(), + ext.map(|s| format!(".{s}")).unwrap_or_default() + ); + + let output_path = { + let l = config.lock(); + l.files().local_path() + }; + + let path = std::path::PathBuf::new().join(output_path).join(&file_name); + let mut file = match std::fs::File::create(path) { + Ok(f) => f, + Err(e) => return Err(Error::CantWrite(e)), + }; + + while let Some(b) = stream.recv().await { + match file.write(&[b]) { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::StorageFull => return Err(Error::NoSpace), + Err(e) => return Err(Error::CantWrite(e)), + } + } + Ok(FileName::from(file_name)) +} diff --git a/actors/order_manager/Cargo.toml b/actors/order_manager/Cargo.toml new file mode 100644 index 0000000..074a85d --- /dev/null +++ b/actors/order_manager/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "order_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } +database_manager = { path = "../database_manager" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } diff --git a/api/src/actors/order_manager.rs b/actors/order_manager/src/lib.rs similarity index 66% rename from api/src/actors/order_manager.rs rename to actors/order_manager/src/lib.rs index 6abcd6c..0670882 100644 --- a/api/src/actors/order_manager.rs +++ b/actors/order_manager/src/lib.rs @@ -1,10 +1,7 @@ use actix::Message; - -use crate::config::SharedAppConfig; -use crate::database::{self, SharedDatabase}; -use crate::model::{ - AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem, -}; +use config::SharedAppConfig; +use database_manager::{query_db, SharedDatabase}; +use model::{AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem}; #[macro_export] macro_rules! order_async_handler { @@ -63,64 +60,40 @@ pub(crate) async fn create_order( db: SharedDatabase, _config: SharedAppConfig, ) -> Result { - let cart: ShoppingCart = match db - .send(database::FindShoppingCart { + let cart: ShoppingCart = query_db!( + db, + database_manager::FindShoppingCart { id: msg.shopping_cart_id, - }) - .await - { - Ok(Ok(cart)) => cart, - Ok(Err(e)) => { - log::error!("{e}"); - return Err(Error::ShoppingCart); - } - Err(e) => { - log::error!("{e:?}"); - return Err(Error::DatabaseInternal); - } - }; - let items: Vec = match db - .send(database::AccountShoppingCartItems { + }, + Error::ShoppingCart, + Error::DatabaseInternal + ); + let items: Vec = query_db!( + db, + database_manager::AccountShoppingCartItems { account_id: cart.buyer_id, shopping_cart_id: Some(cart.id), - }) - .await - { - Ok(Ok(items)) => items, - Ok(Err(e)) => { - log::error!("{e}"); - return Err(Error::ShoppingCart); - } - Err(e) => { - log::error!("{e:?}"); - return Err(Error::DatabaseInternal); - } - }; - let order = match db - .send(database::CreateAccountOrder { + }, + Error::ShoppingCart, + Error::DatabaseInternal + ); + let order = query_db!( + db, + database_manager::CreateAccountOrder { shopping_cart_id: cart.id, buyer_id: msg.account_id, items: items .into_iter() - .map(|item| database::create_order::OrderItem { + .map(|item| database_manager::create_order::OrderItem { product_id: item.product_id, quantity: item.quantity, quantity_unit: item.quantity_unit, }) .collect(), - }) - .await - { - Ok(Ok(order)) => order, - Ok(Err(e)) => { - log::error!("{e}"); - return Err(Error::CreateAccountOrder); - } - Err(e) => { - log::error!("{e:?}"); - return Err(Error::DatabaseInternal); - } - }; + }, + Error::CreateAccountOrder, + Error::DatabaseInternal + ); Ok(order) } diff --git a/actors/payment_manager/Cargo.toml b/actors/payment_manager/Cargo.toml new file mode 100644 index 0000000..351dd40 --- /dev/null +++ b/actors/payment_manager/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "payment_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } +database_manager = { path = "../database_manager" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } + +derive_more = { version = "0.99", features = [] } +parking_lot = { version = "0.12", features = [] } + +serde = { version = "1.0", features = ["derive"] } + +pay_u = { version = '0.1', features = ["single-client"] } diff --git a/api/src/actors/payment_manager.rs b/actors/payment_manager/src/lib.rs similarity index 94% rename from api/src/actors/payment_manager.rs rename to actors/payment_manager/src/lib.rs index b365847..bd570ba 100644 --- a/api/src/actors/payment_manager.rs +++ b/actors/payment_manager/src/lib.rs @@ -2,13 +2,11 @@ use std::collections::HashMap; use std::sync::Arc; use actix::Addr; +use config::SharedAppConfig; +use database_manager::{query_db, Database}; +use model::{AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit}; use parking_lot::Mutex; -use crate::config::SharedAppConfig; -use crate::database::Database; -use crate::model::{AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit}; -use crate::{database, model, query_db}; - #[macro_export] macro_rules! pay_async_handler { ($msg: ty, $async: ident, $res: ty) => { @@ -183,14 +181,14 @@ pub(crate) async fn request_payment( let cart: model::ShoppingCart = query_db!( db, - database::EnsureActiveShoppingCart { + database_manager::EnsureActiveShoppingCart { buyer_id: msg.buyer_id }, Error::UnavailableShoppingCart ); let cart_items: Vec = query_db!( db, - database::CartItems { + database_manager::CartItems { shopping_cart_id: cart.id, }, Error::UnavailableShoppingCart @@ -205,7 +203,7 @@ pub(crate) async fn request_payment( let cart_products: Vec = query_db!( db, - database::ShoppingCartProducts { + database_manager::ShoppingCartProducts { shopping_cart_id: cart.id, }, Error::UnavailableShoppingCart @@ -213,7 +211,7 @@ pub(crate) async fn request_payment( let db_order: model::AccountOrder = query_db!( db, - database::CreateAccountOrder { + database_manager::CreateAccountOrder { buyer_id: msg.buyer_id, items: cart_products .iter() @@ -225,7 +223,7 @@ pub(crate) async fn request_payment( model::QuantityUnit::Gram, ) }); - database::create_order::OrderItem { + database_manager::create_order::OrderItem { product_id: product.id, quantity, quantity_unit, @@ -276,7 +274,7 @@ pub(crate) async fn request_payment( query_db!( db, - database::SetOrderServiceId { + database_manager::SetOrderServiceId { service_order_id: order_id.0, id: db_order.id, }, @@ -289,7 +287,7 @@ pub(crate) async fn request_payment( let order_items = query_db!( db, - database::OrderItems { + database_manager::OrderItems { order_id: db_order.id }, Error::CreateOrder @@ -332,7 +330,7 @@ pub(crate) async fn update_payment( pay_u::PaymentStatus::Canceled => OrderStatus::Cancelled, }; let _ = db - .send(database::UpdateAccountOrderByExt { + .send(database_manager::UpdateAccountOrderByExt { status, order_ext_id, }) diff --git a/actors/search_manager/Cargo.toml b/actors/search_manager/Cargo.toml new file mode 100644 index 0000000..2784370 --- /dev/null +++ b/actors/search_manager/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "search_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } + +derive_more = { version = "0.99", features = [] } +parking_lot = { version = "0.12", features = [] } + +serde = { version = "1.0", features = ["derive"] } + +sonic-channel = { version = "0.6.0", features = ["ingest"] } diff --git a/actors/search_manager/src/lib.rs b/actors/search_manager/src/lib.rs new file mode 100644 index 0000000..68cd708 --- /dev/null +++ b/actors/search_manager/src/lib.rs @@ -0,0 +1,140 @@ +use std::sync::{Arc, Mutex}; + +use config::SharedAppConfig; +use sonic_channel::SonicChannel; + +#[macro_export] +macro_rules! search_async_handler { + ($msg: ty, $async: ident, $res: ty) => { + impl actix::Handler<$msg> for SearchManager { + type Result = actix::ResponseActFuture>>; + + fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { + use actix::WrapFuture; + match self.channels.clone() { + Some(channels) => { + let config = self.config.clone(); + Box::pin(async { $async(msg, channels, config).await }.into_actor(self)) + } + None => Box::pin(async { Ok(None) }.into_actor(self)), + } + } + } + }; +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Can't create index")] + CantCreate, + #[error("Failed to find records in bucket")] + QueryFailed, +} + +pub type Result = std::result::Result; + +#[derive(Clone)] +pub struct Channels { + search: Arc>, + ingest: Arc>, +} + +#[derive(Clone)] +pub struct SearchManager { + channels: Option, + config: SharedAppConfig, +} + +impl SearchManager { + pub fn new(config: SharedAppConfig) -> Self { + let enabled = config.lock().search().search_active(); + + let channels = if enabled { + let search = { + let l = config.lock(); + Arc::new(Mutex::new( + sonic_channel::SearchChannel::start( + l.search().sonic_search_addr(), + l.search().sonic_search_pass(), + ) + .expect("Failed to connect to sonic search channel"), + )) + }; + let ingest = { + let l = config.lock(); + Arc::new(Mutex::new( + sonic_channel::IngestChannel::start( + l.search().sonic_ingest_addr(), + l.search().sonic_ingest_pass(), + ) + .expect("Failed to connect to sonic ingest channel"), + )) + }; + Some(Channels { search, ingest }) + } else { + None + }; + Self { channels, config } + } +} + +impl actix::Actor for SearchManager { + type Context = actix::Context; +} + +#[derive(actix::Message)] +#[rtype(result = "Result>>")] +pub struct Search { + pub query: String, + pub collection: String, + pub lang: String, +} + +search_async_handler!(Search, search, Vec); + +pub(crate) async fn search( + msg: Search, + channels: Channels, + _config: SharedAppConfig, +) -> Result>> { + if let Ok(l) = channels.search.lock() { + match l.query(&msg.collection, &msg.lang, &msg.query) { + Ok(res) => Ok(Some(res)), + Err(e) => { + log::error!("{e:?}"); + Err(Error::QueryFailed) + } + } + } else { + Ok(Some(vec![])) + } +} + +#[derive(actix::Message)] +#[rtype(result = "Result>")] +pub struct CreateIndex { + pub key: String, + pub value: String, + pub collection: String, + pub lang: String, +} + +search_async_handler!(CreateIndex, create_index, ()); + +pub(crate) async fn create_index( + msg: CreateIndex, + channels: Channels, + _config: SharedAppConfig, +) -> Result> { + if let Ok(l) = channels.ingest.lock() { + match l.push(&msg.collection, &msg.lang, &msg.key, &msg.value) { + Ok(_) => Ok(Some(())), + Err(e) => { + log::error!("{e:?}"); + Err(Error::CantCreate) + } + } + } else { + Ok(Some(())) + } +} diff --git a/actors/token_manager/Cargo.toml b/actors/token_manager/Cargo.toml new file mode 100644 index 0000000..786010d --- /dev/null +++ b/actors/token_manager/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "token_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } +database_manager = { path = "../database_manager" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } + +derive_more = { version = "0.99", features = [] } +parking_lot = { version = "0.12", features = [] } + +serde = { version = "1.0", features = ["derive"] } + +password-hash = { version = "0.4", features = ["alloc"] } +argon2 = { version = "0.4", features = ["parallel", "password-hash"] } +rand_core = { version = "0.6", features = ["std"] } + +jwt = { version = "0.16", features = [] } +hmac = { version = "0.12", features = [] } +sha2 = { version = "0.10", features = [] } diff --git a/api/src/actors/token_manager.rs b/actors/token_manager/src/lib.rs similarity index 89% rename from api/src/actors/token_manager.rs rename to actors/token_manager/src/lib.rs index 0062370..7e5b6ab 100644 --- a/api/src/actors/token_manager.rs +++ b/actors/token_manager/src/lib.rs @@ -3,15 +3,13 @@ use std::str::FromStr; use actix::{Addr, Message}; use chrono::prelude::*; +use config::SharedAppConfig; +use database_manager::{query_db, Database}; use hmac::digest::KeyInit; use hmac::Hmac; +use model::{AccountId, Audience, Role, Token, TokenString}; use sha2::Sha256; -use crate::config::SharedAppConfig; -use crate::database::{Database, TokenByJti}; -use crate::model::{AccountId, Audience, Token, TokenString}; -use crate::{database, Role}; - #[macro_export] macro_rules! token_async_handler { ($msg: ty, $async: ident, $res: ty) => { @@ -105,25 +103,17 @@ pub(crate) async fn create_token( } = msg; let audience = audience.unwrap_or_default(); - let token: Token = match db - .send(database::CreateToken { + let token: Token = query_db!( + db, + database_manager::CreateToken { customer_id, role, subject, audience, - }) - .await - { - Ok(Ok(token)) => token, - Ok(Err(db_err)) => { - log::error!("{db_err}"); - return Err(Error::Save); - } - Err(act_err) => { - log::error!("{act_err:?}"); - return Err(Error::SaveInternal); - } - }; + }, + Error::Save, + Error::SaveInternal + ); let token_string = { use jwt::SignWithKey; @@ -212,25 +202,17 @@ pub(crate) async fn validate( _ => return Err(Error::Validate), }; - let token: Token = match db - .send(TokenByJti { + let token: Token = query_db!( + db, + database_manager::TokenByJti { jti: match uuid::Uuid::from_str(jti) { Ok(uid) => uid, _ => return Err(Error::Validate), }, - }) - .await - { - Ok(Ok(token)) => token, - Ok(Err(e)) => { - log::error!("{e}"); - return Err(Error::Validate); - } - Err(e) => { - log::error!("{e:?}"); - return Err(Error::ValidateInternal); - } - }; + }, + Error::Validate, + Error::ValidateInternal + ); if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) { return Ok((token, false)); diff --git a/api/Cargo.toml b/api/Cargo.toml index e51ac68..3477199 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -4,6 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +model = { path = "../shared/model", version = "0.1", features = ["db"] } +config = { path = "../shared/config" } +database_manager = { path = "../actors/database_manager" } +cart_manager = { path = "../actors/cart_manager" } +email_manager = { path = "../actors/email_manager" } +order_manager = { path = "../actors/order_manager" } +payment_manager = { path = "../actors/payment_manager" } +search_manager = { path = "../actors/search_manager" } +token_manager = { path = "../actors/token_manager" } + actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } actix-web = { version = "4.0", features = [] } @@ -53,21 +63,11 @@ tokio = { version = "1.17", features = ["full"] } futures = { version = "0.3", features = [] } futures-util = { version = "0.3", features = [] } -jwt = { version = "0.16", features = [] } -hmac = { version = "0.12", features = [] } -sha2 = { version = "0.10", features = [] } - oauth2 = { version = "4.1", features = [] } async-trait = { version = "0.1", features = [] } jemallocator = { version = "0.3", features = [] } -sendgrid = { version = "0.17", features = ["async"] } - -pay_u = { version = '0.1', features = ["single-client"] } - -sonic-channel = { version = "0.6.0", features = ["ingest"] } - # For rewrite into bus-based app messagebus = { version = "0.9.13" } diff --git a/api/src/actors/mod.rs b/api/src/actors/mod.rs deleted file mode 100644 index 5bd4ace..0000000 --- a/api/src/actors/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod cart_manager; -pub mod database; -pub mod email_manager; -pub mod order_manager; -pub mod payment_manager; -pub mod search_manager; -pub mod token_manager; diff --git a/api/src/actors/search_manager.rs b/api/src/actors/search_manager.rs deleted file mode 100644 index fc470bf..0000000 --- a/api/src/actors/search_manager.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use sonic_channel::SonicChannel; - -use crate::config::SharedAppConfig; - -pub struct Channels { - search: sonic_channel::SearchChannel, - ingest: sonic_channel::IngestChannel, -} - -#[derive(Clone)] -pub struct SearchManager { - channels: Option>>, - config: SharedAppConfig, -} - -impl SearchManager { - pub fn new(config: SharedAppConfig) -> Self { - let enabled = config.lock().search().search_active(); - - let channels = if enabled { - let search = { - let l = config.lock(); - sonic_channel::SearchChannel::start( - l.search().sonic_search_addr(), - l.search().sonic_search_pass(), - ) - .expect("Failed to connect to sonic search channel") - }; - let ingest = { - let l = config.lock(); - sonic_channel::IngestChannel::start( - l.search().sonic_ingest_addr(), - l.search().sonic_ingest_pass(), - ) - .expect("Failed to connect to sonic ingest channel") - }; - Some(Arc::new(Mutex::new(Channels { search, ingest }))) - } else { - None - }; - Self { channels, config } - } -} - -impl actix::Actor for SearchManager { - type Context = actix::Context; -} diff --git a/api/src/logic/mod.rs b/api/src/logic/mod.rs index c64be5a..a66f398 100644 --- a/api/src/logic/mod.rs +++ b/api/src/logic/mod.rs @@ -1,7 +1,7 @@ use argon2::{Algorithm, Argon2, Params, Version}; +use model::Password; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; -use crate::model::Password; use crate::PassHash; mod order_state; diff --git a/api/src/main.rs b/api/src/main.rs index 3e18ed1..2806b48 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -8,7 +8,10 @@ use actix_session::SessionMiddleware; use actix_web::middleware::Logger; use actix_web::web::Data; use actix_web::{App, HttpServer}; +use config::UpdateConfig; +use email_manager::TestMail; use jemallocator::Jemalloc; +use model::{Email, Login, PassHash, Password, Role}; use opts::{ Command, CreateAccountCmd, CreateAccountOpts, GenerateHashOpts, MigrateOpts, Opts, ServerOpts, TestMailerOpts, @@ -16,18 +19,9 @@ use opts::{ use password_hash::SaltString; use validator::{validate_email, validate_length}; -use crate::actors::{ - database, email_manager, order_manager, payment_manager, search_manager, token_manager, -}; -use crate::email_manager::TestMail; use crate::logic::encrypt_password; -use crate::model::{Email, Login, PassHash, Password, Role}; -use crate::opts::UpdateConfig; -pub mod actors; -pub mod config; pub mod logic; -pub mod model; mod opts; pub mod routes; @@ -43,7 +37,7 @@ pub enum Error { #[error("Unable to read password from STDIN. {0:?}")] ReadPass(std::io::Error), #[error("{0}")] - Database(#[from] database::Error), + Database(#[from] database_manager::Error), } pub type Result = std::result::Result; @@ -53,7 +47,9 @@ async fn server(opts: ServerOpts) -> Result<()> { let app_config = config::default_load(&opts); - let db = database::Database::build(app_config.clone()).await?.start(); + let db = database_manager::Database::build(app_config.clone()) + .await? + .start(); let token_manager = token_manager::TokenManager::new(app_config.clone(), db.clone()).start(); let order_manager = order_manager::OrderManager::new(app_config.clone(), db.clone()).start(); let payment_manager = payment_manager::PaymentManager::build(app_config.clone(), db.clone()) @@ -89,7 +85,11 @@ async fn server(opts: ServerOpts) -> Result<()> { .app_data(Data::new(payment_manager.clone())) .app_data(Data::new(search_manager.clone())) .configure(routes::configure) - // .default_service(web::to(HttpResponse::Ok)) + .service({ + let l = app_config.lock(); + actix_files::Files::new(&l.files().public_path(), l.files().local_path()) + }) + .default_service(actix_web::web::to(actix_web::HttpResponse::Ok)) }) .bind(addr) .map_err(Error::Boot)? @@ -102,7 +102,7 @@ async fn migrate(opts: MigrateOpts) -> Result<()> { use sqlx::migrate::MigrateError; let config = config::default_load(&opts); - let db = database::Database::build(config).await?; + let db = database_manager::Database::build(config).await?; let res: std::result::Result<(), MigrateError> = sqlx::migrate!("../migrations").run(db.pool()).await; match res { @@ -133,7 +133,9 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> { panic!("Login must have at least 4 characters and no more than 100"); } let config = config::default_load(&opts); - let db = database::Database::build(config.clone()).await?.start(); + let db = database_manager::Database::build(config.clone()) + .await? + .start(); let pass = match opts.pass_file { Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?, None => { @@ -160,7 +162,7 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> { } let hash = encrypt_password(&Password::from(pass), &config.lock().web().pass_salt()).unwrap(); - db.send(database::CreateAccount { + db.send(database_manager::CreateAccount { email: Email::from(opts.email), login: Login::from(opts.login), pass_hash: PassHash::from(hash), @@ -208,6 +210,9 @@ async fn main() -> Result<()> { Command::GenerateHash(opts) => generate_hash(opts).await, Command::CreateAccount(opts) => create_account(opts).await, Command::TestMailer(opts) => test_mailer(opts).await, - Command::ConfigInfo(_) => config::config_info().await, + Command::ConfigInfo(_) => { + config::config_info().await.unwrap(); + Ok(()) + } } } diff --git a/api/src/opts.rs b/api/src/opts.rs index b7ad20b..4dcc30f 100644 --- a/api/src/opts.rs +++ b/api/src/opts.rs @@ -1,11 +1,6 @@ +use config::{AppConfig, UpdateConfig}; use gumdrop::Options; - -use crate::config::AppConfig; -use crate::model::Email; - -pub trait UpdateConfig { - fn update_config(&self, config: &mut AppConfig); -} +use model::Email; pub trait ResolveDbUrl { fn own_db_url(&self) -> Option; diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 22f244e..9760460 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -4,14 +4,14 @@ 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::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role}; use serde::{Deserialize, Serialize}; -use crate::config::SharedAppConfig; -use crate::database::{AccountByIdentity, Database}; use crate::logic::encrypt_password; -use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role}; +use crate::routes; use crate::routes::{RequireLogin, Result}; -use crate::{database, model, routes}; #[macro_export] macro_rules! admin_send_db { @@ -42,7 +42,7 @@ pub enum Error { #[error("Password and password confirmation are different")] DifferentPasswords, #[error("{0}")] - Database(#[from] database::Error), + Database(#[from] database_manager::Error), } #[derive(Serialize)] @@ -71,23 +71,14 @@ async fn sign_in( ) -> Result { log::debug!("{:?}", payload); let db = db.into_inner(); - let user: model::FullAccount = match db - .send(AccountByIdentity { + let user: model::FullAccount = query_db!( + db, + database_manager::AccountByIdentity { email: payload.email, login: payload.login, - }) - .await - { - Ok(Ok(user)) => user, - Ok(Err(e)) => { - log::error!("{}", e); - return Err(routes::Error::Unauthorized); - } - Err(e) => { - log::error!("{}", e); - return Err(routes::Error::Unauthorized); - } - }; + }, + routes::Error::Unauthorized + ); if let Err(e) = crate::logic::validate_password(&payload.password, &user.pass_hash) { log::error!("Password validation failed. {}", e); Err(routes::Error::Unauthorized) @@ -143,27 +134,17 @@ async fn register( } }; - match db - .send(database::CreateAccount { + query_db!( + db, + database_manager::CreateAccount { email: input.email, login: input.login, pass_hash: PassHash::from(hash), role: input.role, - }) - .await - { - Ok(Ok(account)) => { - response.account = Some(account.into()); - } - Ok(Err(e)) => { - log::error!("{}", e); - return Err(super::Error::Admin(Error::Register)); - } - Err(e) => { - log::error!("{}", e); - return Err(super::Error::Admin(Error::Register)); - } - }; + }, + super::Error::Admin(Error::Register), + super::Error::Admin(Error::Register) + ); response.success = response.errors.is_empty(); Ok(if response.success { diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs index fc3f4c6..4696633 100644 --- a/api/src/routes/admin/api_v1/accounts.rs +++ b/api/src/routes/admin/api_v1/accounts.rs @@ -2,10 +2,10 @@ use actix::Addr; use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{get, patch, post, HttpResponse}; +use config::SharedAppConfig; +use database_manager::Database; +use model::{AccountId, AccountState, PasswordConfirmation}; -use crate::config::SharedAppConfig; -use crate::database::{self, Database}; -use crate::model::{AccountId, AccountState, PasswordConfirmation}; use crate::routes::admin::Error; use crate::routes::RequireLogin; use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Password, Role}; @@ -13,7 +13,7 @@ use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Pas #[get("/accounts")] pub async fn accounts(session: Session, db: Data>) -> routes::Result { session.require_admin()?; - let accounts = admin_send_db!(db, database::AllAccounts); + let accounts = admin_send_db!(db, database_manager::AllAccounts); Ok(HttpResponse::Ok().json(accounts)) } @@ -63,7 +63,7 @@ pub async fn update_account( let account = admin_send_db!( db, - database::UpdateAccount { + database_manager::UpdateAccount { id: payload.id, email: payload.email, login: payload.login, @@ -107,7 +107,7 @@ pub async fn create_account( let account = admin_send_db!( db, - database::CreateAccount { + database_manager::CreateAccount { email: payload.email, login: payload.login, pass_hash: PassHash::from(hash), diff --git a/api/src/routes/admin/api_v1/orders.rs b/api/src/routes/admin/api_v1/orders.rs index a7299f0..fd95368 100644 --- a/api/src/routes/admin/api_v1/orders.rs +++ b/api/src/routes/admin/api_v1/orders.rs @@ -2,19 +2,19 @@ use actix::Addr; use actix_session::Session; use actix_web::get; use actix_web::web::{Data, Json, ServiceConfig}; +use database_manager::Database; +use model::api::AccountOrders; -use crate::database::Database; -use crate::model::api::AccountOrders; use crate::routes::admin::Error; use crate::routes::RequireLogin; -use crate::{admin_send_db, database, model, routes}; +use crate::{admin_send_db, routes}; #[get("/orders")] async fn orders(session: Session, db: Data>) -> routes::Result> { session.require_admin()?; - let orders: Vec = admin_send_db!(&db, database::AllAccountOrders); - let items: Vec = admin_send_db!(db, database::AllOrderItems); + let orders: Vec = admin_send_db!(&db, database_manager::AllAccountOrders); + let items: Vec = admin_send_db!(db, database_manager::AllOrderItems); Ok(Json((orders, items).into())) } diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs index f087180..2654c72 100644 --- a/api/src/routes/admin/api_v1/products.rs +++ b/api/src/routes/admin/api_v1/products.rs @@ -2,22 +2,23 @@ use actix::Addr; use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{delete, get, patch, post, HttpResponse}; -use serde::Deserialize; - -use crate::database::Database; -use crate::model::{ +use database_manager::Database; +use model::{ Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc, Quantity, QuantityUnit, }; +use search_manager::SearchManager; +use serde::Deserialize; + use crate::routes::admin::Error; use crate::routes::RequireLogin; -use crate::{admin_send_db, database, routes}; +use crate::{admin_send_db, routes}; #[get("/products")] async fn products(session: Session, db: Data>) -> routes::Result { session.require_admin()?; - let products = admin_send_db!(db, database::AllProducts); + let products = admin_send_db!(db, database_manager::AllProducts); Ok(HttpResponse::Ok().json(products)) } @@ -42,7 +43,7 @@ async fn update_product( let product = admin_send_db!( db, - database::UpdateProduct { + database_manager::UpdateProduct { id: payload.id, name: payload.name, short_description: payload.short_description, @@ -63,19 +64,21 @@ pub struct CreateProduct { pub category: Option, pub price: Price, pub deliver_days_flag: Days, + pub lang: String, } #[post("/product")] async fn create_product( session: Session, db: Data>, + search: Data>, Json(payload): Json, ) -> routes::Result { session.require_admin()?; - let product = admin_send_db!( + let product: model::Product = admin_send_db!( db.clone(), - database::CreateProduct { + database_manager::CreateProduct { name: payload.name, short_description: payload.short_description, long_description: payload.long_description, @@ -84,9 +87,17 @@ async fn create_product( deliver_days_flag: payload.deliver_days_flag, } ); + + search.do_send(search_manager::CreateIndex { + key: format!("{}", product.id), + value: product.long_description.to_string(), + collection: "products".into(), + lang: payload.lang, + }); + let _ = admin_send_db!( db, - database::CreateStock { + database_manager::CreateStock { product_id: product.id, quantity: Quantity::try_from(0).unwrap_or_default(), quantity_unit: QuantityUnit::Piece, @@ -110,7 +121,7 @@ async fn delete_product( let product = admin_send_db!( db, - database::DeleteProduct { + database_manager::DeleteProduct { product_id: payload.id } ); diff --git a/api/src/routes/admin/api_v1/stocks.rs b/api/src/routes/admin/api_v1/stocks.rs index 1af40cc..22f9b56 100644 --- a/api/src/routes/admin/api_v1/stocks.rs +++ b/api/src/routes/admin/api_v1/stocks.rs @@ -2,19 +2,19 @@ use actix::Addr; use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{delete, get, patch, post, HttpResponse}; +use database_manager::Database; +use model::{ProductId, Quantity, QuantityUnit, StockId}; use serde::Deserialize; -use crate::database::Database; -use crate::model::{ProductId, Quantity, QuantityUnit, StockId}; use crate::routes::admin::Error; use crate::routes::RequireLogin; -use crate::{admin_send_db, database, routes}; +use crate::{admin_send_db, routes}; #[get("/stocks")] async fn stocks(session: Session, db: Data>) -> routes::Result { session.require_admin()?; - let stocks = admin_send_db!(db, database::AllStocks); + let stocks = admin_send_db!(db, database_manager::AllStocks); Ok(HttpResponse::Created().json(stocks)) } @@ -36,7 +36,7 @@ async fn update_stock( let stock = admin_send_db!( db, - database::UpdateStock { + database_manager::UpdateStock { id: payload.id, product_id: payload.product_id, quantity: payload.quantity, @@ -63,7 +63,7 @@ async fn create_stock( let stock = admin_send_db!( db, - database::CreateStock { + database_manager::CreateStock { product_id: payload.product_id, quantity: payload.quantity, quantity_unit: payload.quantity_unit @@ -87,7 +87,7 @@ async fn delete_stock( let stock = admin_send_db!( db, - database::DeleteStock { + database_manager::DeleteStock { stock_id: payload.id } ); diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index 8251ce7..ff28d19 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -9,13 +9,13 @@ 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 serde::Serialize; +use token_manager::TokenManager; pub use self::admin::Error as AdminError; pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError}; -use crate::model::{RecordId, Token, TokenString}; -use crate::token_manager::TokenManager; -use crate::{routes, token_manager}; +use crate::routes; pub trait RequireLogin { fn require_admin(&self) -> Result; diff --git a/api/src/routes/public.rs b/api/src/routes/public.rs index fc06602..4b2ce77 100644 --- a/api/src/routes/public.rs +++ b/api/src/routes/public.rs @@ -4,8 +4,6 @@ use actix_web::web::ServiceConfig; use actix_web::{get, HttpResponse}; pub use api_v1::{Error as V1Error, ShoppingCartError as V1ShoppingCartError}; -use crate::database; - #[macro_export] macro_rules! public_send_db { ($db: expr, $msg: expr) => {{ @@ -35,7 +33,7 @@ pub enum Error { #[error("Internal server error")] DatabaseConnection, #[error("{0}")] - Database(#[from] database::Error), + Database(#[from] database_manager::Error), } #[get("/")] diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index f8b3767..eaf7680 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -2,17 +2,16 @@ 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 database_manager::{query_db, Database}; +use model::{api, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartItemId}; +use payment_manager::{query_pay, PaymentManager}; +use token_manager::TokenManager; -use crate::actors::cart_manager; -use crate::actors::cart_manager::CartManager; -use crate::database::Database; -use crate::model::{api, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartItemId}; -use crate::payment_manager::PaymentManager; +use crate::routes; use crate::routes::public::api_v1::{Error as ApiV1Error, ShoppingCartError}; use crate::routes::public::Error as PublicError; use crate::routes::{RequireUser, Result}; -use crate::token_manager::TokenManager; -use crate::{database, model, payment_manager, query_db, query_pay, routes}; #[get("/shopping-cart")] async fn shopping_cart( @@ -23,7 +22,7 @@ async fn shopping_cart( let (token, _) = credentials.require_user(tm.into_inner()).await?; let cart: model::ShoppingCart = query_db!( db, - database::EnsureActiveShoppingCart { + database_manager::EnsureActiveShoppingCart { buyer_id: AccountId::from(token.subject), }, routes::Error::Public(PublicError::ApiV1(ApiV1Error::ShoppingCart( @@ -32,7 +31,7 @@ async fn shopping_cart( ); let items: Vec = query_db!( db, - database::AccountShoppingCartItems { + database_manager::AccountShoppingCartItems { account_id: cart.buyer_id, shopping_cart_id: Some(cart.id), }, @@ -110,22 +109,14 @@ async fn delete_cart_item( ) -> Result { let (token, _) = credentials.require_user(tm.into_inner()).await?; - let sc: model::ShoppingCart = match db - .send(database::EnsureActiveShoppingCart { + let sc: model::ShoppingCart = query_db!( + db, + database_manager::EnsureActiveShoppingCart { buyer_id: AccountId::from(token.subject), - }) - .await - { - Ok(Ok(cart)) => cart, - Ok(Err(e)) => { - log::error!("{e:}"); - return Err(routes::Error::Public(super::Error::RemoveItem.into())); - } - Err(e) => { - log::error!("{e:?}"); - return Err(routes::Error::Public(PublicError::DatabaseConnection)); - } - }; + }, + routes::Error::Public(super::Error::RemoveItem.into()), + routes::Error::Public(PublicError::DatabaseConnection) + ); match cart .into_inner() diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index 74f7f1c..5c7d92d 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -1,24 +1,24 @@ use actix::Addr; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{get, post, HttpResponse}; +use database_manager::{query_db, Database}; +use model::{Audience, FullAccount, Token, TokenString}; +use payment_manager::{PaymentManager, PaymentNotification}; +use token_manager::TokenManager; -use crate::database::{self, Database}; use crate::logic::validate_password; -use crate::model::{Audience, FullAccount, Token, TokenString}; -use crate::payment_manager::{PaymentManager, PaymentNotification}; use crate::routes::public::Error as PublicError; use crate::routes::{self, Result}; -use crate::token_manager::TokenManager; -use crate::{payment_manager, public_send_db, token_manager, Login, Password}; +use crate::{public_send_db, Login, Password}; #[get("/products")] async fn products(db: Data>) -> Result { - public_send_db!(db.into_inner(), database::AllProducts) + public_send_db!(db.into_inner(), database_manager::AllProducts) } #[get("/stocks")] async fn stocks(db: Data>) -> Result { - public_send_db!(db.into_inner(), database::AllStocks) + public_send_db!(db.into_inner(), database_manager::AllStocks) } #[derive(serde::Deserialize)] @@ -41,23 +41,15 @@ async fn sign_in( let db = db.into_inner(); let tm = tm.into_inner(); - let account: FullAccount = match db - .send(database::AccountByIdentity { + let account: FullAccount = query_db!( + db, + database_manager::AccountByIdentity { login: Some(Login::from(payload.login)), email: None, - }) - .await - { - Ok(Ok(account)) => account, - Ok(Err(db_err)) => { - log::error!("{db_err}"); - return Err(routes::Error::Public(PublicError::DatabaseConnection)); - } - Err(db_err) => { - log::error!("{db_err}"); - return Err(routes::Error::Public(PublicError::DatabaseConnection)); - } - }; + }, + routes::Error::Public(PublicError::DatabaseConnection), + routes::Error::Public(PublicError::DatabaseConnection) + ); if validate_password(&Password::from(payload.password), &account.pass_hash).is_err() { return Err(routes::Error::Unauthorized); } diff --git a/shared/config/Cargo.toml b/shared/config/Cargo.toml new file mode 100644 index 0000000..3bb4a83 --- /dev/null +++ b/shared/config/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "config" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = [] } +toml = { version = "0.5", features = [] } + +parking_lot = { version = "0.12", features = [] } + +password-hash = { version = "0.4", features = ["alloc"] } + +pay_u = { version = '0.1', features = ["single-client"] } + +actix-web = { version = "4.0", features = [] } + +log = { version = "0.4" } + +thiserror = { version = "1.0" } diff --git a/api/src/config.rs b/shared/config/src/lib.rs similarity index 90% rename from api/src/config.rs rename to shared/config/src/lib.rs index ba7ea82..98aaa5e 100644 --- a/api/src/config.rs +++ b/shared/config/src/lib.rs @@ -4,7 +4,14 @@ use parking_lot::Mutex; use password_hash::SaltString; use serde::{Deserialize, Serialize}; -use crate::UpdateConfig; +#[derive(Debug, thiserror::Error)] +pub enum Error {} + +pub type Result = std::result::Result; + +pub trait UpdateConfig { + fn update_config(&self, config: &mut AppConfig); +} trait Example: Sized { fn example() -> Self; @@ -343,6 +350,48 @@ impl SearchConfig { } } +#[derive(Serialize, Deserialize)] +pub struct FilesConfig { + public_path: Option, + local_path: Option, +} + +impl Example for FilesConfig { + fn example() -> Self { + Self { + public_path: Some(String::from("/uploads")), + local_path: Some(String::from("/var/local/bazzar")), + } + } +} + +impl Default for FilesConfig { + fn default() -> Self { + Self { + public_path: Some(String::from("/uploads")), + local_path: Some(String::from("/var/local/bazzar")), + } + } +} + +impl FilesConfig { + pub fn public_path(&self) -> String { + self.public_path + .as_ref() + .cloned() + .or_else(|| std::env::var("FILES_PUBLIC_PATH").ok()) + .unwrap_or_else(|| String::from("/uploads")) + } + + pub fn local_path(&self) -> String { + self.local_path + .as_ref() + .cloned() + .or_else(|| std::env::var("FILES_LOCAL_PATH").ok()) + .unwrap_or_else(|| String::from("/var/local/bazzar")) + } +} + #[derive(Serialize, Deserialize)] pub struct AppConfig { payment: PaymentConfig, @@ -350,6 +399,7 @@ pub struct AppConfig { mail: MailConfig, database: DatabaseConfig, search: SearchConfig, + files: FilesConfig, #[serde(skip)] config_path: String, } @@ -362,6 +412,7 @@ impl Example for AppConfig { mail: MailConfig::example(), database: DatabaseConfig::example(), search: SearchConfig::example(), + files: FilesConfig::example(), config_path: "".to_string(), } } @@ -403,6 +454,10 @@ impl AppConfig { pub fn search(&self) -> &SearchConfig { &self.search } + + pub fn files(&self) -> &FilesConfig { + &self.files + } } impl Default for AppConfig { @@ -413,6 +468,7 @@ impl Default for AppConfig { mail: Default::default(), database: DatabaseConfig::default(), search: Default::default(), + files: FilesConfig::default(), config_path: "".to_string(), } } @@ -444,27 +500,35 @@ fn load(config_path: &str, opts: &impl UpdateConfig) -> SharedAppConfig { } } -pub async fn config_info() -> crate::Result<()> { +pub async fn config_info() -> Result<()> { println!( r#"Environment variables: + PAYU_CLIENT_ID - PayU client id, you can obtain it by creating account (account requires one-time payment) PAYU_CLIENT_SECRET - PayU client secret PAYU_CLIENT_MERCHANT_ID - PayU client merchant id, you can obtain it by creating account (account requires one-time payment) + WEB_HOST - your domain name, it's required for PayU notifications, service emails and redirections PASS_SALT - password encryption secret string, you can generate it with this CLI SESSION_SECRET - 100 characters admin session encryption JWT_SECRET - 100 characters user session encryption BAZZAR_BIND - address to which server should be bind, typically 0.0.0.0 BAZZAR_PORT - port which server should use, typically 80 + SENDGRID_SECRET - e-mail sending service secret SENDGRID_API_KEY - e-mail sending service api key SMTP_FROM - e-mail sending service authorized e-mail address used as sender e-mail address + DATABASE_URL - postgresql address (ex. postgres://postgres@localhost/bazzar) + SONIC_SEARCH_ADDR - search engine query address SONIC_SEARCH_PASS - search engine query password SONIC_INGEST_ADDR - search engine push address SONIC_INGEST_PASS - search engine push password SEARCH_ACTIVE - should use search engine + +FILES_PUBLIC_PATH - path to files in browser +FILES_LOCAL_PATH - path where files are saved on server "# ); diff --git a/shared/model/Cargo.toml b/shared/model/Cargo.toml new file mode 100644 index 0000000..52406d4 --- /dev/null +++ b/shared/model/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "model" +version = "0.1.0" +edition = "2021" + +[features] +db = ["sqlx", "sqlx-core"] + +[dependencies] +serde = { version = "1.0.137" } + +sqlx = { version = "0.5", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"], optional = true } +sqlx-core = { version = "0.5", features = [], optional = true } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +derive_more = { version = "0.99.17" } + +thiserror = { version = "1.0.31" } + +validator = { version = "0.15.0" } diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs new file mode 100644 index 0000000..d13e330 --- /dev/null +++ b/shared/model/src/api.rs @@ -0,0 +1,123 @@ +use serde::Serialize; + +#[derive(Serialize, Debug)] +#[serde(transparent)] +pub struct AccountOrders(pub Vec); + +impl From<(Vec, Vec)> for AccountOrders { + fn from((orders, mut items): (Vec, Vec)) -> Self { + Self( + orders + .into_iter() + .map( + |crate::AccountOrder { + id, + buyer_id, + status, + order_id, + order_ext_id: _, + service_order_id: _, + }| { + AccountOrder { + id, + buyer_id, + status, + order_id, + items: items.drain_filter(|item| item.order_id == id).collect(), + } + }, + ) + .collect(), + ) + } +} + +impl From<(crate::AccountOrder, Vec)> for AccountOrder { + fn from( + ( + crate::AccountOrder { + id, + buyer_id, + status, + order_id, + order_ext_id: _, + service_order_id: _, + }, + mut items, + ): (crate::AccountOrder, Vec), + ) -> Self { + AccountOrder { + id, + buyer_id, + status, + order_id, + items: items.drain_filter(|item| item.order_id == id).collect(), + } + } +} + +#[derive(Serialize, Debug)] +pub struct AccountOrder { + pub id: crate::AccountOrderId, + pub buyer_id: crate::AccountId, + pub status: crate::OrderStatus, + pub order_id: Option, + pub items: Vec, +} + +#[derive(Serialize, 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, +} + +#[derive(Serialize, Debug)] +pub struct ShoppingCart { + pub id: crate::ShoppingCartId, + pub buyer_id: crate::AccountId, + pub payment_method: crate::PaymentMethod, + pub state: crate::ShoppingCartState, + pub items: Vec, +} + +impl From<(crate::ShoppingCart, Vec)> for ShoppingCart { + fn from( + ( + crate::ShoppingCart { + id, + buyer_id, + payment_method, + state, + }, + items, + ): (crate::ShoppingCart, Vec), + ) -> Self { + Self { + id, + buyer_id, + payment_method, + state, + items: items + .into_iter() + .map( + |crate::ShoppingCartItem { + id, + product_id, + shopping_cart_id, + quantity, + quantity_unit, + }| ShoppingCartItem { + id, + product_id, + shopping_cart_id, + quantity, + quantity_unit, + }, + ) + .collect(), + } + } +} diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs new file mode 100644 index 0000000..3124989 --- /dev/null +++ b/shared/model/src/lib.rs @@ -0,0 +1,682 @@ +#![feature(drain_filter)] + +pub mod api; + +use std::fmt::Formatter; +use std::str::FromStr; + +use derive_more::{Deref, Display, From}; +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum TransformError { + #[error("Given value is below minimal value")] + BelowMinimal, + #[error("Given value is not valid day flag")] + NotDay, + #[error("Given value is not valid email address")] + NotEmail, +} + +pub type RecordId = i32; + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderStatus { + #[display(fmt = "Potwierdzone")] + Confirmed, + #[display(fmt = "Odebrane")] + Delivered, + #[display(fmt = "Opłacone")] + Payed, + #[display(fmt = "Anulowane")] + Cancelled, + #[display(fmt = "Wymaga zwrotu płatności")] + RequireRefund, + #[display(fmt = "Płatność zwrócona")] + Refunded, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Role { + #[display(fmt = "Adminitrator")] + Admin, + #[display(fmt = "Użytkownik")] + User, +} + +impl PartialEq<&str> for Role { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl Role { + pub fn as_str(&self) -> &str { + match self { + Role::Admin => "Admin", + Role::User => "User", + } + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum QuantityUnit { + #[cfg_attr(feature = "db", sqlx(rename = "g"))] + Gram, + #[cfg_attr(feature = "db", sqlx(rename = "dkg"))] + Decagram, + #[cfg_attr(feature = "db", sqlx(rename = "kg"))] + Kilogram, + #[cfg_attr(feature = "db", sqlx(rename = "piece"))] + Piece, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentMethod { + PayU, + PaymentOnTheSpot, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ShoppingCartState { + Active, + Closed, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Audience { + Web, + Mobile, + Feed, + AdminPanel, +} + +impl PartialEq<&str> for Audience { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl Audience { + pub fn as_str(&self) -> &str { + match self { + Audience::Web => "Web", + Audience::Mobile => "Mobile", + Audience::Feed => "Feed", + Audience::AdminPanel => "AdminPanel", + } + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] +#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AccountState { + Active, + Suspended, + Banned, +} + +impl Default for Audience { + fn default() -> Self { + Self::Web + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] +#[serde(transparent)] +pub struct Price(NonNegative); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] +#[serde(transparent)] +pub struct Quantity(NonNegative); + +impl TryFrom for Quantity { + type Error = TransformError; + + fn try_from(value: i32) -> Result { + Ok(Self(value.try_into()?)) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Deserialize, Serialize, Debug, Deref, From, Display)] +#[serde(transparent)] +pub struct Login(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Debug, Deref, From, Display)] +#[serde(transparent)] +pub struct Email(String); + +impl FromStr for Email { + type Err = TransformError; + + fn from_str(s: &str) -> Result { + if validator::validate_email(s) { + Ok(Self(String::from(s))) + } else { + Err(TransformError::NotEmail) + } + } +} + +impl<'de> serde::Deserialize<'de> for Email { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EmailVisitor {} + impl<'v> Visitor<'v> for EmailVisitor { + type Value = String; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("valid e-mail address") + } + + fn visit_str(self, s: &str) -> Result + where + E: Error, + { + if validator::validate_email(s) { + Ok(String::from(s)) + } else { + Err(E::custom("this is not email address")) + } + } + } + + Ok(Email(deserializer.deserialize_str(EmailVisitor {})?)) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Default, Debug, Copy, Clone, Deref, Display)] +#[serde(transparent)] +pub struct NonNegative(i32); + +impl TryFrom for NonNegative { + type Error = TransformError; + + fn try_from(value: i32) -> Result { + if value < 0 { + Err(TransformError::BelowMinimal) + } else { + Ok(Self(value)) + } + } +} + +impl<'de> serde::Deserialize<'de> for NonNegative { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct NonNegativeVisitor; + impl<'v> Visitor<'v> for NonNegativeVisitor { + type Value = i32; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("value equal or greater than 0") + } + + fn visit_i32(self, v: i32) -> Result + where + E: Error, + { + if v >= 0 { + Ok(v) + } else { + Err(E::custom("Value must be equal or greater than 0")) + } + } + + fn visit_i64(self, v: i64) -> Result + where + E: Error, + { + let v = v + .try_into() + .map_err(|_| E::custom("Value must be equal or greater than 0"))?; + if v >= 0 { + Ok(v) + } else { + Err(E::custom("Value must be equal or greater than 0")) + } + } + + fn visit_u32(self, v: u32) -> Result + where + E: Error, + { + let v = v + .try_into() + .map_err(|_| E::custom("Value must be equal or greater than 0"))?; + Ok(v) + } + + fn visit_u64(self, v: u64) -> Result + where + E: Error, + { + let v = v + .try_into() + .map_err(|_| E::custom("Value must be equal or greater than 0"))?; + Ok(v) + } + } + + Ok(NonNegative( + deserializer.deserialize_i32(NonNegativeVisitor)?, + )) + } +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Display, From)] +#[serde(rename_all = "lowercase")] +pub enum Day { + Monday = 1 << 0, + Tuesday = 1 << 1, + Wednesday = 1 << 2, + Thursday = 1 << 3, + Friday = 1 << 4, + Saturday = 1 << 5, + Sunday = 1 << 6, +} + +impl TryFrom for Day { + type Error = TransformError; + + fn try_from(value: i32) -> Result { + if value == (Day::Monday as i32) { + Ok(Day::Monday) + } else if value == (Day::Tuesday as i32) { + Ok(Day::Tuesday) + } else if value == (Day::Wednesday as i32) { + Ok(Day::Wednesday) + } else if value == (Day::Thursday as i32) { + Ok(Day::Thursday) + } else if value == (Day::Friday as i32) { + Ok(Day::Friday) + } else if value == (Day::Saturday as i32) { + Ok(Day::Saturday) + } else if value == (Day::Sunday as i32) { + Ok(Day::Sunday) + } else { + Err(TransformError::NotDay) + } + } +} + +#[derive(Serialize, Deserialize, Deref, Debug)] +#[serde(transparent)] +pub struct Days(Vec); + +#[cfg(feature = "db")] +impl<'q> ::sqlx::encode::Encode<'q, sqlx::Postgres> for Days +where + i32: ::sqlx::encode::Encode<'q, sqlx::Postgres>, +{ + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> ::sqlx::encode::IsNull { + let value = self.0.iter().fold(1, |memo, v| memo | *v as i32); + + >::encode_by_ref(&value, buf) + } + + fn size_hint(&self) -> usize { + >::size_hint(&Default::default()) + } +} + +#[cfg(feature = "db")] +impl<'r> ::sqlx::decode::Decode<'r, sqlx::Postgres> for Days +where + i32: ::sqlx::decode::Decode<'r, sqlx::Postgres>, +{ + fn decode( + value: >::ValueRef, + ) -> ::std::result::Result< + Self, + ::std::boxed::Box< + dyn ::std::error::Error + 'static + ::std::marker::Send + ::std::marker::Sync, + >, + > { + let value = >::decode(value)?; + Ok(Days( + (0..9) + .into_iter() + .filter_map(|n| { + eprintln!( + "d {} {} {} {:?}", + n, + 1 << n, + value & 1 << n, + Day::try_from(value & 1 << n).ok() + ); + Day::try_from(value & 1 << n).ok() + }) + .collect(), + )) + } +} + +#[cfg(feature = "db")] +impl sqlx::Type for Days +where + i32: ::sqlx::Type, +{ + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Deref, From, Display)] +#[serde(transparent)] +pub struct Password(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Deref, From, Display)] +#[serde(transparent)] +pub struct PasswordConfirmation(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Deref, From, Display)] +#[serde(transparent)] +pub struct PassHash(String); + +impl PartialEq for Password { + fn eq(&self, other: &PasswordConfirmation) -> bool { + self.0 == other.0 + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Deref, Display, From)] +#[serde(transparent)] +pub struct AccountId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct FullAccount { + pub id: AccountId, + pub email: Email, + pub login: Login, + pub pass_hash: PassHash, + pub role: Role, + pub customer_id: uuid::Uuid, + pub state: AccountState, +} + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct Account { + pub id: AccountId, + pub email: Email, + pub login: Login, + pub role: Role, + pub customer_id: uuid::Uuid, + pub state: AccountState, +} + +impl From for Account { + fn from( + FullAccount { + id, + email, + login, + pass_hash: _, + role, + customer_id, + state, + }: FullAccount, + ) -> Self { + Self { + id, + email, + login, + role, + customer_id, + state, + } + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Deref, Display, From)] +#[serde(transparent)] +pub struct ProductId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[serde(transparent)] +pub struct ProductName(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[serde(transparent)] +pub struct ProductShortDesc(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[serde(transparent)] +pub struct ProductLongDesc(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[serde(transparent)] +pub struct ProductCategory(String); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct Product { + pub id: ProductId, + pub name: ProductName, + pub short_description: ProductShortDesc, + pub long_description: ProductLongDesc, + pub category: Option, + pub price: Price, + pub deliver_days_flag: Days, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct StockId(pub RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct Stock { + pub id: StockId, + pub product_id: ProductId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)] +#[serde(transparent)] +pub struct AccountOrderId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Display, Deref)] +#[serde(transparent)] +pub struct OrderId(String); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct AccountOrder { + pub id: AccountOrderId, + pub buyer_id: AccountId, + pub status: OrderStatus, + pub order_id: Option, + pub order_ext_id: uuid::Uuid, + pub service_order_id: Option, +} + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct PublicAccountOrder { + pub id: AccountOrderId, + pub buyer_id: AccountId, + pub status: OrderStatus, + pub order_id: Option, +} + +impl From for PublicAccountOrder { + fn from( + AccountOrder { + id, + buyer_id, + status, + order_id, + order_ext_id: _, + service_order_id: _, + }: AccountOrder, + ) -> Self { + Self { + id, + buyer_id, + status, + order_id, + } + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Deref)] +pub struct OrderItemId(pub RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize, Debug)] +pub struct OrderItem { + pub id: OrderItemId, + pub product_id: ProductId, + pub order_id: AccountOrderId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Deref, Display)] +#[serde(transparent)] +pub struct ShoppingCartId(pub RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct ShoppingCart { + pub id: ShoppingCartId, + pub buyer_id: AccountId, + pub payment_method: PaymentMethod, + pub state: ShoppingCartState, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Deref, Display)] +#[serde(transparent)] +pub struct ShoppingCartItemId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct ShoppingCartItem { + pub id: ShoppingCartId, + pub product_id: ProductId, + pub shopping_cart_id: ShoppingCartId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +impl ShoppingCartItem { + pub fn quantity(&self) -> Quantity { + self.quantity + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)] +#[serde(transparent)] +pub struct TokenId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize)] +pub struct Token { + pub id: TokenId, + pub customer_id: uuid::Uuid, + pub role: Role, + /// iss (issuer): Issuer of the JWT + pub issuer: String, + /// sub (subject): Subject of the JWT (the user) + pub subject: i32, + /// aud (audience): Recipient for which the JWT is intended + pub audience: Audience, + /// exp (expiration time): Time after which the JWT expires + pub expiration_time: chrono::NaiveDateTime, + /// nbt (not before time): Time before which the JWT must not be accepted + /// for processing + pub not_before_time: chrono::NaiveDateTime, + /// iat (issued at time): Time at which the JWT was issued; can be used to + /// determine age of the JWT, + pub issued_at_time: chrono::NaiveDateTime, + /// jti (JWT ID): Unique identifier; can be used to prevent the JWT from + /// being replayed (allows a token to be used only once) + pub jwt_id: uuid::Uuid, +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +pub struct TokenString(String); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +pub struct FileName(String);