From 9658abe3b858df39beefdf19c5ebffcc3c8c1e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Thu, 28 Apr 2022 15:54:18 +0200 Subject: [PATCH] Add many additional create order fields --- api/src/actors/database.rs | 7 +- api/src/actors/email_manager.rs | 16 +- api/src/actors/order_manager.rs | 15 +- api/src/actors/payment_manager.rs | 41 ++--- api/src/actors/token_manager.rs | 21 +-- api/src/config.rs | 126 +++++++++++--- api/src/main.rs | 198 ++++----------------- api/src/opts.rs | 222 ++++++++++++++++++++++++ api/src/routes/admin.rs | 9 +- api/src/routes/admin/api_v1/accounts.rs | 15 +- pay_u/src/lib.rs | 204 +++++++++++++++++++++- 11 files changed, 614 insertions(+), 260 deletions(-) create mode 100644 api/src/opts.rs diff --git a/api/src/actors/database.rs b/api/src/actors/database.rs index 1c3f2fe..490c611 100644 --- a/api/src/actors/database.rs +++ b/api/src/actors/database.rs @@ -9,6 +9,8 @@ use sqlx::PgPool; pub use stocks::*; pub use tokens::*; +use crate::config::SharedAppConfig; + pub mod account_orders; pub mod accounts; pub mod order_items; @@ -72,8 +74,9 @@ impl Clone for Database { } impl Database { - pub(crate) async fn build(url: &str) -> Result { - let pool = sqlx::PgPool::connect(url).await.map_err(Error::Connect)?; + pub(crate) async fn build(config: SharedAppConfig) -> Result { + let url = config.lock().database().url(); + let pool = sqlx::PgPool::connect(&url).await.map_err(Error::Connect)?; Ok(Database { pool }) } diff --git a/api/src/actors/email_manager.rs b/api/src/actors/email_manager.rs index d447942..1e9bc4a 100644 --- a/api/src/actors/email_manager.rs +++ b/api/src/actors/email_manager.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::config::SharedAppConfig; use crate::Email; #[macro_export] @@ -29,7 +30,7 @@ pub struct SendState { pub struct EmailManager(Arc); pub(crate) struct Inner { - from: Email, + config: SharedAppConfig, send_grid: sendgrid::SGClient, } @@ -38,14 +39,10 @@ impl actix::Actor for EmailManager { } impl EmailManager { - pub fn build() -> Result { - let from = std::env::var("SMTP_FROM").expect("Missing SMTP_FROM variable"); - + pub fn build(config: SharedAppConfig) -> Result { Ok(Self(Arc::new(Inner { - from: Email::from(from), - send_grid: sendgrid::SGClient::new( - std::env::var("SENDGRID_SECRET").expect("Missing SENDGRID_SECRET variable"), - ), + config: config.clone(), + send_grid: sendgrid::SGClient::new(config.lock().mail().sendgrid_secret()), }))) } } @@ -59,13 +56,12 @@ pub struct TestMail { mail_async_handler!(TestMail, test_mail, SendState); pub(crate) async fn test_mail(msg: TestMail, inner: Arc) -> Result { - let from: &str = &*inner.from; let status = inner .send_grid .send( sendgrid::Mail::new() .add_to((msg.receiver.as_str(), "User").into()) - .add_from(from) + .add_from(&inner.config.lock().mail().smtp_from()) .add_subject("Test e-mail") .add_html("

Test e-mail

") .build(), diff --git a/api/src/actors/order_manager.rs b/api/src/actors/order_manager.rs index e081fdc..6abcd6c 100644 --- a/api/src/actors/order_manager.rs +++ b/api/src/actors/order_manager.rs @@ -1,5 +1,6 @@ use actix::Message; +use crate::config::SharedAppConfig; use crate::database::{self, SharedDatabase}; use crate::model::{ AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem, @@ -14,7 +15,8 @@ macro_rules! order_async_handler { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { use actix::WrapFuture; let db = self.db.clone(); - Box::pin(async { $async(msg, db).await }.into_actor(self)) + let config = self.config.clone(); + Box::pin(async { $async(msg, db, config).await }.into_actor(self)) } } }; @@ -34,6 +36,7 @@ pub type Result = std::result::Result; pub struct OrderManager { db: SharedDatabase, + config: SharedAppConfig, } impl actix::Actor for OrderManager { @@ -41,8 +44,8 @@ impl actix::Actor for OrderManager { } impl OrderManager { - pub fn new(db: SharedDatabase) -> Self { - Self { db } + pub fn new(config: SharedAppConfig, db: SharedDatabase) -> Self { + Self { db, config } } } @@ -55,7 +58,11 @@ pub struct CreateOrder { order_async_handler!(CreateOrder, create_order, AccountOrder); -pub(crate) async fn create_order(msg: CreateOrder, db: SharedDatabase) -> Result { +pub(crate) async fn create_order( + msg: CreateOrder, + db: SharedDatabase, + _config: SharedAppConfig, +) -> Result { let cart: ShoppingCart = match db .send(database::FindShoppingCart { id: msg.shopping_cart_id, diff --git a/api/src/actors/payment_manager.rs b/api/src/actors/payment_manager.rs index dea2688..14b58a8 100644 --- a/api/src/actors/payment_manager.rs +++ b/api/src/actors/payment_manager.rs @@ -2,8 +2,9 @@ use std::sync::Arc; use actix::Addr; use parking_lot::Mutex; -use pay_u::{MerchantPosId, OrderCreateRequest}; +use pay_u::OrderCreateRequest; +use crate::config::SharedAppConfig; use crate::database; use crate::database::Database; use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId}; @@ -43,18 +44,12 @@ pub struct PaymentManager { } impl PaymentManager { - pub async fn build( - client_id: ClientId, - client_secret: ClientSecret, - merchant_pos_id: MerchantPosId, - db: Addr, - ) -> Result - where - ClientId: Into, - ClientSecret: Into, - { - let mut client = - pay_u::Client::new(client_id.into(), client_secret.into(), merchant_pos_id); + pub async fn build(config: SharedAppConfig, db: Addr) -> Result { + let mut client = pay_u::Client::new( + config.lock().payment().payu_client_id(), + config.lock().payment().payu_client_secret(), + config.lock().payment().payu_client_merchant_id(), + ); client.authorize().await?; Ok(Self { client: Arc::new(Mutex::new(client)), @@ -154,14 +149,16 @@ pub(crate) async fn request_payment( } }; - let mut client = client.lock(); - let order = client - .create_order( - OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) - .with_description(msg.description) - .with_notify_url(msg.redirect_uri) - .with_products(msg.products.into_iter().map(Into::into)), - ) - .await?; + let order = { + client + .lock() + .create_order( + OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) + .with_description(msg.description) + .with_notify_url(msg.redirect_uri) + .with_products(msg.products.into_iter().map(Into::into)), + ) + .await? + }; Ok(order.order_id) } diff --git a/api/src/actors/token_manager.rs b/api/src/actors/token_manager.rs index 1b3edbd..0062370 100644 --- a/api/src/actors/token_manager.rs +++ b/api/src/actors/token_manager.rs @@ -1,6 +1,5 @@ use std::collections::BTreeMap; use std::str::FromStr; -use std::sync::Arc; use actix::{Addr, Message}; use chrono::prelude::*; @@ -8,6 +7,7 @@ use hmac::digest::KeyInit; use hmac::Hmac; use sha2::Sha256; +use crate::config::SharedAppConfig; use crate::database::{Database, TokenByJti}; use crate::model::{AccountId, Audience, Token, TokenString}; use crate::{database, Role}; @@ -21,8 +21,8 @@ macro_rules! token_async_handler { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { use actix::WrapFuture; let db = self.db.clone(); - let secret = self.secret.clone(); - Box::pin(async { $async(msg, db, secret).await }.into_actor(self)) + let config = self.config.clone(); + Box::pin(async { $async(msg, db, config).await }.into_actor(self)) } } }; @@ -68,7 +68,7 @@ pub type Result = std::result::Result; pub struct TokenManager { db: Addr, - secret: Arc, + config: SharedAppConfig, } impl actix::Actor for TokenManager { @@ -76,9 +76,8 @@ impl actix::Actor for TokenManager { } impl TokenManager { - pub fn new(db: Addr) -> Self { - let secret = Arc::new(std::env::var("JWT_SECRET").expect("JWT_SECRET is required")); - Self { db, secret } + pub fn new(config: SharedAppConfig, db: Addr) -> Self { + Self { db, config } } } @@ -96,7 +95,7 @@ token_async_handler!(CreateToken, create_token, (Token, TokenString)); pub(crate) async fn create_token( msg: CreateToken, db: Addr, - secret: Arc, + config: SharedAppConfig, ) -> Result<(Token, TokenString)> { let CreateToken { customer_id, @@ -129,6 +128,7 @@ pub(crate) async fn create_token( let token_string = { use jwt::SignWithKey; + let secret = config.lock().web().jwt_secret(); let key: Hmac = build_key(secret)?; let mut claims = BTreeMap::new(); @@ -195,12 +195,13 @@ token_async_handler!(Validate, validate, (Token, bool)); pub(crate) async fn validate( msg: Validate, db: Addr, - secret: Arc, + config: SharedAppConfig, ) -> Result<(Token, bool)> { use jwt::VerifyWithKey; log::info!("Validating token {:?}", msg.token); + let secret = config.lock().web().jwt_secret(); let key: Hmac = build_key(secret)?; let claims: BTreeMap = match msg.token.verify_with_key(&key) { Ok(claims) => claims, @@ -260,7 +261,7 @@ pub(crate) async fn validate( Ok((token, true)) } -fn build_key(secret: Arc) -> Result> { +fn build_key(secret: String) -> Result> { match Hmac::new_from_slice(secret.as_bytes()) { Ok(key) => Ok(key), Err(e) => { diff --git a/api/src/config.rs b/api/src/config.rs index 7e42c4d..22bf62c 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -1,9 +1,36 @@ +use std::sync::Arc; + +use parking_lot::Mutex; +use password_hash::SaltString; use serde::{Deserialize, Serialize}; trait Example: Sized { fn example() -> Self; } +#[derive(Clone)] +pub struct SharedAppConfig(Arc>); + +impl SharedAppConfig { + fn new(app_config: AppConfig) -> Self { + Self(Arc::new(Mutex::new(app_config))) + } +} + +impl std::ops::Deref for SharedAppConfig { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SharedAppConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[derive(Serialize, Deserialize, Default)] pub struct PaymentConfig { payu_client_id: Option, @@ -32,10 +59,9 @@ impl PaymentConfig { .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") - }) + .expect("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() @@ -45,14 +71,20 @@ impl PaymentConfig { .ok() .map(pay_u::ClientSecret) }) - .unwrap_or_else(|| { - panic!("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given") - }) + .expect("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")) + .or_else(|| { + std::env::var("PAYU_CLIENT_MERCHANT_ID") + .ok() + .and_then(|s| s.parse::().ok()) + .map(pay_u::MerchantPosId) + }) + .expect( + "payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given", + ) } } @@ -90,35 +122,48 @@ impl WebConfig { .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")) + .expect("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 pass_salt(&self) -> SaltString { + SaltString::new( + &self + .pass_salt + .as_ref() + .cloned() + .or_else(|| std::env::var("PASS_SALT").ok()) + .expect("Web config pass_salt nor PASS_SALT env was given"), + ) + .expect("Invalid password hash") } + 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")) + .expect("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")) + .expect("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 set_bind>(&mut self, bind: S) { + self.bind = Some(bind.into()); + } + pub fn port(&self) -> Option { self.port.as_ref().copied().or_else(|| { std::env::var("BAZZAR_PORT") @@ -126,6 +171,10 @@ impl WebConfig { .and_then(|s| s.parse::().ok()) }) } + + pub fn set_port(&mut self, port: u16) { + self.port = Some(port); + } } #[derive(Serialize, Deserialize, Default)] @@ -157,25 +206,23 @@ impl MailConfig { .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") - }) + .expect("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") - }) + .expect("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")) + .expect("Mail smtp_from config nor SMTP_FROM env was given") } } @@ -198,7 +245,11 @@ impl DatabaseConfig { .as_ref() .cloned() .or_else(|| std::env::var("DATABASE_URL").ok()) - .unwrap_or_else(|| panic!("Database url nor DATABASE_URL env was given")) + .expect("Database url nor DATABASE_URL env was given") + } + + pub fn set_url>(&mut self, url: S) { + self.url = Some(url.into()); } } @@ -228,15 +279,34 @@ 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 } + + pub fn payment_mut(&mut self) -> &mut PaymentConfig { + &mut self.payment + } + + pub fn web_mut(&mut self) -> &mut WebConfig { + &mut self.web + } + + pub fn mail_mut(&mut self) -> &mut MailConfig { + &mut self.mail + } + + pub fn database_mut(&mut self) -> &mut DatabaseConfig { + &mut self.database + } } impl Default for AppConfig { @@ -251,13 +321,13 @@ impl Default for AppConfig { } } -pub fn load(config_path: &str) -> AppConfig { +pub fn load(config_path: &str) -> SharedAppConfig { match std::fs::read_to_string(config_path) { - Ok(c) => toml::from_str(&c).unwrap(), + Ok(c) => SharedAppConfig::new(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 + SharedAppConfig::new(config) } Err(e) => { log::error!("{e:?}"); diff --git a/api/src/main.rs b/api/src/main.rs index a0466e6..c3288c0 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,5 +1,4 @@ use std::io::Write; -use std::sync::Arc; use actix::Actor; use actix_session::storage::RedisActorSessionStore; @@ -8,36 +7,30 @@ use actix_web::cookie::Key; use actix_web::middleware::Logger; use actix_web::web::Data; use actix_web::{App, HttpServer}; -use gumdrop::Options; use jemallocator::Jemalloc; +use opts::{ + Command, CreateAccountCmd, CreateAccountOpts, GenerateHashOpts, MigrateOpts, Opts, ServerOpts, + TestMailerOpts, +}; use password_hash::SaltString; -use pay_u::MerchantPosId; use validator::{validate_email, validate_length}; use crate::actors::{database, email_manager, order_manager, payment_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; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; -trait ResolveDbUrl { - fn own_db_url(&self) -> Option; - - fn db_url(&self) -> String { - self.own_db_url() - .or_else(|| std::env::var("DATABASE_URL").ok()) - .unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar")) - } -} - #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Failed to boot. {0:?}")] @@ -52,127 +45,6 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Options, Debug)] -struct Opts { - help: bool, - #[options(command)] - cmd: Option, -} - -#[derive(Options, Debug)] -enum Command { - #[options(help = "Run server")] - Server(ServerOpts), - #[options(help = "Migrate database")] - Migrate(MigrateOpts), - #[options(help = "Generate new salt for passwords")] - GenerateHash(GenerateHashOpts), - #[options(help = "Create new account")] - CreateAccount(CreateAccountOpts), - #[options(help = "Check mailer config")] - TestMailer(TestMailerOpts), -} - -impl Default for Command { - fn default() -> Self { - Command::Server(ServerOpts::default()) - } -} - -#[derive(Options, Debug)] -struct GenerateHashOpts { - help: bool, -} - -#[derive(Options, Debug)] -struct ServerOpts { - help: bool, - bind: String, - port: u16, - db_url: Option, -} - -impl Default for ServerOpts { - fn default() -> Self { - Self { - help: false, - bind: "0.0.0.0".to_string(), - port: 8080, - db_url: None, - } - } -} - -impl ResolveDbUrl for ServerOpts { - fn own_db_url(&self) -> Option { - self.db_url.as_deref().map(String::from) - } -} - -#[derive(Options, Debug)] -pub struct TestMailerOpts { - help: bool, - #[options(help = "E-mail receiver")] - receiver: Option, -} - -#[derive(Options, Debug)] -struct MigrateOpts { - help: bool, - db_url: Option, -} - -impl ResolveDbUrl for MigrateOpts { - fn own_db_url(&self) -> Option { - self.db_url.as_deref().map(String::from) - } -} - -#[derive(Debug, Options)] -struct CreateAccountOpts { - help: bool, - #[options(command)] - cmd: Option, -} - -#[derive(Debug, Options)] -enum CreateAccountCmd { - Admin(CreateAccountDefinition), - User(CreateAccountDefinition), -} - -#[derive(Debug, Options)] -struct CreateAccountDefinition { - help: bool, - #[options(free)] - login: String, - #[options(free)] - email: String, - #[options(free)] - pass_file: Option, - #[options(help = "Database url, it will also look for DATABASE_URL env")] - db_url: Option, -} - -impl ResolveDbUrl for CreateAccountDefinition { - fn own_db_url(&self) -> Option { - self.db_url.as_deref().map(String::from) - } -} - -pub struct Config { - pass_salt: SaltString, -} - -impl Config { - fn load() -> Self { - let pass_salt = - SaltString::new(&std::env::var("PASS_SALT").expect("PASS_SALT is required")) - .expect("Invalid password salt"); - Self { pass_salt } - } -} - async fn server(opts: ServerOpts) -> Result<()> { let secret_key = { let key_secret = std::env::var("SESSION_SECRET") @@ -181,28 +53,25 @@ async fn server(opts: ServerOpts) -> Result<()> { }; let redis_connection_string = "127.0.0.1:6379"; - let app_config = crate::config::load("./bazzar.toml"); + let app_config = config::load("./bazzar.toml"); + { + opts.update_config(&mut *app_config.lock()); + } - 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(); - let order_manager = order_manager::OrderManager::new(db.clone()).start(); - let payment_manager = { - let client_id = std::env::var("PAYU_CLIENT_ID").expect("Missing PAYU_CLIENT_ID env"); - let client_secret = - std::env::var("PAYU_CLIENT_SECRET").expect("Missing PAYU_CLIENT_SECRET env"); - let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID") - .expect("Missing PAYU_CLIENT_MERCHANT_ID env") - .parse::() - .map(MerchantPosId::from) - .expect("Variable PAYU_CLIENT_MERCHANT_ID must be number"); - payment_manager::PaymentManager::build(client_id, client_secret, merchant_id, db.clone()) - .await - .expect("Failed to start payment manager") - .start() - }; + let db = database::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()) + .await + .expect("Failed to start payment manager") + .start(); + let addr = ( + app_config.lock().web().bind().unwrap_or(opts.bind), + app_config.lock().web().port().unwrap_or(opts.port), + ); HttpServer::new(move || { + let config = app_config.clone(); App::new() .wrap(Logger::default()) .wrap(actix_web::middleware::Compress::default()) @@ -211,7 +80,7 @@ async fn server(opts: ServerOpts) -> Result<()> { RedisActorSessionStore::new(redis_connection_string), secret_key.clone(), )) - .app_data(Data::new(config.clone())) + .app_data(Data::new(config)) .app_data(Data::new(db.clone())) .app_data(Data::new(token_manager.clone())) .app_data(Data::new(order_manager.clone())) @@ -219,10 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> { .configure(routes::configure) // .default_service(web::to(HttpResponse::Ok)) }) - .bind(( - app_config.web().bind().unwrap_or(opts.bind), - app_config.web().port().unwrap_or(opts.port), - )) + .bind(addr) .map_err(Error::Boot)? .run() .await @@ -232,7 +98,9 @@ async fn server(opts: ServerOpts) -> Result<()> { async fn migrate(opts: MigrateOpts) -> Result<()> { use sqlx::migrate::MigrateError; - let db = database::Database::build(&opts.db_url()).await?; + let config = config::load("./bazzar.toml"); + opts.update_config(&mut *config.lock()); + let db = database::Database::build(config).await?; let res: std::result::Result<(), MigrateError> = sqlx::migrate!("../db/migrate").run(db.pool()).await; match res { @@ -262,7 +130,9 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> { if !validate_length(&opts.login, Some(4), Some(100), None) { panic!("Login must have at least 4 characters and no more than 100"); } - let db = database::Database::build(&opts.db_url()).await?.start(); + let config = config::load("./bazzar.toml"); + opts.update_config(&mut *config.lock()); + let db = database::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 => { @@ -287,8 +157,7 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> { if pass.trim().is_empty() { panic!("Password cannot be empty!"); } - let config = Config::load(); - let hash = encrypt_password(&Password::from(pass), &config.pass_salt).unwrap(); + let hash = encrypt_password(&Password::from(pass), &config.lock().web().pass_salt()).unwrap(); db.send(database::CreateAccount { email: Email::from(opts.email), @@ -303,7 +172,10 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> { } async fn test_mailer(opts: TestMailerOpts) -> Result<()> { - let manager = email_manager::EmailManager::build() + let config = config::load("./bazzar.toml"); + opts.update_config(&mut *config.lock()); + + let manager = email_manager::EmailManager::build(config) .expect("Invalid email manager config") .start(); if manager diff --git a/api/src/opts.rs b/api/src/opts.rs new file mode 100644 index 0000000..8df4390 --- /dev/null +++ b/api/src/opts.rs @@ -0,0 +1,222 @@ +use gumdrop::Options; + +use crate::config::AppConfig; +use crate::model::Email; + +pub trait UpdateConfig { + fn update_config(&self, config: &mut AppConfig); +} + +pub trait ResolveDbUrl { + fn own_db_url(&self) -> Option; + + fn db_url(&self) -> String { + self.own_db_url() + .or_else(|| std::env::var("DATABASE_URL").ok()) + .unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar")) + } +} + +#[derive(Options, Debug)] +pub struct Opts { + pub help: bool, + #[options(command)] + pub cmd: Option, +} + +impl UpdateConfig for Opts { + fn update_config(&self, config: &mut AppConfig) { + match &self.cmd { + None => {} + Some(cmd) => { + cmd.update_config(config); + } + } + } +} + +#[derive(Options, Debug)] +pub enum Command { + #[options(help = "Run server")] + Server(ServerOpts), + #[options(help = "Migrate database")] + Migrate(MigrateOpts), + #[options(help = "Generate new salt for passwords")] + GenerateHash(GenerateHashOpts), + #[options(help = "Create new account")] + CreateAccount(CreateAccountOpts), + #[options(help = "Check mailer config")] + TestMailer(TestMailerOpts), +} + +impl UpdateConfig for Command { + fn update_config(&self, config: &mut AppConfig) { + match self { + Command::Server(opts) => { + opts.update_config(config); + } + Command::Migrate(opts) => { + opts.update_config(config); + } + Command::GenerateHash(opts) => { + opts.update_config(config); + } + Command::CreateAccount(opts) => { + opts.update_config(config); + } + Command::TestMailer(opts) => { + opts.update_config(config); + } + } + } +} + +impl Default for Command { + fn default() -> Self { + Command::Server(ServerOpts::default()) + } +} + +#[derive(Options, Debug)] +pub struct GenerateHashOpts { + pub help: bool, +} + +impl UpdateConfig for GenerateHashOpts { + fn update_config(&self, _config: &mut AppConfig) {} +} + +#[derive(Options, Debug)] +pub struct ServerOpts { + pub help: bool, + pub bind: String, + pub port: u16, + pub db_url: Option, +} + +impl Default for ServerOpts { + fn default() -> Self { + Self { + help: false, + bind: "0.0.0.0".to_string(), + port: 8080, + db_url: None, + } + } +} + +impl UpdateConfig for ServerOpts { + fn update_config(&self, config: &mut AppConfig) { + { + let web = config.web_mut(); + if web.bind().is_none() { + web.set_bind(&self.bind); + } + if web.port().is_none() { + web.set_port(self.port); + } + } + if let Some(url) = self.db_url.as_ref() { + config.database_mut().set_url(url); + } + } +} + +impl ResolveDbUrl for ServerOpts { + fn own_db_url(&self) -> Option { + self.db_url.as_deref().map(String::from) + } +} + +#[derive(Options, Debug)] +pub struct TestMailerOpts { + pub help: bool, + #[options(help = "E-mail receiver")] + pub receiver: Option, +} + +impl UpdateConfig for TestMailerOpts { + fn update_config(&self, _config: &mut AppConfig) {} +} + +#[derive(Options, Debug)] +pub struct MigrateOpts { + pub help: bool, + pub db_url: Option, +} + +impl UpdateConfig for MigrateOpts { + fn update_config(&self, config: &mut AppConfig) { + if let Some(url) = self.db_url.as_deref() { + config.database_mut().set_url(url); + } + } +} + +impl ResolveDbUrl for MigrateOpts { + fn own_db_url(&self) -> Option { + self.db_url.as_deref().map(String::from) + } +} + +#[derive(Debug, Options)] +pub struct CreateAccountOpts { + pub help: bool, + #[options(command)] + pub cmd: Option, +} + +impl UpdateConfig for CreateAccountOpts { + fn update_config(&self, config: &mut AppConfig) { + match &self.cmd { + None => {} + Some(opts) => opts.update_config(config), + }; + } +} + +#[derive(Debug, Options)] +pub enum CreateAccountCmd { + Admin(CreateAccountDefinition), + User(CreateAccountDefinition), +} + +impl UpdateConfig for CreateAccountCmd { + fn update_config(&self, config: &mut AppConfig) { + match &self { + CreateAccountCmd::Admin(opts) => { + opts.update_config(config); + } + CreateAccountCmd::User(opts) => { + opts.update_config(config); + } + } + } +} + +#[derive(Debug, Options)] +pub struct CreateAccountDefinition { + pub help: bool, + #[options(free)] + pub login: String, + #[options(free)] + pub email: String, + #[options(free)] + pub pass_file: Option, + #[options(help = "Database url, it will also look for DATABASE_URL env")] + pub db_url: Option, +} + +impl UpdateConfig for CreateAccountDefinition { + fn update_config(&self, config: &mut AppConfig) { + if let Some(url) = self.db_url.as_deref() { + config.database_mut().set_url(url); + } + } +} + +impl ResolveDbUrl for CreateAccountDefinition { + fn own_db_url(&self) -> Option { + self.db_url.as_deref().map(String::from) + } +} diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 3c82571..22f244e 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -1,18 +1,17 @@ mod api_v1; -use std::sync::Arc; - use actix::Addr; use actix_session::Session; use actix_web::web::{scope, Data, Json, ServiceConfig}; use actix_web::{delete, get, post, HttpResponse}; 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::{RequireLogin, Result}; -use crate::{database, model, routes, Config}; +use crate::{database, model, routes}; #[macro_export] macro_rules! admin_send_db { @@ -127,7 +126,7 @@ async fn register( session: Session, Json(input): Json, db: Data>, - config: Data>, + config: Data, ) -> Result { let mut response = RegisterResponse::default(); session.require_admin()?; @@ -136,7 +135,7 @@ async fn register( response.errors.push(RegisterError::PasswordDiffer); } - let hash = match encrypt_password(&input.password, &config.pass_salt) { + let hash = match encrypt_password(&input.password, &config.lock().web().pass_salt()) { Ok(s) => s, Err(e) => { log::error!("{e:?}"); diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs index 13c8fe0..fc3f4c6 100644 --- a/api/src/routes/admin/api_v1/accounts.rs +++ b/api/src/routes/admin/api_v1/accounts.rs @@ -1,17 +1,14 @@ -use std::sync::Arc; - use actix::Addr; use actix_session::Session; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::{get, patch, post, HttpResponse}; +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, Config, Email, Login, PassHash, Password, Role, -}; +use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Password, Role}; #[get("/accounts")] pub async fn accounts(session: Session, db: Data>) -> routes::Result { @@ -36,7 +33,7 @@ pub async fn update_account( session: Session, db: Data>, Json(payload): Json, - config: Data>, + config: Data, ) -> routes::Result { session.require_admin()?; @@ -48,7 +45,7 @@ pub async fn update_account( routes::admin::Error::DifferentPasswords, )); } - let hash = match encrypt_password(&p1, &config.pass_salt) { + let hash = match encrypt_password(&p1, &config.lock().web().pass_salt()) { Ok(hash) => hash, Err(e) => { log::error!("{e:?}"); @@ -92,7 +89,7 @@ pub async fn create_account( session: Session, db: Data>, Json(payload): Json, - config: Data>, + config: Data, ) -> routes::Result { session.require_admin()?; if payload.password != payload.password_confirmation { @@ -100,7 +97,7 @@ pub async fn create_account( routes::admin::Error::DifferentPasswords, )); } - let hash = match encrypt_password(&payload.password, &config.pass_salt) { + let hash = match encrypt_password(&payload.password, &config.lock().web().pass_salt()) { Ok(hash) => hash, Err(e) => { log::error!("{e:?}"); diff --git a/pay_u/src/lib.rs b/pay_u/src/lib.rs index ed2b3f8..c7582f7 100644 --- a/pay_u/src/lib.rs +++ b/pay_u/src/lib.rs @@ -27,6 +27,8 @@ pub static SUCCESS: &str = "SUCCESS"; pub enum Error { #[error("Client is not authorized. No bearer token available")] NoToken, + #[error("Invalid customer ip. IP 0.0.0.0 is not acceptable")] + CustomerIp, #[error("{0}")] Io(#[from] std::io::Error), #[error("Total value is not sum of products price")] @@ -316,9 +318,105 @@ impl Product { } } +/// MultiUseCartToken +pub mod muct { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + pub enum CardOnFile { + /// Payment initialized by the card owner who agreed to save card for + /// future use. You can expect full authentication (3D Secure + /// and/or CVV). If you want to use multi-use token (TOKC_) + /// later, you have to be confident, that first payment was + /// successful. Default value for single-use token (TOK_). + /// + /// In case of plain card data payments you should retrieve transaction + /// data to obtain first TransactionId. It should be passed in + /// payMethods.payMethod.card section for transactions marked as + /// STANDARD, STANDARD_CARDHOLDER and STANDARD_MERCHANT; + /// STANDARD_CARDHOLDER - payment with already saved card, + /// initialized by the card owner. This transaction has + /// multi-use token (TOKC_). Depending of payment parameters + /// (e.g. high transaction amount) strong authentication can be + /// expected (3D Secure and/or CVV). Default value for multi-use token + /// (TOKC_); + First, + /// Payment with already saved card, initialized by the shop without the + /// card owner participation. This transaction has multi-use token + /// (TOKC_). By the definition, this payment type does not + /// require strong authentication. You cannot use it if FIRST + /// card-on-file payment failed. + StandardMerchant, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + pub enum Recurring { + /// Payment initialized by the card owner who agreed to save card for + /// future use in recurring plan. You can expect full authentication (3D + /// Secure and/or CVV). If you want to use multi-use token (TOKC_) + /// later, you have to be confident, that first recurring + /// payment was successful. + First, + /// Subsequent recurring payment (user is not present). This transaction + /// has multi use token (TOKC_). You cannot use it if FIRST recurring + /// payment failed. + Standard, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct MultiUseCartToken { + /// Information about party initializing order: + /// + /// * `FIRST` - payment initialized by the card owner who agreed to save + /// card for future use. You can expect full authentication (3D Secure + /// and/or CVV). If you want to use multi-use token (TOKC_) later, you + /// have to be confident, that first payment was successful. Default + /// value for single-use token (TOK_). + /// + /// In case of plain card data payments you should retrieve + /// transaction data to obtain first TransactionId. It should + /// be passed in payMethods.payMethod.card section for + /// transactions marked as STANDARD, STANDARD_CARDHOLDER and + /// STANDARD_MERCHANT; STANDARD_CARDHOLDER - payment with + /// already saved card, initialized by the card owner. This + /// transaction has multi-use token (TOKC_). Depending of payment + /// parameters (e.g. high transaction amount) strong authentication + /// can be expected (3D Secure and/or CVV). Default value for + /// multi-use token (TOKC_); + /// * `STANDARD_MERCHANT` - payment with already saved card, initialized + /// by the shop without the card owner participation. This transaction + /// has multi-use token (TOKC_). By the definition, this payment type + /// does not require strong authentication. You cannot use it if FIRST + /// card-on-file payment failed. + /// + /// `cardOnFile` parameter cannot be used with recurring parameter. + pub card_on_file: CardOnFile, + /// Marks the order as recurring payment. + /// + /// * `FIRST` - payment initialized by the card owner who agreed to save + /// card for future use in recurring plan. You can expect full + /// authentication (3D Secure and/or CVV). If you want to use + /// multi-use token (TOKC_) later, you have to be confident, that + /// first recurring payment was successful. + /// * `STANDARD` - subsequent recurring payment (user is not present). + /// This transaction has multi use token (TOKC_). You cannot use it if + /// FIRST recurring payment failed. + /// + /// `recurring` parameter cannot be used with cardOnFile parameter. + pub recurring: Recurring, + } +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct OrderCreateRequest { + /// ID of an order used in merchant system. Order identifier assigned by the + /// merchant. It enables merchants to find a specific order in their system. + /// This value must be unique within a single POS. + ext_order_id: Option, /// URL to which web hook will be send. It's important to return 200 to all /// notifications. /// @@ -352,7 +450,22 @@ pub struct OrderCreateRequest { /// | 20| 72 hours | #[serde(skip_serializing_if = "Option::is_none")] notify_url: Option, - /// Customer client IP address + /// Address for redirecting the customer after payment is commenced. If the + /// payment has not been authorized, error=501 parameter will be added. + /// Please note that no decision regarding payment status should be made + /// depending on the presence or lack of this parameter (to get payment + /// status, wait for notification or retrieve order details). + /// + /// IMPORTANT: the address must be compliant with the structure below: + /// + /// + /// Please keep in mind: + /// * accepted schemas are http and https, + /// * such elements as port, path, query and fragment are optional, + /// * query values must be encoded. + #[serde(skip_serializing_if = "Option::is_none")] + continue_url: Option, + /// Payer’s IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not accepted. customer_ip: String, /// Secret pos ip. This is connected to PayU account #[serde( @@ -377,29 +490,106 @@ pub struct OrderCreateRequest { products: Vec, #[serde(skip_serializing)] order_create_date: Option, + /// Duration for the validity of an order (in seconds), during which time + /// payment must be made. Default value 86400. + #[serde(skip_serializing_if = "Option::is_none")] + validity_time: Option, + /// Additional description of the order. + #[serde(skip_serializing_if = "Option::is_none")] + additional_description: Option, + /// Text visible on the PayU payment page (max. 80 chars). + #[serde(skip_serializing_if = "Option::is_none")] + visible_description: Option, + /// Payment recipient name followed by payment description (order ID, ticket + /// number etc) visible on card statement (max. 22 chars). The name should + /// be easy to recognize by the cardholder (e.g "shop.com 124343"). If field + /// is not provided, static name configured by PayU will be used. + statement_description: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + muct: Option, } impl OrderCreateRequest { - pub fn new( + pub fn build( buyer: Buyer, customer_ip: CustomerIp, currency: Currency, - ) -> Self + description: Description, + ) -> Result where CustomerIp: Into, Currency: Into, + Description: Into, { - Self { + let customer_ip = customer_ip.into(); + if &customer_ip == "0.0.0.0" { + return Err(Error::CustomerIp); + } + Ok(Self { + ext_order_id: None, notify_url: None, - customer_ip: customer_ip.into(), + continue_url: None, + customer_ip, merchant_pos_id: 0.into(), - description: String::from(""), + description: description.into(), currency_code: currency.into(), total_amount: 0, buyer: Some(buyer), products: Vec::new(), order_create_date: None, - } + validity_time: None, + additional_description: None, + visible_description: None, + statement_description: None, + muct: None, + }) + } + + /// ID of an order used in merchant system. Order identifier assigned by the + /// merchant. It enables merchants to find a specific order in their system. + /// This value must be unique within a single POS. + pub fn with_ext_order_id>(mut self, ext_order_id: S) -> Self { + self.ext_order_id = Some(ext_order_id.into()); + self + } + + /// Duration for the validity of an order (in seconds), during which time + /// payment must be made. Default value 86400. + pub fn with_validity_time(mut self, validity_time: u16) -> Self { + self.validity_time = Some(validity_time); + self + } + + /// Additional description of the order. + pub fn with_additional_description>( + mut self, + additional_description: S, + ) -> Self { + self.additional_description = Some(additional_description.into()); + self + } + + /// Text visible on the PayU payment page (max. 80 chars). + pub fn with_visible_description(mut self, visible_description: &str) -> Self { + let visible_description = if visible_description.len() > 60 { + &visible_description[..60] + } else { + visible_description + }; + self.visible_description = Some(String::from(visible_description)); + self + } + + pub fn with_multi_use_token( + mut self, + recurring: muct::Recurring, + card_on_file: muct::CardOnFile, + ) -> Self { + self.muct = Some(muct::MultiUseCartToken { + recurring, + card_on_file, + }); + self } pub fn with_products(mut self, products: Products) -> Self