Add working search ingest

This commit is contained in:
Adrian Woźniak 2022-05-06 11:47:18 +02:00
parent 4740aedb73
commit 1b75a6a1ac
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
46 changed files with 1856 additions and 426 deletions

222
Cargo.lock generated
View File

@ -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"

View File

@ -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",
]

View File

@ -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 = [] }

View File

@ -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<T> = std::result::Result<T, Error>;
pub struct CartManager {
db: Addr<Database>,
db: actix::Addr<Database>,
}
impl Actor for CartManager {
type Context = Context<Self>;
impl actix::Actor for CartManager {
type Context = actix::Context<Self>;
}
impl CartManager {
pub fn new(db: Addr<Database>) -> Self {
pub fn new(db: actix::Addr<Database>) -> Self {
Self { db }
}
}
#[derive(Message)]
#[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCartItem>")]
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<Database>) -> Result<ShoppingCartItem> {
async fn add_item(msg: AddItem, db: actix::Addr<Database>) -> Result<ShoppingCartItem> {
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<Database>) -> Result<ShoppingCartItem>
_ => 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<Database>) -> Result<ShoppingCartItem>
_ => 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<Database>) -> Result<ShoppingCartItem>
}
}
#[derive(Message)]
#[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct RemoveProduct {
pub shopping_cart_id: ShoppingCartId,
@ -119,10 +117,10 @@ cart_async_handler!(RemoveProduct, remove_product, Option<ShoppingCartItem>);
pub(crate) async fn remove_product(
msg: RemoveProduct,
db: Addr<Database>,
db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
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<Option<ShoppingCartItem>>")]
pub struct ChangeQuantity {
pub shopping_cart_id: ShoppingCartId,
@ -154,7 +152,7 @@ cart_async_handler!(ChangeQuantity, change_quantity, Option<ShoppingCartItem>);
pub(crate) async fn change_quantity(
msg: ChangeQuantity,
db: Addr<Database>,
db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
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
)))
}

View File

@ -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 = [] }

View File

@ -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,

View File

@ -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 {

View File

@ -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<Self> {
pub async fn build(config: SharedAppConfig) -> Result<Self> {
let url = config.lock().database().url();
let pool = sqlx::PgPool::connect(&url).await.map_err(Error::Connect)?;
Ok(Database { pool })

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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,
})

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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<Token>")]
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)
})
}

View File

@ -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 = [] }

View File

@ -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<SendState>")]
pub struct TestMail {
pub receiver: Email,
pub receiver: model::Email,
}
mail_async_handler!(TestMail, test_mail, SendState);

View File

@ -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"] }

View File

@ -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<Self, Result<$res>>;
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<T> = std::result::Result<T, Error>;
pub struct FsManager {
config: SharedAppConfig,
}
impl actix::Actor for FsManager {
type Context = actix::Context<FsManager>;
}
impl FsManager {
pub async fn build(config: SharedAppConfig) -> Result<Self> {
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<FileName>")]
pub struct WriteFile {
pub file_name: String,
pub stream: tokio::sync::mpsc::UnboundedReceiver<u8>,
}
fs_async_handler!(WriteFile, write_file, FileName);
pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Result<FileName> {
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))
}

View File

@ -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 = [] }

View File

@ -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<AccountOrder> {
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<ShoppingCartItem> = match db
.send(database::AccountShoppingCartItems {
},
Error::ShoppingCart,
Error::DatabaseInternal
);
let items: Vec<ShoppingCartItem> = 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)
}

View File

@ -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"] }

View File

@ -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<model::ShoppingCartItem> = 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<model::Product> = 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,
})

View File

@ -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"] }

View File

@ -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<Self, Result<Option<$res>>>;
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<T> = std::result::Result<T, Error>;
#[derive(Clone)]
pub struct Channels {
search: Arc<Mutex<sonic_channel::SearchChannel>>,
ingest: Arc<Mutex<sonic_channel::IngestChannel>>,
}
#[derive(Clone)]
pub struct SearchManager {
channels: Option<Channels>,
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<Self>;
}
#[derive(actix::Message)]
#[rtype(result = "Result<Option<Vec<String>>>")]
pub struct Search {
pub query: String,
pub collection: String,
pub lang: String,
}
search_async_handler!(Search, search, Vec<String>);
pub(crate) async fn search(
msg: Search,
channels: Channels,
_config: SharedAppConfig,
) -> Result<Option<Vec<String>>> {
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<Option<()>>")]
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<Option<()>> {
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(()))
}
}

View File

@ -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 = [] }

View File

@ -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));

View File

@ -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" }

View File

@ -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;

View File

@ -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<Arc<Mutex<Channels>>>,
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<Self>;
}

View File

@ -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;

View File

@ -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<T> = std::result::Result<T, Error>;
@ -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(())
}
}
}

View File

@ -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<String>;

View File

@ -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<HttpResponse> {
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 {

View File

@ -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<Addr<Database>>) -> routes::Result<HttpResponse> {
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),

View File

@ -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<Addr<Database>>) -> routes::Result<Json<AccountOrders>> {
session.require_admin()?;
let orders: Vec<model::AccountOrder> = admin_send_db!(&db, database::AllAccountOrders);
let items: Vec<model::OrderItem> = admin_send_db!(db, database::AllOrderItems);
let orders: Vec<model::AccountOrder> = admin_send_db!(&db, database_manager::AllAccountOrders);
let items: Vec<model::OrderItem> = admin_send_db!(db, database_manager::AllOrderItems);
Ok(Json((orders, items).into()))
}

View File

@ -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<Addr<Database>>) -> routes::Result<HttpResponse> {
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<ProductCategory>,
pub price: Price,
pub deliver_days_flag: Days,
pub lang: String,
}
#[post("/product")]
async fn create_product(
session: Session,
db: Data<Addr<Database>>,
search: Data<Addr<SearchManager>>,
Json(payload): Json<CreateProduct>,
) -> routes::Result<HttpResponse> {
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
}
);

View File

@ -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<Addr<Database>>) -> routes::Result<HttpResponse> {
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
}
);

View File

@ -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<RecordId>;

View File

@ -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("/")]

View File

@ -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<model::ShoppingCartItem> = 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<HttpResponse> {
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()

View File

@ -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<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database::AllProducts)
public_send_db!(db.into_inner(), database_manager::AllProducts)
}
#[get("/stocks")]
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
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);
}

21
shared/config/Cargo.toml Normal file
View File

@ -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" }

View File

@ -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<T> = std::result::Result<T, Error>;
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<String>,
local_path: Option<String>,
}
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
"#
);

22
shared/model/Cargo.toml Normal file
View File

@ -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" }

123
shared/model/src/api.rs Normal file
View File

@ -0,0 +1,123 @@
use serde::Serialize;
#[derive(Serialize, Debug)]
#[serde(transparent)]
pub struct AccountOrders(pub Vec<AccountOrder>);
impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
fn from((orders, mut items): (Vec<crate::AccountOrder>, Vec<crate::OrderItem>)) -> 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<crate::OrderItem>)> for AccountOrder {
fn from(
(
crate::AccountOrder {
id,
buyer_id,
status,
order_id,
order_ext_id: _,
service_order_id: _,
},
mut items,
): (crate::AccountOrder, Vec<crate::OrderItem>),
) -> 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<crate::OrderId>,
pub items: Vec<crate::OrderItem>,
}
#[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<ShoppingCartItem>,
}
impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart {
fn from(
(
crate::ShoppingCart {
id,
buyer_id,
payment_method,
state,
},
items,
): (crate::ShoppingCart, Vec<crate::ShoppingCartItem>),
) -> 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(),
}
}
}

682
shared/model/src/lib.rs Normal file
View File

@ -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<i32> for Quantity {
type Error = TransformError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
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<Self, Self::Err> {
if validator::validate_email(s) {
Ok(Self(String::from(s)))
} else {
Err(TransformError::NotEmail)
}
}
}
impl<'de> serde::Deserialize<'de> for Email {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, s: &str) -> Result<Self::Value, E>
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<i32> for NonNegative {
type Error = TransformError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value < 0 {
Err(TransformError::BelowMinimal)
} else {
Ok(Self(value))
}
}
}
impl<'de> serde::Deserialize<'de> for NonNegative {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: i32) -> Result<Self::Value, E>
where
E: Error,
{
if v >= 0 {
Ok(v)
} else {
Err(E::custom("Value must be equal or greater than 0"))
}
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
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<E>(self, v: u32) -> Result<Self::Value, E>
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<E>(self, v: u64) -> Result<Self::Value, E>
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<i32> for Day {
type Error = TransformError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
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<Day>);
#[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 <sqlx::Postgres as ::sqlx::database::HasArguments<'q>>::ArgumentBuffer,
) -> ::sqlx::encode::IsNull {
let value = self.0.iter().fold(1, |memo, v| memo | *v as i32);
<i32 as ::sqlx::encode::Encode<sqlx::Postgres>>::encode_by_ref(&value, buf)
}
fn size_hint(&self) -> usize {
<i32 as ::sqlx::encode::Encode<sqlx::Postgres>>::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: <sqlx::Postgres as ::sqlx::database::HasValueRef<'r>>::ValueRef,
) -> ::std::result::Result<
Self,
::std::boxed::Box<
dyn ::std::error::Error + 'static + ::std::marker::Send + ::std::marker::Sync,
>,
> {
let value = <i32 as ::sqlx::decode::Decode<'r, sqlx::Postgres>>::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<sqlx::Postgres> for Days
where
i32: ::sqlx::Type<sqlx::Postgres>,
{
fn type_info() -> <sqlx::Postgres as ::sqlx::Database>::TypeInfo {
<i32 as ::sqlx::Type<sqlx::Postgres>>::type_info()
}
fn compatible(ty: &<sqlx::Postgres as ::sqlx::Database>::TypeInfo) -> bool {
<i32 as ::sqlx::Type<sqlx::Postgres>>::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<PasswordConfirmation> 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<FullAccount> 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<ProductCategory>,
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<OrderId>,
pub order_ext_id: uuid::Uuid,
pub service_order_id: Option<String>,
}
#[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<OrderId>,
}
impl From<AccountOrder> 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);