This commit is contained in:
Adrian Woźniak 2022-04-27 16:25:57 +02:00
parent f55f2c4a0d
commit 9d6534c2a2
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
8 changed files with 335 additions and 14 deletions

4
.env
View File

@ -1,7 +1,7 @@
DATABASE_URL=postgres://postgres@localhost/bazzar DATABASE_URL=postgres://postgres@localhost/bazzar
PASS_SALT=18CHwV7eGFAea16z+qMKZg PASS_SALT=18CHwV7eGFAea16z+qMKZg
RUST_LOG=debug 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" JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN"
PGDATESTYLE= PGDATESTYLE=
@ -12,3 +12,5 @@ SMTP_FROM=adrian.wozniak@ita-prog.pl
PAYU_CLIENT_ID="145227" PAYU_CLIENT_ID="145227"
PAYU_CLIENT_SECRET="12f071174cb7eb79d4aac5bc2f07563f" PAYU_CLIENT_SECRET="12f071174cb7eb79d4aac5bc2f07563f"
PAYU_CLIENT_MERCHANT_ID=300746 PAYU_CLIENT_MERCHANT_ID=300746
WEB_HOST=https://bazzar.ita-prog.pl

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
bazzar.toml

View File

@ -1,9 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use actix::Addr;
use parking_lot::Mutex; use parking_lot::Mutex;
use pay_u::{MerchantPosId, OrderCreateRequest}; 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_export]
macro_rules! pay_async_handler { 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 { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture; use actix::WrapFuture;
let db = self.client.clone(); let client = self.client.clone();
Box::pin(async { $async(msg, db).await }.into_actor(self)) let db = self.db.clone();
Box::pin(async { $async(msg, client, db).await }.into_actor(self))
} }
} }
}; };
@ -26,6 +30,8 @@ pub type PayUClient = Arc<Mutex<pay_u::Client>>;
pub enum Error { pub enum Error {
#[error("{0}")] #[error("{0}")]
PayU(#[from] pay_u::Error), PayU(#[from] pay_u::Error),
#[error("Failed to create order")]
CreateOrder,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -33,6 +39,7 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)] #[derive(Clone)]
pub struct PaymentManager { pub struct PaymentManager {
client: PayUClient, client: PayUClient,
db: Addr<Database>,
} }
impl PaymentManager { impl PaymentManager {
@ -40,6 +47,7 @@ impl PaymentManager {
client_id: ClientId, client_id: ClientId,
client_secret: ClientSecret, client_secret: ClientSecret,
merchant_pos_id: MerchantPosId, merchant_pos_id: MerchantPosId,
db: Addr<Database>,
) -> Result<Self> ) -> Result<Self>
where where
ClientId: Into<pay_u::ClientId>, ClientId: Into<pay_u::ClientId>,
@ -50,6 +58,7 @@ impl PaymentManager {
client.authorize().await?; client.authorize().await?;
Ok(Self { Ok(Self {
client: Arc::new(Mutex::new(client)), client: Arc::new(Mutex::new(client)),
db,
}) })
} }
} }
@ -85,8 +94,10 @@ impl From<Buyer> for pay_u::Buyer {
#[derive(Debug)] #[derive(Debug)]
pub struct Product { pub struct Product {
pub id: ProductId,
pub name: String, pub name: String,
pub unit_price: Price, pub unit_price: Price,
pub quantity_unit: QuantityUnit,
pub quantity: Quantity, pub quantity: Quantity,
} }
@ -105,6 +116,8 @@ pub struct RequestPayment {
pub description: String, pub description: String,
pub buyer: Buyer, pub buyer: Buyer,
pub customer_ip: String, pub customer_ip: String,
pub buyer_id: AccountId,
pub shopping_cart_id: ShoppingCartId,
} }
pay_async_handler!(RequestPayment, request_payment, pay_u::OrderId); 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( pub(crate) async fn request_payment(
msg: RequestPayment, msg: RequestPayment,
client: PayUClient, client: PayUClient,
db: Addr<Database>,
) -> Result<pay_u::OrderId> { ) -> Result<pay_u::OrderId> {
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 let order = client
.create_order( .create_order(
OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency)

272
api/src/config.rs Normal file
View File

@ -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<pay_u::ClientId>,
payu_client_secret: Option<pay_u::ClientSecret>,
payu_client_merchant_id: Option<pay_u::MerchantPosId>,
}
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::<i32>().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<String>,
/// Encrypt password salt
pass_salt: Option<String>,
/// Used by redis to save admin session across actors
session_secret: Option<String>,
/// Encrypt JWT
jwt_secret: Option<String>,
bind: Option<String>,
port: Option<u16>,
}
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<String> {
self.bind
.as_ref()
.cloned()
.or_else(|| std::env::var("BAZZAR_BIND").ok())
}
pub fn port(&self) -> Option<u16> {
self.port.as_ref().copied().or_else(|| {
std::env::var("BAZZAR_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok())
})
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct MailConfig {
sendgrid_secret: Option<String>,
sendgrid_api_key: Option<String>,
smtp_from: Option<String>,
}
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<String>,
}
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();
}

View File

@ -20,6 +20,7 @@ use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role}; use crate::model::{Email, Login, PassHash, Password, Role};
pub mod actors; pub mod actors;
pub mod config;
pub mod logic; pub mod logic;
pub mod model; pub mod model;
pub mod routes; pub mod routes;
@ -174,12 +175,14 @@ impl Config {
async fn server(opts: ServerOpts) -> Result<()> { async fn server(opts: ServerOpts) -> Result<()> {
let secret_key = { 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"); .expect("session requires secret key with 64 or more characters");
Key::from(key_secret.as_bytes()) Key::from(key_secret.as_bytes())
}; };
let redis_connection_string = "127.0.0.1:6379"; let redis_connection_string = "127.0.0.1:6379";
let app_config = crate::config::load("./bazzar.toml");
let config = Arc::new(Config::load()); let config = Arc::new(Config::load());
let db = database::Database::build(&opts.db_url()).await?.start(); let db = database::Database::build(&opts.db_url()).await?.start();
let token_manager = token_manager::TokenManager::new(db.clone()).start(); let token_manager = token_manager::TokenManager::new(db.clone()).start();
@ -193,7 +196,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.parse::<i32>() .parse::<i32>()
.map(MerchantPosId::from) .map(MerchantPosId::from)
.expect("Variable PAYU_CLIENT_MERCHANT_ID must be number"); .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 .await
.expect("Failed to start payment manager") .expect("Failed to start payment manager")
.start() .start()
@ -216,7 +219,10 @@ async fn server(opts: ServerOpts) -> Result<()> {
.configure(routes::configure) .configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok)) // .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)? .map_err(Error::Boot)?
.run() .run()
.await .await

View File

@ -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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct Price(NonNegative); 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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct Quantity(NonNegative); 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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct NonNegative(i32); pub struct NonNegative(i32);

View File

@ -1,5 +1,2 @@
ALTER TABLE accounts ALTER TABLE accounts
ADD COLUMN customer_id uuid not null default gen_random_uuid(); ADD COLUMN customer_id uuid not null default gen_random_uuid();
ALTER TABLE account_orders
ADD COLUMN order_id varchar unique;

View File

@ -0,0 +1,2 @@
ALTER TABLE account_orders
ADD COLUMN order_ext_id uuid not null default uuid_generate_v4();