More tests, documentation and prepare to add other payment methods

This commit is contained in:
Adrian Woźniak 2022-06-09 15:28:15 +02:00
parent 4bc914b6df
commit 69bf0b5dff
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
25 changed files with 554 additions and 166 deletions

30
Cargo.lock generated
View File

@ -559,9 +559,9 @@ checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "async-trait"
version = "0.1.53"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600"
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
dependencies = [
"proc-macro2",
"quote",
@ -1183,6 +1183,7 @@ version = "0.1.0"
dependencies = [
"actix 0.13.0",
"actix-rt",
"async-trait",
"chrono",
"config",
"fake",
@ -3127,11 +3128,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.38"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
dependencies = [
"unicode-xid",
"unicode-ident",
]
[[package]]
@ -4003,13 +4004,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.94"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"unicode-ident",
]
[[package]]
@ -4219,6 +4220,7 @@ dependencies = [
"rand_core",
"serde",
"sha2",
"testx",
"thiserror",
"tokio",
"tracing",
@ -4565,6 +4567,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
@ -4580,12 +4588,6 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "unicode_categories"
version = "0.1.1"

View File

@ -31,5 +31,7 @@ itertools = { version = "0.10.3" }
serde = { version = "1.0", features = ["derive"] }
async-trait = { version = "0.1.56" }
[dev-dependencies]
testx = { path = "../../shared/testx" }

View File

@ -248,7 +248,7 @@ mod test {
#[actix::test]
async fn full_check() {
testx::db_t!(t);
testx::db_t_ref!(t);
// account
let account = test_create_account(&mut t).await;

View File

@ -268,7 +268,7 @@ mod tests {
#[actix::test]
async fn create_account() {
testx::db_t!(t);
testx::db_t_ref!(t);
let login: String = fake::faker::internet::en::Username().fake();
let email: String = fake::faker::internet::en::FreeEmail().fake();
@ -302,7 +302,7 @@ mod tests {
#[actix::test]
async fn all_accounts() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_create_account(&mut t, None, None, None).await;
test_create_account(&mut t, None, None, None).await;
@ -316,7 +316,7 @@ mod tests {
#[actix::test]
async fn update_account_without_pass() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original_login: String = fake::faker::internet::en::Username().fake();
let original_email: String = fake::faker::internet::en::FreeEmail().fake();
@ -364,7 +364,7 @@ mod tests {
#[actix::test]
async fn update_account_with_pass() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original_login: String = fake::faker::internet::en::Username().fake();
let original_email: String = fake::faker::internet::en::FreeEmail().fake();
@ -413,7 +413,7 @@ mod tests {
#[actix::test]
async fn find() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_create_account(&mut t, None, None, None).await;
@ -432,7 +432,7 @@ mod tests {
#[actix::test]
async fn find_identity_email() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_create_account(&mut t, None, None, None).await;
@ -452,7 +452,7 @@ mod tests {
#[actix::test]
async fn find_identity_login() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_create_account(&mut t, None, None, None).await;

View File

@ -72,6 +72,16 @@ macro_rules! db_async_handler {
Box::pin(async { $inner_async(msg, pool).await }.into_actor(self))
}
}
#[async_trait::async_trait]
impl $crate::Queue<$msg> for crate::Transaction {
type Result = $res;
async fn handle(&mut self, msg: $msg) -> Result<Self::Result> {
let t = &mut self.t;
$async(msg, t).await
}
}
};
}
@ -159,6 +169,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct Database {
pool: PgPool,
config: SharedAppConfig,
}
pub type SharedDatabase = actix::Addr<Database>;
@ -167,6 +178,7 @@ impl Clone for Database {
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
config: self.config.clone(),
}
}
}
@ -178,18 +190,55 @@ impl Database {
tracing::error!("Failed to connect to database. {e:?}");
std::process::exit(1);
});
Database { pool }
Self { pool, config }
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn begin(&self) -> sqlx::Result<Transaction> {
Ok(Transaction {
t: self.pool.begin().await?,
})
}
}
impl Actor for Database {
type Context = Context<Self>;
}
pub struct Transaction {
t: sqlx::Transaction<'static, sqlx::Postgres>,
}
impl Transaction {
pub async fn commit(self) -> Result<()> {
self.t.commit().await.map_err(|e| {
tracing::error!("{e:?}");
dbg!(e);
Error::TransactionFailed
})?;
Ok(())
}
pub async fn rollback(self) -> Result<()> {
self.t.rollback().await.map_err(|e| {
tracing::error!("{e:?}");
dbg!(e);
Error::TransactionFailed
})?;
Ok(())
}
}
#[async_trait::async_trait]
pub trait Queue<Msg> {
type Result;
async fn handle(&mut self, msg: Msg) -> Result<Self::Result>;
}
/// Multi-query load for large amount of records to read
///
/// Examples
@ -203,7 +252,11 @@ impl Actor for Database {
/// &mut t,
/// "SELECT id, name FROM products WHERE ",
/// " id = "
/// );
/// )
/// // order by id
/// .with_sorting("id ASC")
/// // 100 rows per db query
/// .with_size(100);
/// let products: Vec<model::Product> = multi.load(4, vec![1, 2, 3, 4].into_iter(), |_| Error::All.into())
/// .await.unwrap();
/// t.commit().await.unwrap();
@ -214,6 +267,7 @@ pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> {
header: &'header str,
condition: &'condition str,
sort: Option<String>,
size: usize,
__phantom: std::marker::PhantomData<T>,
}
@ -232,6 +286,7 @@ where
header,
condition,
sort: None,
size: 20,
__phantom: Default::default(),
}
}
@ -241,6 +296,11 @@ where
self
}
pub fn with_size(mut self, size: usize) -> Self {
self.size = size;
self
}
pub async fn load<'query, Error, Ids>(
&mut self,
len: usize,
@ -252,12 +312,14 @@ where
Error: Fn(sqlx::Error) -> crate::Error,
{
let mut res = Vec::new();
let size = self.size;
for ids in items.fold(
Vec::<Vec<model::RecordId>>::with_capacity(len),
|mut v, id| {
if matches!(v.last().map(|v| v.len()), Some(20) | None) {
v.push(Vec::with_capacity(20));
let last_len = v.last().map(|v| v.len());
if last_len == Some(size) || last_len == None {
v.push(Vec::with_capacity(size));
}
v.last_mut().unwrap().push(id);
v

View File

@ -184,7 +184,7 @@ mod tests {
#[actix::test]
async fn create() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_order_address(&mut t).await;
@ -193,7 +193,7 @@ mod tests {
#[actix::test]
async fn update() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original = test_order_address(&mut t).await;
let updated = super::update_order_address(
@ -231,7 +231,7 @@ mod tests {
}
async fn order_address() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_order_address(&mut t).await;

View File

@ -284,7 +284,7 @@ mod tests {
#[actix::test]
async fn create() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_order_item(&mut t, None).await;
@ -293,7 +293,7 @@ mod tests {
#[actix::test]
async fn order_items() {
testx::db_t!(t);
testx::db_t_ref!(t);
let order1 = test_order(&mut t).await;
test_order_item(&mut t, Some(order1.id)).await;

View File

@ -404,7 +404,7 @@ mod tests {
#[actix::test]
async fn empty_order_without_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let address_id = test_order_address(&mut t).await.id;
test_empty_order_without_cart(&mut t, None, address_id).await;
@ -414,7 +414,7 @@ mod tests {
#[actix::test]
async fn empty_order_with_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let buyer_id = test_account(&mut t).await.id;
let address_id = test_order_address(&mut t).await.id;
@ -440,7 +440,7 @@ mod tests {
#[actix::test]
async fn non_empty_order_with_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let buyer_id = test_account(&mut t).await.id;
let address_id = test_order_address(&mut t).await.id;
@ -477,7 +477,7 @@ mod tests {
#[actix::test]
async fn non_empty_order_without_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let buyer_id = test_account(&mut t).await.id;
let address_id = test_order_address(&mut t).await.id;
@ -510,7 +510,7 @@ mod tests {
#[actix::test]
async fn update_by_ext() {
testx::db_t!(t);
testx::db_t_ref!(t);
let address_id = test_order_address(&mut t).await.id;
let original = test_empty_order_without_cart(&mut t, None, address_id).await;

View File

@ -192,7 +192,7 @@ mod tests {
#[actix::test]
async fn create_photo() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_photo(&mut t, None, None, None).await;
@ -201,7 +201,7 @@ mod tests {
#[actix::test]
async fn all() {
testx::db_t!(t);
testx::db_t_ref!(t);
let p1 = test_photo(&mut t, None, None, None).await;
let p2 = test_photo(&mut t, None, None, None).await;
@ -215,7 +215,7 @@ mod tests {
#[actix::test]
async fn products_photos() {
testx::db_t!(t);
testx::db_t_ref!(t);
let product_1 = test_product(&mut t).await;
let p1 = test_product_photo(&mut t, product_1.id).await;

View File

@ -165,7 +165,7 @@ mod tests {
#[actix::test]
async fn create_photo() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_product_photo(&mut t).await;
@ -174,7 +174,7 @@ mod tests {
#[actix::test]
async fn delete() {
testx::db_t!(t);
testx::db_t_ref!(t);
let p1 = test_product_photo(&mut t).await;
let p2 = test_product_photo(&mut t).await;
@ -192,7 +192,7 @@ mod tests {
#[actix::test]
async fn create() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_product_photo(&mut t).await;

View File

@ -360,7 +360,7 @@ mod tests {
#[actix::test]
async fn create() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_product(&mut t, None, None, None, None, None, None).await;
@ -369,7 +369,7 @@ mod tests {
#[actix::test]
async fn all() {
testx::db_t!(t);
testx::db_t_ref!(t);
let p1 = test_product(&mut t, None, None, None, None, None, None).await;
let p2 = test_product(&mut t, None, None, None, None, None, None).await;
@ -383,7 +383,7 @@ mod tests {
#[actix::test]
async fn find() {
testx::db_t!(t);
testx::db_t_ref!(t);
let p1 = test_product(&mut t, None, None, None, None, None, None).await;
let p2 = test_product(&mut t, None, None, None, None, None, None).await;
@ -401,7 +401,7 @@ mod tests {
#[actix::test]
async fn update() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original = test_product(&mut t, None, None, None, None, None, None).await;
let updated = update_product(

View File

@ -541,7 +541,7 @@ WHERE buyer_id = $1
#[actix::test]
async fn create() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_shopping_cart_item(&mut t, None, None).await;
@ -550,7 +550,7 @@ WHERE buyer_id = $1
#[actix::test]
async fn all() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account_id = test_account(&mut t, None, None, None).await.id;
@ -581,7 +581,7 @@ WHERE buyer_id = $1
#[actix::test]
async fn account_cart_with_cart_id() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account_id = test_account(&mut t, None, None, None).await.id;
@ -618,7 +618,7 @@ WHERE buyer_id = $1
#[actix::test]
async fn account_cart_without_cart_id() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account_id = test_account(&mut t, None, None, None).await.id;
@ -655,7 +655,7 @@ WHERE buyer_id = $1
#[actix::test]
async fn update() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account_id = test_account(&mut t, None, None, None).await.id;
let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await;
let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await;

View File

@ -345,7 +345,7 @@ mod tests {
#[actix::test]
async fn create_shopping_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_account(&mut t, None, None, None).await;
@ -364,7 +364,7 @@ mod tests {
#[actix::test]
async fn update_shopping_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_account(&mut t, None, None, None).await;
@ -399,7 +399,7 @@ mod tests {
#[actix::test]
async fn without_cart_ensure_shopping_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_account(&mut t, None, None, None).await;
@ -429,7 +429,7 @@ mod tests {
#[actix::test]
async fn with_inactive_cart_ensure_shopping_cart() {
testx::db_t!(t);
testx::db_t_ref!(t);
let account = test_account(&mut t, None, None, None).await;

View File

@ -267,7 +267,7 @@ mod tests {
#[actix::test]
async fn create_stock() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_stock(&mut t, None, None, None).await;
@ -276,7 +276,7 @@ mod tests {
#[actix::test]
async fn products_stock() {
testx::db_t!(t);
testx::db_t_ref!(t);
let first = test_stock(&mut t, None, None, None).await;
let second = test_stock(&mut t, None, None, None).await;
@ -296,7 +296,7 @@ mod tests {
#[actix::test]
async fn all_stocks() {
testx::db_t!(t);
testx::db_t_ref!(t);
let first = test_stock(&mut t, None, None, None).await;
let second = test_stock(&mut t, None, None, None).await;
@ -309,7 +309,7 @@ mod tests {
#[actix::test]
async fn delete_stock() {
testx::db_t!(t);
testx::db_t_ref!(t);
let first = test_stock(&mut t, None, None, None).await;
let second = test_stock(&mut t, None, None, None).await;
@ -332,7 +332,7 @@ mod tests {
#[actix::test]
async fn update_stock() {
testx::db_t!(t);
testx::db_t_ref!(t);
let first = test_stock(&mut t, None, None, None).await;
let second = test_stock(&mut t, None, None, None).await;

View File

@ -1,5 +1,5 @@
use actix::Message;
use model::{AccountId, Audience, Token};
use model::{AccountId, Audience, Token, TokenId};
use crate::{db_async_handler, Result};
@ -138,6 +138,31 @@ RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not
crate::Error::Token(Error::Create)
})
}
#[derive(Message)]
#[rtype(result = "Result<Option<Token>>")]
pub struct DeleteToken {
pub token_id: TokenId,
}
db_async_handler!(DeleteToken, delete_token, Option<Token>, inner_delete_token);
pub(crate) async fn delete_token(
msg: DeleteToken,
t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<Token>> {
sqlx::query_as(r#"
DELETE FROM tokens
WHERE id = $1
RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
"#)
.bind(msg.token_id)
.fetch_optional(t)
.await
.map_err(|e| {
tracing::error!("{e:?}");
crate::Error::Token(Error::Jti)
})
}
#[cfg(test)]
mod tests {
@ -205,7 +230,7 @@ mod tests {
#[actix::test]
async fn create_token() {
testx::db_t!(t);
testx::db_t_ref!(t);
super::create_token(
CreateToken {
@ -222,7 +247,7 @@ mod tests {
#[actix::test]
async fn create_extended_token() {
testx::db_t!(t);
testx::db_t_ref!(t);
test_create_account(&mut t).await;
@ -231,7 +256,7 @@ mod tests {
#[actix::test]
async fn find_by_jti() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original = test_create_token_extended(&mut t, None, None, None, None, None).await;
@ -250,7 +275,7 @@ mod tests {
#[actix::test]
async fn find_by_jti_expired() {
testx::db_t!(t);
testx::db_t_ref!(t);
let original = test_create_token_extended(
&mut t,

View File

@ -217,7 +217,7 @@ pub(crate) async fn request_payment(
},
Error::UnavailableShoppingCart
);
let mut items =
let items =
cart_items
.iter()
.fold(HashMap::with_capacity(cart_items.len()), |mut agg, item| {
@ -233,60 +233,34 @@ pub(crate) async fn request_payment(
Error::UnavailableShoppingCart
);
// let payment_required = {
// let l = config.lock();
// l.payment().optional_payment() != false
// };
let redirect_uri = {
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = {
client
.lock()
.create_order(
pay_u::req::OrderCreate::build(
msg.buyer.into(),
msg.customer_ip,
msg.currency,
format!("Order #{}", db_order.id),
)
.map_err(|e| {
tracing::error!("{}", e);
Error::InvalidOrder
})?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(db_order.order_ext_id.to_string())
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await
.map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?
};
let redirect_uri = match msg.payment_method {
PaymentMethod::PayU => {
let (redirect_uri, ext_order_id) = pay_u_adapter::CreatePayment {
client,
buyer: msg.buyer,
customer_ip: msg.customer_ip,
currency: msg.currency,
description: format!("Order #{}", db_order.id),
cart_products,
items,
order_ext_id: db_order.order_ext_id.to_string(),
notify_uri,
continue_uri,
}
.create_payment()
.await?;
query_db!(
db,
database_manager::SetOrderServiceId {
service_order_id: order_id.0,
id: db_order.id,
},
Error::CreateOrder
);
redirect_uri
query_db!(
db,
database_manager::SetOrderServiceId {
service_order_id: ext_order_id.into_inner(),
id: db_order.id,
},
Error::CreateOrder
);
redirect_uri
}
PaymentMethod::PaymentOnTheSpot => unreachable!(),
};
let order_items = query_db!(

View File

@ -0,0 +1,69 @@
use std::collections::HashMap;
use model::*;
use crate::{Buyer, Error, PayUClient, Result};
pub struct CreatePayment {
pub client: PayUClient,
pub buyer: Buyer,
pub customer_ip: String,
pub currency: String,
pub description: String,
pub cart_products: Vec<model::Product>,
pub items: HashMap<ProductId, (model::Quantity, QuantityUnit)>,
pub order_ext_id: String,
pub notify_uri: String,
pub continue_uri: String,
}
impl CreatePayment {
pub(crate) async fn create_payment(self) -> Result<(String, ExtOrderId)> {
let CreatePayment {
client,
buyer,
customer_ip,
currency,
description,
cart_products,
mut items,
order_ext_id,
notify_uri,
continue_uri,
} = self;
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = client
.lock()
.create_order(
pay_u::req::OrderCreate::build(buyer.into(), customer_ip, currency, description)
.map_err(|e| {
tracing::error!("{}", e);
Error::InvalidOrder
})?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(order_ext_id)
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await
.map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?;
Ok((redirect_uri, ExtOrderId::new(order_id.0)))
}
}

View File

@ -35,3 +35,6 @@ sha2 = { version = "0.10", features = [] }
tokio = { version = "1.17", features = ["full"] }
futures = { version = "0.3", features = [] }
futures-util = { version = "0.3", features = [] }
[dev-dependencies]
testx = { path = "../../shared/testx" }

View File

@ -1,3 +1,75 @@
//! Tokens management system.
//! It's responsible for creating and validating all tokens.
//!
//! Application flow goes like this:
//!
//! ```ascii
//! Client API TokenManager Database
//!
//! │ │ │ │
//! │ │ │ │
//! │ │ │ ┌───────────────►│
//! ├────────────────►├──────────────────►├──────►│ │
//! │ Sign In │ CreatePair │ └───────────────►│
//! │ │ │ Create │
//! │ │ │ * AccessToken │
//! │ │ │ * RefreshToken │
//! │ │ │ │
//! │ │ │ │
//!
//! │ │ │ │
//! ├────────────────►├──────────────────►├───────────────────────►│
//! │ Validate token │ ValidateToken │ Load token │
//! │ │ (string) │◄───────────────────────┤
//! │ │ │ │
//! │ │ Is Valid? │
//! │ │ │ │
//! │ │◄──────────────── YES │
//! │ │ AccessToken │ │
//! │ │ │ │
//! │ │ │ │
//! │ │◄──────────────── NO │
//! │ │ Error │ │
//!
//! │ │ │ │
//! │ │ │ │
//! │ │ │ ┌───────────────►│
//! ├────────────────►├──────────────────►├──────►│ │
//! │ Refresh token │ CreatePair │ └───────────────►│
//! │ │ │ Create │
//! │ │◄──────────────────┤ * AccessToken │
//! │ │ Access Token │ * RefreshToken │
//! │ │ Refresh Token │ │
//! │ │ │ │
//! ```
//!
//! If you need to operate on tokens from API or any other actor you should
//! always use this actor and never touch database directly.
//!
//! # Examples
//!
//! ```
//! use actix::{Actor, Addr};
//! use config::SharedAppConfig;
//! use database_manager::Database;
//! use token_manager::*;
//! use model::*;
//!
//! async fn tokens(db: Addr<Database>, config: SharedAppConfig) {
//! let manager = TokenManager::new(config, db);
//!
//! let manager_addr = manager.start();
//!
//! let AuthPair { access_token, access_token_string, refresh_token_string, .. } = manager_addr.send(CreatePair {
//! customer_id: uuid::Uuid::new_v4(),
//! account_id: AccountId::from(0),
//! role: Role::Admin
//! }).await.unwrap().unwrap();
//!
//! manager_addr.send(Validate { token: access_token_string }).await.unwrap().unwrap();
//! }
//! ```
use std::collections::BTreeMap;
use std::str::FromStr;
@ -138,6 +210,22 @@ impl TokenManager {
}
}
/// Creates single token, it's mostly used by [CreatePair]
///
/// # Examples
///
/// ```
/// use actix::Addr;
/// use model::{AccountId, Role};
/// use token_manager::*;
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await {
/// Ok(Ok(pair)) => {}
/// Ok(Err(manager_error)) => {}
/// Err(actor_error) => {}
/// }
/// }
/// ```
#[derive(Message)]
#[rtype(result = "Result<(Token, AccessTokenString)>")]
pub struct CreateToken {
@ -256,12 +344,28 @@ pub struct AuthPair {
pub refresh_token_string: model::RefreshTokenString,
}
/// Creates access token and refresh token
///
/// # Examples
///
/// ```
/// use actix::Addr;
/// use model::{AccountId, Role};
/// use token_manager::CreatePair;
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await {
/// Ok(Ok(pair)) => {}
/// Ok(Err(manager_error)) => {}
/// Err(actor_error) => {}
/// }
/// }
/// ```
#[derive(Message)]
#[rtype(result = "Result<AuthPair>")]
pub struct CreatePair {
pub customer_id: uuid::Uuid,
pub role: Role,
pub id: AccountId,
pub account_id: AccountId,
}
token_async_handler!(CreatePair, create_pair, AuthPair);
@ -276,7 +380,7 @@ pub(crate) async fn create_pair(
CreateToken {
customer_id: msg.customer_id,
role: msg.role,
subject: msg.id,
subject: msg.account_id,
audience: Some(model::Audience::Web),
exp: None
},
@ -287,7 +391,7 @@ pub(crate) async fn create_pair(
CreateToken {
customer_id: msg.customer_id,
role: msg.role,
subject: msg.id,
subject: msg.account_id,
audience: Some(model::Audience::Web),
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
},
@ -305,6 +409,22 @@ pub(crate) async fn create_pair(
})
}
/// Checks if token is still valid
///
/// # Examples
///
/// ```
/// use actix::Addr;
/// use model::{AccessTokenString, AccountId, Role};
/// use token_manager::{CreatePair, Validate};
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>, token: AccessTokenString) {
/// match token_manager.send(Validate { token }).await {
/// Ok(Ok(pair)) => {}
/// Ok(Err(manager_error)) => {}
/// Err(actor_error) => {}
/// }
/// }
/// ```
#[derive(Message)]
#[rtype(result = "Result<Token>")]
pub struct Validate {
@ -349,30 +469,14 @@ pub(crate) async fn validate(
return Err(Error::Validate);
}
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "sub", token.subject, validate_num) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "nbt", &token.not_before_time, validate_time) {
return Err(Error::Invalid);
}
if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) {
return Err(Error::Invalid);
}
validate_pair(&claims, "cti", token.customer_id, validate_uuid)?;
validate_pair(&claims, "arl", token.role, eq)?;
validate_pair(&claims, "iss", &token.issuer, eq)?;
validate_pair(&claims, "sub", token.subject, validate_num)?;
validate_pair(&claims, "aud", token.audience, eq)?;
validate_pair(&claims, "exp", &token.expiration_time, validate_time)?;
validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?;
validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?;
tracing::info!("JWT token valid");
Ok(token)
@ -383,31 +487,155 @@ fn build_key(secret: String) -> Result<Hmac<Sha256>> {
Ok(key) => Ok(key),
Err(e) => {
tracing::error!("{e:?}");
dbg!(e);
Err(Error::ValidateInternal)
}
}
}
fn validate_pair<F, V>(claims: &BTreeMap<String, String>, key: &str, v: V, cmp: F) -> bool
#[inline(always)]
fn validate_pair<F, V>(
claims: &BTreeMap<String, String>,
key: &str,
v: V,
cmp: F,
) -> std::result::Result<(), Error>
where
F: FnOnce(&str, V) -> bool,
F: for<'s> FnOnce(V, &'s str) -> bool,
V: PartialEq,
{
claims.get(key).map(|s| cmp(s, v)).unwrap_or_default()
claims
.get(key)
.map(|s| cmp(v, s.as_str()))
.unwrap_or_default()
.then_some(())
.ok_or(Error::Invalid)
}
fn validate_time(left: &str, right: &NaiveDateTime) -> bool {
chrono::DateTime::parse_from_str(left, "%+")
.map(|t| t.naive_utc() == *right)
#[inline(always)]
fn eq<V>(value: V, text: &str) -> bool
where
V: for<'s> PartialEq<&'s str>,
{
value == text
}
#[inline(always)]
fn validate_time(left: &NaiveDateTime, right: &str) -> bool {
chrono::DateTime::parse_from_str(right, "%+")
.map(|t| t.naive_utc() == *left)
.unwrap_or_default()
}
fn validate_num(left: &str, right: i32) -> bool {
left.parse::<i32>().map(|n| n == right).unwrap_or_default()
#[inline(always)]
fn validate_num(left: i32, right: &str) -> bool {
right.parse::<i32>().map(|n| left == n).unwrap_or_default()
}
fn validate_uuid(left: &str, right: uuid::Uuid) -> bool {
uuid::Uuid::from_str(left)
.map(|u| u == right)
#[inline(always)]
fn validate_uuid(left: uuid::Uuid, right: &str) -> bool {
uuid::Uuid::from_str(right)
.map(|u| u == left)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use actix::Actor;
use config::UpdateConfig;
use database_manager::Database;
use model::*;
use super::*;
pub struct NoOpts;
impl UpdateConfig for NoOpts {}
#[actix::test]
async fn create_token() {
testx::db!(config, db);
let db = db.start();
let (token, _text) = super::create_token(
CreateToken {
customer_id: Default::default(),
role: Role::Admin,
subject: AccountId::from(1),
audience: None,
exp: None,
},
db.clone(),
config,
)
.await
.unwrap();
db.send(database_manager::DeleteToken { token_id: token.id })
.await
.ok();
}
#[actix::test]
async fn create_pair() {
testx::db!(config, db);
let db = db.start();
let AuthPair {
access_token,
access_token_string: _,
refresh_token_string: _,
_refresh_token,
} = super::create_pair(
CreatePair {
customer_id: Default::default(),
role: Role::Admin,
account_id: AccountId::from(0),
},
db.clone(),
config,
)
.await
.unwrap();
db.send(database_manager::DeleteToken {
token_id: access_token.id,
})
.await
.ok();
db.send(database_manager::DeleteToken {
token_id: _refresh_token.id,
})
.await
.ok();
}
#[actix::test]
async fn validate() {
testx::db!(config, db);
let db = db.start();
let (token, text) = super::create_token(
CreateToken {
customer_id: Default::default(),
role: Role::Admin,
subject: AccountId::from(1),
audience: None,
exp: None,
},
db.clone(),
config.clone(),
)
.await
.unwrap();
super::validate(Validate { token: text }, db.clone(), config.clone())
.await
.unwrap();
db.send(database_manager::DeleteToken { token_id: token.id })
.await
.ok();
}
}

View File

@ -55,7 +55,7 @@ async fn sign_in(
.send(token_manager::CreatePair {
customer_id: account.customer_id,
role: account.role,
id: account.id,
account_id: account.id,
})
.await
.map_err(|_| routes::Error::CriticalFailure)??;

View File

@ -52,7 +52,7 @@ async fn refresh_token(
.send(token_manager::CreatePair {
customer_id: account.customer_id,
role: account.role,
id: account.id,
account_id: account.id,
})
.await
.map_err(|_| routes::Error::CriticalFailure)??;

View File

@ -163,7 +163,7 @@ pub async fn create_account(
.send(token_manager::CreatePair {
customer_id: account.customer_id,
role: account.role,
id: account.id,
account_id: account.id,
})
.await
.map_err(|_| routes::Error::CriticalFailure)??;
@ -205,7 +205,7 @@ async fn sign_in(
.send(token_manager::CreatePair {
customer_id: account.customer_id,
role: account.role,
id: account.id,
account_id: account.id,
})
.await
.map_err(|_| routes::Error::CriticalFailure)??;

View File

@ -3,4 +3,4 @@
psql postgres postgres -c "DROP DATABASE bazzar_test"
psql postgres postgres -c "CREATE DATABASE bazzar_test"
sqlx migrate run --database-url='postgres://postgres@localhost/bazzar_test'
cargo test
cargo test --all

View File

@ -901,24 +901,34 @@ pub struct Stock {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)]
#[serde(transparent)]
pub struct OrderAddressId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)]
#[serde(transparent)]
pub struct OrderId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)]
#[serde(transparent)]
pub struct ExtOrderId(String);
impl ExtOrderId {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn into_inner(self) -> String {
self.0
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Serialize, Deserialize)]

View File

@ -1,5 +1,5 @@
#[macro_export]
macro_rules! db_t {
macro_rules! db_t_ref {
($t: ident) => {
let config = config::default_load(&mut NoOpts);
config
@ -13,6 +13,19 @@ macro_rules! db_t {
};
}
#[macro_export]
macro_rules! db {
($config: ident, $db: ident) => {
let $config = config::default_load(&mut NoOpts);
$config
.lock()
.database_mut()
.set_url("postgres://postgres@localhost/bazzar_test");
let $db = Database::build($config.clone()).await;
};
}
#[macro_export]
macro_rules! db_rollback {
($t: expr) => {