Token validation

This commit is contained in:
eraden 2022-04-18 22:07:52 +02:00
parent ad046bc389
commit 2c0104ec2d
30 changed files with 1204 additions and 137 deletions

2
.env
View File

@ -2,3 +2,5 @@ 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"
JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN"
PGDATESTYLE=

320
Cargo.lock generated
View File

@ -693,6 +693,21 @@ dependencies = [
"syn",
]
[[package]]
name = "actix-web-httpauth"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c25a48b4684f90520183cd1a688e5f4f7e9905835fa75d02c0fe4f60fcdbe6"
dependencies = [
"actix-service 2.0.2",
"actix-utils 3.0.0",
"actix-web 4.0.1",
"base64 0.13.0",
"futures-core",
"futures-util",
"pin-project-lite 0.2.8",
]
[[package]]
name = "actix-web-opentelemetry"
version = "0.12.0"
@ -958,6 +973,7 @@ dependencies = [
"actix-rt 2.7.0",
"actix-session",
"actix-web 4.0.1",
"actix-web-httpauth",
"actix-web-opentelemetry",
"argon2",
"chrono",
@ -966,13 +982,17 @@ dependencies = [
"futures",
"futures-util",
"gumdrop",
"hmac",
"jwt",
"log",
"oauth2",
"parking_lot 0.12.0",
"password-hash",
"pretty_env_logger",
"rand_core 0.6.3",
"serde",
"serde_json",
"sha2 0.10.2",
"sqlx",
"sqlx-core",
"tera",
@ -1028,6 +1048,15 @@ dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.5",
]
[[package]]
name = "block-buffer"
version = "0.10.2"
@ -1225,7 +1254,7 @@ dependencies = [
"hmac",
"percent-encoding",
"rand 0.8.5",
"sha2",
"sha2 0.10.2",
"subtle",
"time 0.3.9",
"version_check",
@ -1372,6 +1401,15 @@ dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.5",
]
[[package]]
name = "digest"
version = "0.10.3"
@ -1691,8 +1729,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1889,6 +1929,17 @@ dependencies = [
"itoa 1.0.1",
]
[[package]]
name = "http-body"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
dependencies = [
"bytes 1.1.0",
"http",
"pin-project-lite 0.2.8",
]
[[package]]
name = "http-range"
version = "0.1.5"
@ -1922,6 +1973,43 @@ dependencies = [
"quick-error",
]
[[package]]
name = "hyper"
version = "0.14.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"
dependencies = [
"bytes 1.1.0",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.13",
"http",
"http-body",
"httparse",
"httpdate",
"itoa 1.0.1",
"pin-project-lite 0.2.8",
"socket2 0.4.4",
"tokio 1.17.0",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
dependencies = [
"http",
"hyper",
"rustls 0.20.4",
"tokio 1.17.0",
"tokio-rustls 0.23.3",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -1988,9 +2076,15 @@ dependencies = [
"socket2 0.3.19",
"widestring",
"winapi 0.3.9",
"winreg",
"winreg 0.6.2",
]
[[package]]
name = "ipnet"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "ipnetwork"
version = "0.17.0"
@ -2036,6 +2130,21 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jwt"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
dependencies = [
"base64 0.13.0",
"crypto-common",
"digest 0.10.3",
"hmac",
"serde",
"serde_json",
"sha2 0.10.2",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -2396,6 +2505,26 @@ dependencies = [
"libc",
]
[[package]]
name = "oauth2"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e47cfc4c0a1a519d9a025ebfbac3a2439d1b5cdf397d72dcb79b11d9920dab"
dependencies = [
"base64 0.13.0",
"chrono",
"getrandom 0.2.6",
"http",
"rand 0.8.5",
"reqwest",
"serde",
"serde_json",
"serde_path_to_error",
"sha2 0.9.9",
"thiserror",
"url",
]
[[package]]
name = "object"
version = "0.27.1"
@ -2884,6 +3013,44 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "reqwest"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
dependencies = [
"base64 0.13.0",
"bytes 1.1.0",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.13",
"http",
"http-body",
"hyper",
"hyper-rustls",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"percent-encoding",
"pin-project-lite 0.2.8",
"rustls 0.20.4",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded 0.7.1",
"tokio 1.17.0",
"tokio-rustls 0.23.3",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.22.3",
"winreg 0.10.1",
]
[[package]]
name = "resolv-conf"
version = "0.6.3"
@ -2953,8 +3120,29 @@ dependencies = [
"base64 0.13.0",
"log",
"ring",
"sct",
"webpki",
"sct 0.6.1",
"webpki 0.21.4",
]
[[package]]
name = "rustls"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921"
dependencies = [
"log",
"ring",
"sct 0.7.0",
"webpki 0.22.0",
]
[[package]]
name = "rustls-pemfile"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
dependencies = [
"base64 0.13.0",
]
[[package]]
@ -2988,6 +3176,16 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -3040,6 +3238,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.6.1"
@ -3102,6 +3309,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]
name = "sha2"
version = "0.10.2"
@ -3241,11 +3461,11 @@ dependencies = [
"percent-encoding",
"rand 0.8.5",
"rust_decimal",
"rustls",
"rustls 0.19.1",
"serde",
"serde_json",
"sha-1 0.10.0",
"sha2",
"sha2 0.10.2",
"smallvec",
"sqlformat",
"sqlx-rt",
@ -3255,8 +3475,8 @@ dependencies = [
"tokio-stream",
"url",
"uuid",
"webpki",
"webpki-roots",
"webpki 0.21.4",
"webpki-roots 0.21.1",
"whoami",
]
@ -3273,7 +3493,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_json",
"sha2",
"sha2 0.10.2",
"sqlx-core",
"sqlx-rt",
"syn",
@ -3289,7 +3509,7 @@ dependencies = [
"actix-rt 2.7.0",
"once_cell",
"tokio 1.17.0",
"tokio-rustls",
"tokio-rustls 0.22.0",
]
[[package]]
@ -3597,9 +3817,20 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
"rustls",
"rustls 0.19.1",
"tokio 1.17.0",
"webpki",
"webpki 0.21.4",
]
[[package]]
name = "tokio-rustls"
version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e"
dependencies = [
"rustls 0.20.4",
"tokio 1.17.0",
"webpki 0.22.0",
]
[[package]]
@ -3678,6 +3909,12 @@ dependencies = [
"serde",
]
[[package]]
name = "tower-service"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.34"
@ -3760,6 +3997,12 @@ dependencies = [
"trust-dns-proto",
]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "twoway"
version = "0.2.2"
@ -3915,6 +4158,7 @@ dependencies = [
"idna",
"matches",
"percent-encoding",
"serde",
]
[[package]]
@ -3975,6 +4219,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
@ -4018,6 +4272,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.80"
@ -4067,13 +4333,32 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
dependencies = [
"webpki",
"webpki 0.21.4",
]
[[package]]
name = "webpki-roots"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf"
dependencies = [
"webpki 0.22.0",
]
[[package]]
@ -4187,6 +4472,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ws2_32-sys"
version = "0.2.1"

View File

@ -7,6 +7,7 @@ edition = "2021"
actix = { version = "0.13.0" }
actix-rt = { version = "2.7.0" }
actix-web = { version = "4.0.1" }
actix-web-httpauth = { version = "0.6.0" }
actix-auth = { version = "0.1.0" }
actix-cors = { version = "0.6.1" }
actix-files = { version = "0.6.0" }
@ -52,3 +53,9 @@ rand_core = { version = "0.6", features = ["std"] }
tokio = { version = "1.17.0", features = ["full"] }
futures = { version = "0.3.21" }
futures-util = { version = "0.3.21" }
jwt = { version = "0.16.0", features = [] }
hmac = { version = "0.12.1" }
sha2 = { version = "0.10.2" }
oauth2 = { version = "4.1.0" }

View File

@ -34,6 +34,10 @@
<div style="display: flex;justify-content: space-between;">
<div style="width: 49%">
<form style="width: 100%">
<fieldset>
<label for="bearer">bearer</label>
<input name="bearer" id="bearer" />
</fieldset>
<fieldset>
<label for="op">Operation</label>
<select name="op" id="op">
@ -77,11 +81,20 @@
const paramsEl = form.querySelector('#params');
const mthEl = form.querySelector('#method');
const opEL = form.querySelector('#op');
const bearerEl = form.querySelector('#bearer');
const send = (method, path, params) => {
const bearer = bearerEl.value || '';
const rest = method === 'GET'
? {}
: { body: JSON.stringify(params), headers: { 'Content-Type': 'application/json' } };
if (bearer.length) {
if (!rest.headers) rest.headers = {};
rest.headers["Authorization"] = `Bearer ${bearer}`;
}
path = method === 'GET'
? `${ path }?${ JSON.stringify(params) }`
: path;

View File

@ -1,10 +1,11 @@
use actix::{Actor, Addr, Context, Message};
use crate::database::Database;
use crate::model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartId, ShoppingCartItem,
ShoppingCartItemId, ShoppingCartState,
};
use crate::{cart_async_handler, database};
use actix::{Actor, Addr, Context, Handler, Message, ResponseActFuture, WrapFuture};
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -18,6 +19,10 @@ pub enum Error {
Db(#[from] database::Error),
#[error("Unable to update cart item")]
UpdateFailed,
#[error("Failed to change quantity")]
ChangeQuantity,
#[error("Shopping cart item {0} does not exists")]
NotExists(ShoppingCartItemId),
}
pub type Result<T> = std::result::Result<T, Error>;
@ -48,7 +53,12 @@ pub struct AddItem {
cart_async_handler!(AddItem, add_item, ShoppingCartItem);
async fn add_item(msg: AddItem, db: Addr<Database>) -> Result<ShoppingCartItem> {
match db.send(database::EnsureActiveShoppingCart { buyer_id: msg.buyer_id }).await {
match db
.send(database::EnsureActiveShoppingCart {
buyer_id: msg.buyer_id,
})
.await
{
Ok(Ok(_)) => {}
_ => return Err(Error::ShoppingCartFailed),
};
@ -83,6 +93,7 @@ async fn add_item(msg: AddItem, db: Addr<Database>) -> Result<ShoppingCartItem>
_ => Err(Error::CantAddItem),
}
}
#[derive(Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct RemoveProduct {
@ -115,3 +126,66 @@ pub(crate) async fn remove_product(
}
}
}
#[derive(Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct ChangeQuantity {
pub shopping_cart_id: ShoppingCartId,
pub shopping_cart_item_id: ShoppingCartItemId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
cart_async_handler!(ChangeQuantity, change_quantity, Option<ShoppingCartItem>);
pub(crate) async fn change_quantity(
msg: ChangeQuantity,
db: Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
if **msg.quantity == 0 {
return remove_product(
RemoveProduct {
shopping_cart_id: msg.shopping_cart_id,
shopping_cart_item_id: msg.shopping_cart_item_id,
},
db,
)
.await;
}
let item: ShoppingCartItem = match db
.send(database::FindShoppingCartItem {
id: msg.shopping_cart_item_id,
})
.await
{
Ok(Ok(row)) => row,
Ok(Err(db_err)) => {
log::error!("{db_err}");
return Err(Error::NotExists(msg.shopping_cart_item_id));
}
Err(act_err) => {
log::error!("{act_err:?}");
return Err(Error::NotExists(msg.shopping_cart_item_id));
}
};
match db
.send(database::UpdateShoppingCartItem {
id: msg.shopping_cart_item_id,
product_id: item.product_id,
shopping_cart_id: item.shopping_cart_id,
quantity: msg.quantity,
quantity_unit: msg.quantity_unit,
})
.await
{
Ok(Ok(row)) => Ok(Some(row)),
Ok(Err(db_err)) => {
log::error!("{db_err}");
Err(Error::ChangeQuantity)
}
Err(act_err) => {
log::error!("{act_err:?}");
Err(Error::ChangeQuantity)
}
}
}

View File

@ -1,13 +1,13 @@
use actix::{Actor, Context};
use sqlx::PgPool;
pub use account_orders::*;
pub use accounts::*;
use actix::{Actor, Context};
pub use order_items::*;
pub use products::*;
pub use shopping_cart_items::*;
pub use shopping_carts::*;
use sqlx::PgPool;
pub use stocks::*;
pub use tokens::*;
pub mod account_orders;
pub mod accounts;
@ -16,25 +16,28 @@ pub mod products;
pub mod shopping_cart_items;
pub mod shopping_carts;
pub mod stocks;
pub mod tokens;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to connect to database. {0:?}")]
Connect(sqlx::Error),
Connect(#[from] sqlx::Error),
#[error("{0}")]
Account(accounts::Error),
Account(#[from] accounts::Error),
#[error("{0}")]
AccountOrder(account_orders::Error),
AccountOrder(#[from] account_orders::Error),
#[error("{0}")]
Product(products::Error),
Product(#[from] products::Error),
#[error("{0}")]
Stock(stocks::Error),
Stock(#[from] stocks::Error),
#[error("{0}")]
OrderItem(order_items::Error),
OrderItem(#[from] order_items::Error),
#[error("{0}")]
ShoppingCart(shopping_carts::Error),
ShoppingCart(#[from] shopping_carts::Error),
#[error("{0}")]
ShoppingCartItem(shopping_cart_items::Error),
ShoppingCartItem(#[from] shopping_cart_items::Error),
#[error("{0}")]
Token(#[from] tokens::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@ -45,7 +48,9 @@ pub struct Database {
impl Clone for Database {
fn clone(&self) -> Self {
Self { pool: self.pool.clone() }
Self {
pool: self.pool.clone(),
}
}
}

View File

@ -1,11 +1,9 @@
use crate::db_async_handler;
use actix::{Handler, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use crate::database::Database;
use crate::model::*;
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
use crate::model::*;
#[derive(Debug, thiserror::Error)]
pub enum Error {

View File

@ -1,11 +1,9 @@
use crate::db_async_handler;
use actix::{Handler, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use crate::database::Database;
use crate::model::{AccountId, Email, FullAccount, Login, PassHash, Role};
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
use crate::model::{AccountId, Email, FullAccount, Login, PassHash, Role};
#[derive(Debug, thiserror::Error)]
pub enum Error {

View File

@ -1,11 +1,9 @@
use crate::db_async_handler;
use actix::{Handler, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use crate::database::Database;
use crate::model::*;
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
use crate::model::*;
#[derive(Debug, thiserror::Error)]
pub enum Error {

View File

@ -1,4 +1,4 @@
use actix::{Handler, Message, ResponseActFuture, WrapFuture};
use actix::Message;
use sqlx::PgPool;
use super::Result;

View File

@ -1,11 +1,9 @@
use crate::{database, db_async_handler};
use actix::{Handler, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::model::*;
use super::Result;
use crate::{database, db_async_handler};
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -24,14 +22,21 @@ pub enum Error {
#[error("Can't find shopping cart item doe to lack of identity")]
NoIdentity,
#[error("Failed to update shopping cart item with id {shopping_cart_item_id:?} and/or product id {product_id:?}")]
Update { shopping_cart_item_id: Option<ShoppingCartItemId>, product_id: Option<ProductId> },
Update {
shopping_cart_item_id: Option<ShoppingCartItemId>,
product_id: Option<ProductId>,
},
}
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<ShoppingCartItem>>")]
pub struct AllShoppingCartItems;
db_async_handler!(AllShoppingCartItems, all_shopping_cart_items, Vec<ShoppingCartItem>);
db_async_handler!(
AllShoppingCartItems,
all_shopping_cart_items,
Vec<ShoppingCartItem>
);
pub(crate) async fn all_shopping_cart_items(
_msg: AllShoppingCartItems,
@ -57,7 +62,11 @@ pub struct AccountShoppingCartItems {
pub account_id: AccountId,
}
db_async_handler!(AccountShoppingCartItems, account_shopping_cart_items, Vec<ShoppingCartItem>);
db_async_handler!(
AccountShoppingCartItems,
account_shopping_cart_items,
Vec<ShoppingCartItem>
);
pub(crate) async fn account_shopping_cart_items(
msg: AccountShoppingCartItems,
@ -88,7 +97,11 @@ pub struct CreateShoppingCartItem {
pub quantity_unit: QuantityUnit,
}
db_async_handler!(CreateShoppingCartItem, create_shopping_cart_item, ShoppingCartItem);
db_async_handler!(
CreateShoppingCartItem,
create_shopping_cart_item,
ShoppingCartItem
);
pub(crate) async fn create_shopping_cart_item(
msg: CreateShoppingCartItem,
@ -123,7 +136,11 @@ pub struct UpdateShoppingCartItem {
pub quantity_unit: QuantityUnit,
}
db_async_handler!(UpdateShoppingCartItem, update_shopping_cart_item, ShoppingCartItem);
db_async_handler!(
UpdateShoppingCartItem,
update_shopping_cart_item,
ShoppingCartItem
);
pub(crate) async fn update_shopping_cart_item(
msg: UpdateShoppingCartItem,
@ -156,7 +173,11 @@ pub struct FindShoppingCartItem {
pub id: ShoppingCartItemId,
}
db_async_handler!(FindShoppingCartItem, find_shopping_cart_item, ShoppingCartItem);
db_async_handler!(
FindShoppingCartItem,
find_shopping_cart_item,
ShoppingCartItem
);
pub(crate) async fn find_shopping_cart_item(
msg: FindShoppingCartItem,

View File

@ -1,11 +1,9 @@
use crate::db_async_handler;
use actix::{Handler, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use crate::database::Database;
use crate::model::*;
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
use crate::model::*;
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -52,7 +50,11 @@ pub struct AccountShoppingCarts {
pub state: Option<ShoppingCartState>,
}
db_async_handler!(AccountShoppingCarts, account_shopping_carts, Vec<ShoppingCart>);
db_async_handler!(
AccountShoppingCarts,
account_shopping_carts,
Vec<ShoppingCart>
);
pub(crate) async fn account_shopping_carts(
msg: AccountShoppingCarts,
@ -177,18 +179,22 @@ WHERE id = $1
}
#[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCart>>")]
#[rtype(result = "Result<ShoppingCart>")]
pub struct EnsureActiveShoppingCart {
pub buyer_id: AccountId,
}
db_async_handler!(EnsureActiveShoppingCart, ensure_active_shopping_cart, Option<ShoppingCart>);
db_async_handler!(
EnsureActiveShoppingCart,
ensure_active_shopping_cart,
ShoppingCart
);
pub(crate) async fn ensure_active_shopping_cart(
msg: EnsureActiveShoppingCart,
pool: PgPool,
) -> Result<Option<ShoppingCart>> {
sqlx::query_as(
) -> Result<ShoppingCart> {
if let Ok(Some(cart)) = sqlx::query_as(
r#"
INSERT INTO shopping_carts (buyer_id, state)
VALUES ($1, 'active')
@ -200,6 +206,22 @@ RETURNING id, buyer_id, payment_method, state;
.bind(msg.buyer_id)
.fetch_optional(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::ShoppingCart(Error::NotExists)
}) {
return Ok(cart);
};
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
FROM shopping_carts
WHERE buyer_id = $1 AND state = 'active'
"#,
)
.bind(msg.buyer_id)
.fetch_one(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::ShoppingCart(Error::NotExists)

View File

@ -1,4 +1,4 @@
use actix::{Handler, Message, ResponseActFuture, WrapFuture};
use actix::Message;
use sqlx::PgPool;
use super::Result;

View File

@ -0,0 +1,73 @@
use actix::Message;
use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::model::{AccountId, Audience, Token};
use crate::{database, db_async_handler, Role};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to save new token")]
Create,
#[error("Failed to find token by jti")]
Jti,
}
#[derive(Message)]
#[rtype(result = "Result<Token>")]
pub struct TokenByJti {
pub jti: String,
}
db_async_handler!(TokenByJti, token_by_jti, Token);
pub(crate) async fn token_by_jti(msg: TokenByJti, pool: PgPool) -> Result<Token> {
sqlx::query_as(r#"
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
FROM tokens
WHERE jwt_id = $1
"#)
.bind(msg.jti)
.fetch_one(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Token(Error::Jti)
})
}
#[derive(Message)]
#[rtype(result = "Result<Token>")]
pub struct CreateToken {
pub customer_id: uuid::Uuid,
pub role: Role,
pub subject: AccountId,
pub audience: Audience,
}
db_async_handler!(CreateToken, create_token, Token);
pub(crate) async fn create_token(msg: CreateToken, pool: PgPool) -> Result<Token> {
let CreateToken {
customer_id,
role,
subject,
audience,
} = msg;
sqlx::query_as(r#"
INSERT INTO tokens (customer_id, role, subject, audience)
VALUES ($1, $2, $3, $4)
RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
"#)
.bind(customer_id)
.bind(role)
.bind(subject)
.bind(audience)
.fetch_one(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Token(Error::Create)
})
}

View File

@ -1,2 +1,3 @@
pub mod cart_manager;
pub mod database;
pub mod token_manager;

View File

@ -0,0 +1,267 @@
use std::str::FromStr;
use std::sync::Arc;
use actix::{Addr, Message};
use chrono::prelude::*;
use crate::database::{Database, TokenByJti};
use crate::model::{AccountId, Audience, Token, TokenString};
use crate::{database, token_async_handler, Role};
struct Jwt {
/// cti (customer id): Customer uuid identifier used by payment service
pub cti: uuid::Uuid,
/// arl (account role): account role
pub arl: Role,
/// iss (issuer): Issuer of the JWT
pub iss: String,
/// sub (subject): Subject of the JWT (the user)
pub sub: i32,
/// aud (audience): Recipient for which the JWT is intended
pub aud: Audience,
/// exp (expiration time): Time after which the JWT expires
pub exp: chrono::NaiveDateTime,
/// nbt (not before time): Time before which the JWT must not be accepted
/// for processing
pub nbt: chrono::NaiveDateTime,
/// iat (issued at time): Time at which the JWT was issued; can be used to
/// determine age of the JWT,
pub iat: chrono::NaiveDateTime,
/// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
/// being replayed (allows a token to be used only once)
pub jti: uuid::Uuid,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unable to save new token")]
Save,
#[error("Unable to save new token. Can't connect to database")]
SaveInternal,
#[error("Unable to validate token")]
Validate,
#[error("Unable to validate token. Can't connect to database")]
ValidateInternal,
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct TokenManager {
db: Addr<Database>,
secret: Arc<String>,
}
impl actix::Actor for TokenManager {
type Context = actix::Context<Self>;
}
impl TokenManager {
pub fn new(db: Addr<Database>) -> Self {
let secret = Arc::new(std::env::var("JWT_SECRET").expect("JWT_SECRET is required"));
Self { db, secret }
}
}
#[derive(Message)]
#[rtype(result = "Result<(Token, TokenString)>")]
pub struct CreateToken {
pub customer_id: uuid::Uuid,
pub role: Role,
pub subject: AccountId,
pub audience: Option<Audience>,
}
token_async_handler!(CreateToken, create_token, (Token, TokenString));
async fn create_token(
msg: CreateToken,
db: Addr<Database>,
secret: Arc<String>,
) -> Result<(Token, TokenString)> {
let CreateToken {
customer_id,
role,
subject,
audience,
} = msg;
let audience = audience.unwrap_or_default();
let token: Token = match db
.send(database::CreateToken {
customer_id,
role,
subject,
audience,
})
.await
{
Ok(Ok(token)) => token,
Ok(Err(db_err)) => {
log::error!("{db_err}");
return Err(Error::Save);
}
Err(act_err) => {
log::error!("{act_err:?}");
return Err(Error::SaveInternal);
}
};
let token_string = {
use std::collections::BTreeMap;
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use sha2::Sha256;
let key: Hmac<Sha256> = match Hmac::new_from_slice(secret.as_bytes()) {
Ok(key) => key,
Err(e) => {
log::error!("{e:?}");
return Err(Error::SaveInternal);
}
};
let mut claims = BTreeMap::new();
// cti (customer id): Customer uuid identifier used by payment service
claims.insert("cti", format!("{}", token.customer_id));
// arl (account role): account role
claims.insert("arl", format!("{}", token.role.as_str()));
// iss (issuer): Issuer of the JWT
claims.insert("iss", format!("{}", token.issuer));
// sub (subject): Subject of the JWT (the user)
claims.insert("sub", format!("{}", token.subject));
// aud (audience): Recipient for which the JWT is intended
claims.insert("aud", format!("{}", token.audience.as_str()));
// exp (expiration time): Time after which the JWT expires
claims.insert("exp", format!("{}", token.expiration_time.format("%+")));
// nbt (not before time): Time before which the JWT must not be accepted
// for processing
claims.insert("nbt", format!("{}", token.not_before_time.format("%+")));
// iat (issued at time): Time at which the JWT was issued; can be used
// to determine age of the JWT,
claims.insert("iat", format!("{}", token.issued_at_time.format("%+")));
// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
// being replayed (allows a token to be used only once)
claims.insert("jti", format!("{}", token.jwt_id));
TokenString::from(match claims.sign_with_key(&key) {
Ok(s) => s,
Err(e) => {
log::error!("{e:?}");
return Err(Error::SaveInternal);
}
})
};
Ok((token, token_string))
}
#[derive(Message)]
#[rtype(result = "Result<(Token, bool)>")]
pub struct Validate {
pub token: TokenString,
}
token_async_handler!(Validate, validate, (Token, bool));
pub(crate) async fn validate(
msg: Validate,
db: Addr<Database>,
secret: Arc<String>,
) -> Result<(Token, bool)> {
use std::collections::BTreeMap;
use hmac::{Hmac, Mac};
use jwt::VerifyWithKey;
use sha2::Sha256;
log::info!("Validating token {:?}", msg.token);
let key: Hmac<Sha256> = match Hmac::new_from_slice(secret.as_bytes()) {
Ok(key) => key,
Err(e) => {
log::error!("{e:?}");
return Err(Error::ValidateInternal);
}
};
let claims: BTreeMap<String, String> = match msg.token.verify_with_key(&key) {
Ok(claims) => claims,
_ => return Err(Error::Validate),
};
let jti = match claims.get("jti") {
Some(jti) => jti,
_ => return Err(Error::Validate),
};
let token: Token = match db
.send(TokenByJti {
jti: String::from(jti),
})
.await
{
Ok(Ok(token)) => token,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::Validate);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::ValidateInternal);
}
};
match (claims.get("cti"), &token.customer_id) {
(Some(cti), id) => {
if !uuid::Uuid::from_str(cti)
.map(|u| u == *id)
.unwrap_or_default()
{
return Ok((token, false));
}
}
_ => return Ok((token, false)),
}
match (claims.get("arl"), &token.role) {
(Some(arl), role) if arl == role.as_str() => {}
_ => return Ok((token, false)),
}
match (claims.get("iss"), &token.issuer) {
(Some(iss), issuer) if iss == issuer => {}
_ => return Ok((token, false)),
}
match (claims.get("sub"), &token.subject) {
(Some(sub), subject) => {
if !sub
.parse::<i32>()
.map(|n| n == *subject)
.unwrap_or_default()
{
return Ok((token, false));
}
}
_ => return Ok((token, false)),
}
match (claims.get("aud"), &token.audience) {
(Some(aud), audience) if aud == audience.as_str() => {}
_ => return Ok((token, false)),
}
match (claims.get("exp"), &token.expiration_time) {
(Some(left), right) if validate_time(left, right) => {}
_ => return Ok((token, false)),
}
match (claims.get("nbt"), &token.not_before_time) {
(Some(left), right) if validate_time(left, right) => {}
_ => return Ok((token, false)),
}
match (claims.get("iat"), &token.issued_at_time) {
(Some(left), right) if validate_time(left, right) => {}
_ => return Ok((token, false)),
}
Ok((token, true))
}
fn validate_time(left: &str, right: &NaiveDateTime) -> bool {
chrono::DateTime::parse_from_str(left, "%+")
.map(|t| t.naive_utc() == *right)
.unwrap_or_default()
}

View File

@ -8,9 +8,11 @@ mod order_state;
pub fn encrypt_password(pass: &Password, salt: &SaltString) -> password_hash::Result<String> {
log::debug!("Hashing password {:?}", pass);
Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
.hash_password(pass.as_bytes(), &salt)?
.to_string())
Ok(
Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
.hash_password(pass.as_bytes(), &salt)?
.to_string(),
)
}
pub fn validate_password(pass: &Password, pass_hash: &PassHash) -> password_hash::Result<()> {

View File

@ -1,7 +1,11 @@
#![feature(stdio_locked)]
use std::io::{BufRead, Write};
use std::sync::Arc;
use actix::Actor;
use actix_session::{storage::RedisActorSessionStore, SessionMiddleware};
use actix_session::storage::RedisActorSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::middleware::Logger;
use actix_web::web::Data;
@ -10,7 +14,7 @@ use gumdrop::Options;
use password_hash::SaltString;
use validator::{validate_email, validate_length};
use crate::actors::database;
use crate::actors::{database, token_manager};
use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role};
@ -84,7 +88,12 @@ struct ServerOpts {
impl Default for ServerOpts {
fn default() -> Self {
Self { help: false, bind: "0.0.0.0".to_string(), port: 8080, db_url: None }
Self {
help: false,
bind: "0.0.0.0".to_string(),
port: 8080,
db_url: None,
}
}
}
@ -161,6 +170,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
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();
HttpServer::new(move || {
App::new()
@ -173,6 +183,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
))
.app_data(Data::new(config.clone()))
.app_data(Data::new(db.clone()))
.app_data(Data::new(token_manager.clone()))
.configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok))
})
@ -221,13 +232,26 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?,
None => {
let mut s = String::with_capacity(100);
std::io::stdin().read_line(&mut s).map_err(Error::ReadPass)?;
{
let mut std_out = std::io::stdout_locked();
let mut std_in = std::io::stdin_locked();
std_out
.write_all(b"PASS > ")
.expect("Failed to write to stdout");
std_out.flush().expect("Failed to write to stdout");
std_in.read_line(&mut s).map_err(Error::ReadPass)?;
}
if let Some(pos) = s.chars().position(|c| c == '\n') {
s.remove(pos);
}
s
}
};
if pass.trim().is_empty() {
panic!("Password cannot be empty!");
}
let config = Config::load();
let hash = encrypt_password(&Password(pass), &config.pass_salt).unwrap();

View File

@ -15,7 +15,7 @@ pub enum TransformError {
pub type RecordId = i32;
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename_all = "snake_case")]
pub enum OrderStatus {
#[display(fmt = "Potwierdzone")]
Confirmed,
@ -32,7 +32,7 @@ pub enum OrderStatus {
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename_all = "snake_case")]
pub enum Role {
#[display(fmt = "Adminitrator")]
Admin,
@ -40,8 +40,17 @@ pub enum Role {
User,
}
impl Role {
pub fn as_str(&self) -> &str {
match self {
Role::Admin => "Admin",
Role::User => "User",
}
}
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename_all = "snake_case")]
pub enum QuantityUnit {
Gram,
Decagram,
@ -50,19 +59,45 @@ pub enum QuantityUnit {
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename_all = "snake_case")]
pub enum PaymentMethod {
PayU,
PaymentOnTheSpot,
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename_all = "snake_case")]
pub enum ShoppingCartState {
Active,
Closed,
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")]
pub enum Audience {
Web,
Mobile,
Feed,
AdminPanel,
}
impl Audience {
pub fn as_str(&self) -> &str {
match self {
Audience::Web => "Web",
Audience::Mobile => "Mobile",
Audience::Feed => "Feed",
Audience::AdminPanel => "AdminPanel",
}
}
}
impl Default for Audience {
fn default() -> Self {
Self::Web
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
#[sqlx(transparent)]
#[serde(transparent)]
@ -167,7 +202,9 @@ impl<'de> serde::Deserialize<'de> for NonNegative {
}
}
Ok(NonNegative(deserializer.deserialize_i32(NonNegativeVisitor)?))
Ok(NonNegative(
deserializer.deserialize_i32(NonNegativeVisitor)?,
))
}
}
@ -294,7 +331,7 @@ impl PartialEq<PasswordConfirmation> for Password {
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, From)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct AccountId(RecordId);
@ -320,9 +357,22 @@ pub struct Account {
impl From<FullAccount> for Account {
fn from(
FullAccount { id, email, login, pass_hash: _, role, customer_id }: FullAccount,
FullAccount {
id,
email,
login,
pass_hash: _,
role,
customer_id,
}: FullAccount,
) -> Self {
Self { id, email, login, role, customer_id }
Self {
id,
email,
login,
role,
customer_id,
}
}
}
@ -424,7 +474,7 @@ pub struct ShoppingCart {
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ShoppingCartItemId(pub RecordId);
pub struct ShoppingCartItemId(RecordId);
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct ShoppingCartItem {
@ -434,3 +484,35 @@ pub struct ShoppingCartItem {
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct TokenId(RecordId);
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct Token {
pub id: TokenId,
pub customer_id: uuid::Uuid,
pub role: Role,
/// iss (issuer): Issuer of the JWT
pub issuer: String,
/// sub (subject): Subject of the JWT (the user)
pub subject: i32,
/// aud (audience): Recipient for which the JWT is intended
pub audience: Audience,
/// exp (expiration time): Time after which the JWT expires
pub expiration_time: chrono::NaiveDateTime,
/// nbt (not before time): Time before which the JWT must not be accepted
/// for processing
pub not_before_time: chrono::NaiveDateTime,
/// iat (issued at time): Time at which the JWT was issued; can be used to
/// determine age of the JWT,
pub issued_at_time: chrono::NaiveDateTime,
/// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
/// being replayed (allows a token to be used only once)
pub jwt_id: uuid::Uuid,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, Deref, Display, From)]
pub struct TokenString(String);

View File

@ -4,5 +4,9 @@ mod stocks;
use actix_web::web::{scope, ServiceConfig};
pub fn configure(config: &mut ServiceConfig) {
config.service(scope("/api/v1").configure(products::configure).configure(stocks::configure));
config.service(
scope("/api/v1")
.configure(products::configure)
.configure(stocks::configure),
);
}

View File

@ -1,4 +1,9 @@
use crate::database;
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
use serde::Deserialize;
use crate::database::Database;
use crate::model::{
Days, PriceMajor, PriceMinor, ProductCategory, ProductId, ProductLongDesc, ProductName,
@ -6,13 +11,7 @@ use crate::model::{
};
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::{admin_send_db, routes};
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
use serde::Deserialize;
use crate::{admin_send_db, database, routes};
#[get("/products")]
async fn products(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
@ -100,9 +99,14 @@ async fn delete_product(
db: Data<Addr<Database>>,
Json(payload): Json<DeleteProduct>,
) -> routes::Result<HttpResponse> {
session.require_admin()?;
let _ = session.require_admin()?;
admin_send_db!(db, database::DeleteProduct { product_id: payload.id });
admin_send_db!(
db,
database::DeleteProduct {
product_id: payload.id
}
);
}
pub fn configure(config: &mut ServiceConfig) {

View File

@ -1,16 +1,15 @@
use crate::database;
use crate::database::Database;
use crate::model::{ProductId, Quantity, QuantityUnit, StockId};
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::{admin_send_db, routes};
use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
use serde::Deserialize;
use crate::database::Database;
use crate::model::{ProductId, Quantity, QuantityUnit, StockId};
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::{admin_send_db, database, routes};
#[get("/stocks")]
async fn stocks(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
session.require_admin()?;
@ -83,9 +82,18 @@ async fn delete_stock(
) -> routes::Result<HttpResponse> {
session.require_admin()?;
admin_send_db!(db, database::DeleteStock { stock_id: payload.id });
admin_send_db!(
db,
database::DeleteStock {
stock_id: payload.id
}
);
}
pub fn configure(config: &mut ServiceConfig) {
config.service(stocks).service(create_stock).service(update_stock).service(delete_stock);
config
.service(stocks)
.service(create_stock)
.service(update_stock)
.service(delete_stock);
}

View File

@ -1,11 +1,12 @@
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 std::sync::Arc;
use crate::database::{AccountByIdentity, Database};
use crate::logic::encrypt_password;
@ -69,18 +70,23 @@ async fn sign_in(
) -> Result<HttpResponse> {
log::debug!("{:?}", payload);
let db = db.into_inner();
let user: model::FullAccount =
match db.send(AccountByIdentity { email: payload.email, login: payload.login }).await {
Ok(Ok(user)) => user,
Ok(Err(e)) => {
log::error!("{}", e);
return Err(routes::Error::Unauthorized);
}
Err(e) => {
log::error!("{}", e);
return Err(routes::Error::Unauthorized);
}
};
let user: model::FullAccount = match db
.send(AccountByIdentity {
email: payload.email,
login: payload.login,
})
.await
{
Ok(Ok(user)) => user,
Ok(Err(e)) => {
log::error!("{}", e);
return Err(routes::Error::Unauthorized);
}
Err(e) => {
log::error!("{}", e);
return Err(routes::Error::Unauthorized);
}
};
if let Err(e) = crate::logic::validate_password(&payload.password, &user.pass_hash) {
log::error!("Password validation failed. {}", e);
Err(routes::Error::Unauthorized)

View File

@ -1,13 +1,17 @@
pub mod admin;
pub mod public;
use crate::model::RecordId;
use crate::routes;
use std::fmt::{Debug, Display, Formatter};
use actix_session::Session;
use actix_web::body::BoxBody;
use actix_web::web::ServiceConfig;
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
use std::fmt::{Debug, Display, Formatter};
pub use admin::Error as AdminError;
pub use public::{Error as PublicError, V1Error, V1ShoppingCartError};
use crate::model::RecordId;
use crate::routes;
pub trait RequireLogin {
fn require_admin(&self) -> Result<RecordId>;
@ -25,14 +29,23 @@ impl RequireLogin for Session {
}
}
#[derive(Debug, derive_more::From)]
pub enum Error {
#[from(ignore)]
Unauthorized,
Admin(routes::admin::Error),
Public(routes::public::Error),
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
impl From<V1Error> for Error {
fn from(v1: V1Error) -> Self {
Self::Public(PublicError::ApiV1(v1))
}
}
impl From<V1ShoppingCartError> for Error {
fn from(sv1: V1ShoppingCartError) -> Self {
Self::Public(PublicError::ApiV1(V1Error::ShoppingCart(sv1)))
}
}
@ -45,7 +58,8 @@ impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Error::Unauthorized => String::from("Unauthorized"),
Error::Admin(e) => format!("{}", e),
Error::Admin(e) => format!("{e}"),
Error::Public(e) => format!("{e}"),
};
f.write_str(&serde_json::to_string(&Failure { errors: vec![msg] }).unwrap())
}
@ -61,9 +75,16 @@ impl Responder for Error {
Error::Unauthorized => HttpResponse::Unauthorized()
.content_type("application/json")
.body(format!("{}", self)),
Error::Admin(_) => HttpResponse::InternalServerError()
Error::Public(PublicError::DatabaseConnection)
| Error::Public(PublicError::Database(..))
| Error::Admin(..) => HttpResponse::InternalServerError()
.content_type("application/json")
.body(format!("{}", self)),
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
.content_type("application/json")
.body(format!("{}", self)),
},
}
}
}
@ -71,5 +92,7 @@ impl Responder for Error {
pub type Result<T> = std::result::Result<T, Error>;
pub fn configure(config: &mut ServiceConfig) {
config.configure(public::configure).configure(admin::configure);
config
.configure(public::configure)
.configure(admin::configure);
}

View File

@ -1,26 +1,43 @@
mod api_v1;
pub mod api_v1;
use actix_web::web::ServiceConfig;
use actix_web::{get, HttpResponse};
pub use api_v1::{Error as V1Error, ShoppingCartError as V1ShoppingCartError};
use crate::database;
#[macro_export]
macro_rules! public_send_db {
($db: expr, $msg: expr) => {{
use crate::routes::PublicError;
let db = $db;
return match db.send($msg).await {
Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)),
Ok(Err(e)) => {
log::error!("{}", e);
Err(crate::routes::Error::Admin(Error::Database(e)))
Err(crate::routes::Error::Public(PublicError::Database(e)))
}
Err(e) => {
log::error!("{}", e);
Err(crate::routes::Error::Admin(Error::DatabaseConnection))
Err(crate::routes::Error::Public(
PublicError::DatabaseConnection,
))
}
};
}};
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
ApiV1(#[from] api_v1::Error),
#[error("Internal server error")]
DatabaseConnection,
#[error("{0}")]
Database(#[from] database::Error),
}
#[get("/")]
async fn landing() -> HttpResponse {
HttpResponse::NotImplemented().body("")

View File

@ -1,12 +1,28 @@
use actix::Addr;
use actix_web::dev::ServiceRequest;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::{get, HttpResponse};
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
use actix_web_httpauth::middleware::HttpAuthentication;
use crate::database;
use crate::database::Database;
use crate::public_send_db;
use crate::routes::admin::Error;
use crate::model::{AccountId, TokenString};
use crate::routes::Result;
use crate::token_manager::TokenManager;
use crate::{database, public_send_db, token_manager};
#[derive(Debug, thiserror::Error)]
pub enum ShoppingCartError {
#[error("Shopping cart can't be found or created")]
Ensure,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
ShoppingCart(ShoppingCartError),
}
#[get("/products")]
async fn products(db: Data<Addr<Database>>) -> Result<HttpResponse> {
@ -18,6 +34,60 @@ async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database::AllStocks)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(scope("/api/v1").service(products).service(stocks));
#[get("/shopping_cart")]
async fn shopping_cart(db: Data<Addr<Database>>, credentials: BearerAuth) -> Result<HttpResponse> {
let _t = credentials.token();
match db
.send(database::EnsureActiveShoppingCart {
buyer_id: AccountId::from(1),
})
.await
{
Ok(Ok(cart)) => Ok(HttpResponse::Ok().json(cart)),
Ok(Err(e)) => {
log::error!("{e}");
Err(ShoppingCartError::Ensure.into())
}
Err(e) => {
log::error!("{e:?}");
Err(ShoppingCartError::Ensure.into())
}
}
}
pub fn configure(config: &mut ServiceConfig) {
let bearer_auth = HttpAuthentication::bearer(validator);
config.service(
scope("/api/v1")
.service(products)
.service(stocks)
.service(scope("").wrap(bearer_auth).service(shopping_cart)),
);
}
async fn validator(
req: ServiceRequest,
credentials: BearerAuth,
) -> std::result::Result<ServiceRequest, actix_web::Error> {
let tm = match req.app_data::<Data<Addr<TokenManager>>>() {
Some(db) => db,
_ => panic!("DB must be configured"),
};
if let Ok(Ok((_, true))) = tm
.send(token_manager::Validate {
token: TokenString::from(String::from(credentials.token())),
})
.await
{
return Ok(req);
};
let config = req
.app_data::<Config>()
.map(|data| data.clone())
.unwrap_or_else(Default::default)
.scope("account=user");
Err(AuthenticationError::from(config).into())
}

View File

@ -1,10 +1,11 @@
#[macro_export]
macro_rules! db_async_handler {
($msg: ty, $async: ident, $res: ty) => {
impl Handler<$msg> for Database {
type Result = ResponseActFuture<Self, Result<$res>>;
impl actix::Handler<$msg> for Database {
type Result = actix::ResponseActFuture<Self, Result<$res>>;
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture;
let pool = self.pool.clone();
Box::pin(async { $async(msg, pool).await }.into_actor(self))
}
@ -15,13 +16,30 @@ macro_rules! db_async_handler {
#[macro_export]
macro_rules! cart_async_handler {
($msg: ty, $async: ident, $res: ty) => {
impl Handler<$msg> for CartManager {
type Result = ResponseActFuture<Self, Result<$res>>;
impl actix::Handler<$msg> for CartManager {
type Result = actix::ResponseActFuture<Self, Result<$res>>;
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))
}
}
};
}
#[macro_export]
macro_rules! token_async_handler {
($msg: ty, $async: ident, $res: ty) => {
impl actix::Handler<$msg> for TokenManager {
type Result = actix::ResponseActFuture<Self, Result<$res>>;
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))
}
}
};
}

View File

@ -0,0 +1,27 @@
CREATE TYPE "Audience" AS ENUM (
'web',
'mobile',
'feed',
'admin_panel'
);
CREATE TABLE tokens (
id serial not null primary key,
customer_id uuid not null,
role "Role" not null,
-- standard fields
-- iss (issuer): Issuer of the JWT
issuer varchar not null default 'bazzar',
-- sub (subject): Subject of the JWT (the user)
subject int not null /* account_id */ ,
-- aud (audience): Recipient for which the JWT is intended
audience "Audience" not null default 'web',
-- exp (expiration time): Time after which the JWT expires
expiration_time timestamp not null default now() + interval '2 weeks',
-- nbt (not before time): Time before which the JWT must not be accepted for processing
not_before_time timestamp not null default now() - interval '1 minute',
-- iat (issued at time): Time at which the JWT was issued; can be used to determine age of the JWT,
issued_at_time timestamp not null default now(),
-- jti (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once)
jwt_id uuid not null default gen_random_uuid()
);

View File

@ -0,0 +1,4 @@
ALTER TABLE tokens
ADD CONSTRAINT unit_jit UNIQUE (jti);
--SET datestyle = '';

View File

@ -1,2 +1,7 @@
max_width = 100
use_small_heuristics = "Max"
imports_granularity = "Module"
group_imports = "StdExternalCrate"
reorder_modules = true
reorder_imports = true
use_field_init_shorthand = true
wrap_comments = true
edition = "2021"