From 9d6534c2a2608beb959c5d86270f93d27874fb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 27 Apr 2022 16:25:57 +0200 Subject: [PATCH] Config --- .env | 4 +- .gitignore | 1 + api/src/actors/payment_manager.rs | 49 +++- api/src/config.rs | 272 ++++++++++++++++++ api/src/main.rs | 12 +- api/src/model.rs | 6 +- .../202204180708_add_payment_fields.sql | 3 - db/migrate/202204271359_add_order_ext_id.sql | 2 + 8 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 api/src/config.rs create mode 100644 db/migrate/202204271359_add_order_ext_id.sql diff --git a/.env b/.env index e7b6789..dda2ee0 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ DATABASE_URL=postgres://postgres@localhost/bazzar PASS_SALT=18CHwV7eGFAea16z+qMKZg RUST_LOG=debug -KEY_SECRET="NEPJs#8jjn8SK8GC7QEC^*P844UgsyEbQB8mRWXkT%3mPrwewZoc25MMby9H#R*w2KzaQgMkk#Pif$kxrLy*N5L!Ch%jxbWoa%gb" +SESSION_SECRET="NEPJs#8jjn8SK8GC7QEC^*P844UgsyEbQB8mRWXkT%3mPrwewZoc25MMby9H#R*w2KzaQgMkk#Pif$kxrLy*N5L!Ch%jxbWoa%gb" JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN" PGDATESTYLE= @@ -12,3 +12,5 @@ SMTP_FROM=adrian.wozniak@ita-prog.pl PAYU_CLIENT_ID="145227" PAYU_CLIENT_SECRET="12f071174cb7eb79d4aac5bc2f07563f" PAYU_CLIENT_MERCHANT_ID=300746 + +WEB_HOST=https://bazzar.ita-prog.pl diff --git a/.gitignore b/.gitignore index ea8c4bf..1adc981 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +bazzar.toml diff --git a/api/src/actors/payment_manager.rs b/api/src/actors/payment_manager.rs index 22a4c4a..dea2688 100644 --- a/api/src/actors/payment_manager.rs +++ b/api/src/actors/payment_manager.rs @@ -1,9 +1,12 @@ use std::sync::Arc; +use actix::Addr; use parking_lot::Mutex; use pay_u::{MerchantPosId, OrderCreateRequest}; -use crate::model::{Price, Quantity}; +use crate::database; +use crate::database::Database; +use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId}; #[macro_export] macro_rules! pay_async_handler { @@ -13,8 +16,9 @@ macro_rules! pay_async_handler { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { use actix::WrapFuture; - let db = self.client.clone(); - Box::pin(async { $async(msg, db).await }.into_actor(self)) + let client = self.client.clone(); + let db = self.db.clone(); + Box::pin(async { $async(msg, client, db).await }.into_actor(self)) } } }; @@ -26,6 +30,8 @@ pub type PayUClient = Arc>; pub enum Error { #[error("{0}")] PayU(#[from] pay_u::Error), + #[error("Failed to create order")] + CreateOrder, } pub type Result = std::result::Result; @@ -33,6 +39,7 @@ pub type Result = std::result::Result; #[derive(Clone)] pub struct PaymentManager { client: PayUClient, + db: Addr, } impl PaymentManager { @@ -40,6 +47,7 @@ impl PaymentManager { client_id: ClientId, client_secret: ClientSecret, merchant_pos_id: MerchantPosId, + db: Addr, ) -> Result where ClientId: Into, @@ -50,6 +58,7 @@ impl PaymentManager { client.authorize().await?; Ok(Self { client: Arc::new(Mutex::new(client)), + db, }) } } @@ -85,8 +94,10 @@ impl From for pay_u::Buyer { #[derive(Debug)] pub struct Product { + pub id: ProductId, pub name: String, pub unit_price: Price, + pub quantity_unit: QuantityUnit, pub quantity: Quantity, } @@ -105,6 +116,8 @@ pub struct RequestPayment { pub description: String, pub buyer: Buyer, pub customer_ip: String, + pub buyer_id: AccountId, + pub shopping_cart_id: ShoppingCartId, } pay_async_handler!(RequestPayment, request_payment, pay_u::OrderId); @@ -112,8 +125,36 @@ pay_async_handler!(RequestPayment, request_payment, pay_u::OrderId); pub(crate) async fn request_payment( msg: RequestPayment, client: PayUClient, + db: Addr, ) -> Result { - let client = &mut *client.lock(); + let db_order = match db + .send(database::CreateAccountOrder { + buyer_id: msg.buyer_id, + items: msg + .products + .iter() + .map(|product| database::create_order::OrderItem { + product_id: product.id, + quantity: product.quantity, + quantity_unit: product.quantity_unit, + }) + .collect(), + shopping_cart_id: msg.shopping_cart_id, + }) + .await + { + Ok(Ok(order)) => order, + Ok(Err(e)) => { + log::error!("{e}"); + return Err(Error::CreateOrder); + } + Err(e) => { + log::error!("{e:?}"); + return Err(Error::CreateOrder); + } + }; + + let mut client = client.lock(); let order = client .create_order( OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) diff --git a/api/src/config.rs b/api/src/config.rs new file mode 100644 index 0000000..7e42c4d --- /dev/null +++ b/api/src/config.rs @@ -0,0 +1,272 @@ +use serde::{Deserialize, Serialize}; + +trait Example: Sized { + fn example() -> Self; +} + +#[derive(Serialize, Deserialize, Default)] +pub struct PaymentConfig { + payu_client_id: Option, + payu_client_secret: Option, + payu_client_merchant_id: Option, +} + +impl Example for PaymentConfig { + fn example() -> Self { + Self { + payu_client_id: Some(pay_u::ClientId::new( + "Create payu account and copy here client_id", + )), + payu_client_secret: Some(pay_u::ClientSecret::new( + "Create payu account and copy here client_secret", + )), + /// "Create payu account and copy here merchant id" + payu_client_merchant_id: Some(pay_u::MerchantPosId::from(0)), + } + } +} + +impl PaymentConfig { + pub fn payu_client_id(&self) -> pay_u::ClientId { + self.payu_client_id + .as_ref() + .cloned() + .or_else(|| std::env::var("PAYU_CLIENT_ID").ok().map(pay_u::ClientId)) + .unwrap_or_else(|| { + panic!("payment config payu_client_id nor PAYU_CLIENT_ID env was given") + }) + } + pub fn payu_client_secret(&self) -> pay_u::ClientSecret { + self.payu_client_secret + .as_ref() + .cloned() + .or_else(|| { + std::env::var("PAYU_CLIENT_SECRET") + .ok() + .map(pay_u::ClientSecret) + }) + .unwrap_or_else(|| { + panic!("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given") + }) + } + pub fn payu_client_merchant_id(&self) -> pay_u::MerchantPosId { + self.payu_client_merchant_id + .or_else(|| std::env::var("PAYU_CLIENT_MERCHANT_ID").ok().and_then(|s| s.parse::().ok()).map(pay_u::MerchantPosId)) + .unwrap_or_else(|| panic!("payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given")) + } +} + +#[derive(Serialize, Deserialize, Default)] +pub struct WebConfig { + /// Host name + /// Example: https://foo.bar + host: Option, + /// Encrypt password salt + pass_salt: Option, + /// Used by redis to save admin session across actors + session_secret: Option, + /// Encrypt JWT + jwt_secret: Option, + bind: Option, + port: Option, +} + +impl Example for WebConfig { + fn example() -> Self { + Self { + host: Some(String::from("https://your.comain.com")), + pass_salt: Some(String::from("Generate it with bazzar generate-hash")), + session_secret: Some(String::from("100 characters long random string")), + jwt_secret: Some(String::from("100 characters long random string")), + bind: Some(String::from("0.0.0.0")), + port: Some(8080), + } + } +} + +impl WebConfig { + pub fn host(&self) -> String { + self.host + .as_ref() + .cloned() + .or_else(|| std::env::var("WEB_HOST").ok()) + .unwrap_or_else(|| panic!("web host config nor WEB_HOST env was not given")) + } + pub fn pass_salt(&self) -> String { + self.pass_salt + .as_ref() + .cloned() + .or_else(|| std::env::var("PASS_SALT").ok()) + .unwrap_or_else(|| panic!("Web config pass_salt nor PASS_SALT env was given")) + } + pub fn session_secret(&self) -> String { + self.session_secret + .as_ref() + .cloned() + .or_else(|| std::env::var("SESSION_SECRET").ok()) + .unwrap_or_else(|| panic!("Web config session_secret nor SESSION_SECRET env was given")) + } + pub fn jwt_secret(&self) -> String { + self.jwt_secret + .as_ref() + .cloned() + .or_else(|| std::env::var("JWT_SECRET").ok()) + .unwrap_or_else(|| panic!("Web config jwt_secret nor JWT_SECRET env was given")) + } + pub fn bind(&self) -> Option { + self.bind + .as_ref() + .cloned() + .or_else(|| std::env::var("BAZZAR_BIND").ok()) + } + pub fn port(&self) -> Option { + self.port.as_ref().copied().or_else(|| { + std::env::var("BAZZAR_PORT") + .ok() + .and_then(|s| s.parse::().ok()) + }) + } +} + +#[derive(Serialize, Deserialize, Default)] +pub struct MailConfig { + sendgrid_secret: Option, + sendgrid_api_key: Option, + smtp_from: Option, +} + +impl Example for MailConfig { + fn example() -> Self { + Self { + sendgrid_secret: Some(String::from( + "Create sendgrid account and copy credentials here", + )), + sendgrid_api_key: Some(String::from( + "Create sendgrid account and copy credentials here", + )), + smtp_from: Some(String::from( + "Valid sendgrid authorized email address. Example: contact@example.com", + )), + } + } +} + +impl MailConfig { + pub fn sendgrid_secret(&self) -> String { + self.sendgrid_secret + .as_ref() + .cloned() + .or_else(|| std::env::var("SENDGRID_SECRET").ok()) + .unwrap_or_else(|| { + panic!("Mail sendgrid_secret config nor SENDGRID_SECRET env was given") + }) + } + pub fn sendgrid_api_key(&self) -> String { + self.sendgrid_api_key + .as_ref() + .cloned() + .or_else(|| std::env::var("SENDGRID_API_KEY").ok()) + .unwrap_or_else(|| { + panic!("Mail sendgrid_api_key config nor SENDGRID_API_KEY env was given") + }) + } + pub fn smtp_from(&self) -> String { + self.smtp_from + .as_ref() + .cloned() + .or_else(|| std::env::var("SMTP_FROM").ok()) + .unwrap_or_else(|| panic!("Mail smtp_from config nor SMTP_FROM env was given")) + } +} + +#[derive(Serialize, Deserialize, Default)] +pub struct DatabaseConfig { + url: Option, +} + +impl Example for DatabaseConfig { + fn example() -> Self { + Self { + url: Some(String::from("postgres://postgres@localhost/bazzar")), + } + } +} + +impl DatabaseConfig { + pub fn url(&self) -> String { + self.url + .as_ref() + .cloned() + .or_else(|| std::env::var("DATABASE_URL").ok()) + .unwrap_or_else(|| panic!("Database url nor DATABASE_URL env was given")) + } +} + +#[derive(Serialize, Deserialize)] +pub struct AppConfig { + payment: PaymentConfig, + web: WebConfig, + mail: MailConfig, + database: DatabaseConfig, + #[serde(skip)] + config_path: String, +} + +impl Example for AppConfig { + fn example() -> Self { + Self { + payment: PaymentConfig::example(), + web: WebConfig::example(), + mail: MailConfig::example(), + database: DatabaseConfig::example(), + config_path: "".to_string(), + } + } +} + +impl AppConfig { + pub fn payment(&self) -> &PaymentConfig { + &self.payment + } + pub fn web(&self) -> &WebConfig { + &self.web + } + pub fn mail(&self) -> &MailConfig { + &self.mail + } + pub fn database(&self) -> &DatabaseConfig { + &self.database + } +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + payment: Default::default(), + web: WebConfig::default(), + mail: Default::default(), + database: DatabaseConfig::default(), + config_path: "".to_string(), + } + } +} + +pub fn load(config_path: &str) -> AppConfig { + match std::fs::read_to_string(config_path) { + Ok(c) => toml::from_str(&c).unwrap(), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + let config = AppConfig::example(); + std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap(); + config + } + Err(e) => { + log::error!("{e:?}"); + panic!("Config file was not found at path {config_path:?}") + } + } +} + +pub fn save(config_path: &str, config: &mut AppConfig) { + config.config_path = String::from(config_path); + std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap(); +} diff --git a/api/src/main.rs b/api/src/main.rs index 8160984..a0466e6 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -20,6 +20,7 @@ use crate::logic::encrypt_password; use crate::model::{Email, Login, PassHash, Password, Role}; pub mod actors; +pub mod config; pub mod logic; pub mod model; pub mod routes; @@ -174,12 +175,14 @@ impl Config { async fn server(opts: ServerOpts) -> Result<()> { let secret_key = { - let key_secret = std::env::var("KEY_SECRET") + let key_secret = std::env::var("SESSION_SECRET") .expect("session requires secret key with 64 or more characters"); Key::from(key_secret.as_bytes()) }; let redis_connection_string = "127.0.0.1:6379"; + let app_config = crate::config::load("./bazzar.toml"); + let config = Arc::new(Config::load()); let db = database::Database::build(&opts.db_url()).await?.start(); let token_manager = token_manager::TokenManager::new(db.clone()).start(); @@ -193,7 +196,7 @@ async fn server(opts: ServerOpts) -> Result<()> { .parse::() .map(MerchantPosId::from) .expect("Variable PAYU_CLIENT_MERCHANT_ID must be number"); - payment_manager::PaymentManager::build(client_id, client_secret, merchant_id) + payment_manager::PaymentManager::build(client_id, client_secret, merchant_id, db.clone()) .await .expect("Failed to start payment manager") .start() @@ -216,7 +219,10 @@ async fn server(opts: ServerOpts) -> Result<()> { .configure(routes::configure) // .default_service(web::to(HttpResponse::Ok)) }) - .bind((opts.bind, opts.port)) + .bind(( + app_config.web().bind().unwrap_or(opts.bind), + app_config.web().port().unwrap_or(opts.port), + )) .map_err(Error::Boot)? .run() .await diff --git a/api/src/model.rs b/api/src/model.rs index 72499be..e482b49 100644 --- a/api/src/model.rs +++ b/api/src/model.rs @@ -131,12 +131,12 @@ impl Default for Audience { } } -#[derive(sqlx::Type, Serialize, Deserialize, Debug, Deref, From)] +#[derive(sqlx::Type, Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] #[sqlx(transparent)] #[serde(transparent)] pub struct Price(NonNegative); -#[derive(sqlx::Type, Serialize, Deserialize, Default, Debug, Deref, From)] +#[derive(sqlx::Type, Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] #[sqlx(transparent)] #[serde(transparent)] pub struct Quantity(NonNegative); @@ -200,7 +200,7 @@ impl<'de> serde::Deserialize<'de> for Email { } } -#[derive(sqlx::Type, Serialize, Default, Debug, Deref, Display)] +#[derive(sqlx::Type, Serialize, Default, Debug, Copy, Clone, Deref, Display)] #[sqlx(transparent)] #[serde(transparent)] pub struct NonNegative(i32); diff --git a/db/migrate/202204180708_add_payment_fields.sql b/db/migrate/202204180708_add_payment_fields.sql index 152293b..3037fdb 100644 --- a/db/migrate/202204180708_add_payment_fields.sql +++ b/db/migrate/202204180708_add_payment_fields.sql @@ -1,5 +1,2 @@ ALTER TABLE accounts ADD COLUMN customer_id uuid not null default gen_random_uuid(); - -ALTER TABLE account_orders -ADD COLUMN order_id varchar unique; diff --git a/db/migrate/202204271359_add_order_ext_id.sql b/db/migrate/202204271359_add_order_ext_id.sql new file mode 100644 index 0000000..c6130f7 --- /dev/null +++ b/db/migrate/202204271359_add_order_ext_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE account_orders +ADD COLUMN order_ext_id uuid not null default uuid_generate_v4();