diff --git a/Cargo.lock b/Cargo.lock
index d139885..f740127 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -976,6 +976,7 @@ dependencies = [
"actix-web-httpauth",
"actix-web-opentelemetry",
"argon2",
+ "async-trait",
"chrono",
"derive_more",
"dotenv",
diff --git a/api/Cargo.toml b/api/Cargo.toml
index 211dd7b..43694d6 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -59,3 +59,5 @@ hmac = { version = "0.12.1" }
sha2 = { version = "0.10.2" }
oauth2 = { version = "4.1.0" }
+
+async-trait = { version = "0.1.53" }
diff --git a/api/assets/index.html b/api/assets/index.html
index efe161b..77c2dc8 100644
--- a/api/assets/index.html
+++ b/api/assets/index.html
@@ -15,7 +15,6 @@
fieldset {
display: flex;
justify-content: space-between;
- width: 600px;
}
fieldset > label {
@@ -36,14 +35,27 @@
@@ -92,7 +105,7 @@
if (bearer.length) {
if (!rest.headers) rest.headers = {};
- rest.headers["Authorization"] = `Bearer ${bearer}`;
+ rest.headers["Authorization"] = `Bearer ${ bearer }`;
}
path = method === 'GET'
@@ -112,32 +125,82 @@
opEL.addEventListener('change', () => {
switch (opEL.value) {
- case 'auto-login': {
- paramsEl.value = `login=Eraden\npassword=text`
+ case 'api-v1-sign-in': {
+ mthEl.value = 'POST';
+ urlEl.value = '/api/v1/sign-in';
+ paramsEl.value = 'login=Eraden\npassword=test';
+ break;
+ }
+ case 'api-v1-products': {
+ mthEl.value = 'GET';
+ urlEl.value = '/api/v1/products';
+ paramsEl.value = '';
+ break;
+ }
+ case 'api-v1-stocks': {
+ mthEl.value = 'GET';
+ urlEl.value = '/api/v1/stocks';
+ paramsEl.value = '';
+ break;
+ }
+ case 'api-v1-shopping-cart': {
+ mthEl.value = 'GET';
+ urlEl.value = '/api/v1/shopping-cart';
+ paramsEl.value = '';
+ break;
+ }
+ case 'api-v1-shopping-cart-items': {
+ mthEl.value = 'GET';
+ urlEl.value = '/api/v1/shopping-cart-items';
+ paramsEl.value = '';
+ break;
+ }
+
+ /// admin
+ case 'admin-auto-login': {
+ paramsEl.value = `login=Eraden\npassword=test`
mthEl.value = 'POST';
urlEl.value = '/admin/sign-in';
break;
}
- case 'get-products': {
+ case 'admin-get-products': {
mthEl.value = 'GET';
urlEl.value = '/admin/api/v1/products';
+ paramsEl.value = '';
break;
}
- case 'create-product': {
- const p = {
+ case 'admin-create-product': {
+ paramsEl.value = serializeParams({
name: 'Foo',
short_description: 'asd',
long_description: 'asjdoiajd ajio djaso idja s',
- price_major: 12,
- price_minor: 0,
- };
- paramsEl.value = Object.entries(p).map(([k, v]) => `${ k }=${ v }`).join('\n');
+ price: 1200,
+ deliver_days_flag: ["monday"]
+ });
mthEl.value = 'POST';
urlEl.value = '/admin/api/v1/product';
break;
}
+ case 'admin-create-stock': {
+ paramsEl.value = serializeParams({
+ product_id: 1,
+ quantity: 456,
+ quantity_unit: 'gram'
+ });
+ mthEl.value = 'POST';
+ urlEl.value = '/admin/api/v1/stock';
+ break;
+ }
}
- })
+ });
+
+ const serializeParams = (p) =>
+ Object.entries(p).map(([k, v]) => {
+ let value = Array.isArray(v)
+ ? `[${ v.join(',') }]`
+ : v
+ return `${ k }=${ value }`
+ }).join('\n');
form.addEventListener('submit', (ev) => {
ev.preventDefault();
@@ -152,7 +215,11 @@
let [k, ...v] = s.split("=");
v = Array(v).join('=')
try {
- v = JSON.parse(v);
+ if (v.match(/\-?\d+/)) {
+ v = JSON.parse(v);
+ } else if (v.startsWith("[") && v.endsWith(']')) {
+ v = v.substring(1, v.length - 1).split(',').map(s => s.trim())
+ }
} catch (_) {
}
params[k] = v;
diff --git a/api/src/actors/database/accounts.rs b/api/src/actors/database/accounts.rs
index 4e72b80..756a8bb 100644
--- a/api/src/actors/database/accounts.rs
+++ b/api/src/actors/database/accounts.rs
@@ -3,7 +3,7 @@ use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
-use crate::model::{AccountId, Email, FullAccount, Login, PassHash, Role};
+use crate::model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role};
#[derive(Debug, thiserror::Error)]
pub enum Error {
@@ -26,7 +26,7 @@ db_async_handler!(AllAccounts, all_accounts, Vec);
pub(crate) async fn all_accounts(_msg: AllAccounts, pool: PgPool) -> Result> {
sqlx::query_as(
r#"
-SELECT id, email, login, pass_hash, role, customer_id
+SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
"#,
)
@@ -54,7 +54,7 @@ pub(crate) async fn create_account(msg: CreateAccount, db: PgPool) -> Result,
pub role: Role,
+ pub state: AccountState,
}
db_async_handler!(UpdateAccount, update_account, FullAccount);
pub(crate) async fn update_account(msg: UpdateAccount, db: PgPool) -> Result {
- sqlx::query_as(
- r#"
+ match msg.pass_hash {
+ Some(hash) => sqlx::query_as(
+ r#"
+UPDATE accounts
+SET login = $2 AND email = $3 AND role = $4 AND pass_hash = $5 AND state = $6
+WHERE id = $1
+RETURNING id, email, login, pass_hash, role, customer_id, state
+ "#,
+ )
+ .bind(msg.login)
+ .bind(msg.email)
+ .bind(msg.role)
+ .bind(hash)
+ .bind(msg.state),
+ None => sqlx::query_as(
+ r#"
UPDATE accounts
SET login = $2 AND email = $3 AND role = $4 AND pass_hash = $5
WHERE id = $1
-RETURNING id, email, login, pass_hash, role, customer_id
+RETURNING id, email, login, pass_hash, role, customer_id, state
"#,
- )
- .bind(msg.login)
- .bind(msg.email)
- .bind(msg.role)
- .bind(msg.pass_hash)
+ )
+ .bind(msg.login)
+ .bind(msg.email)
+ .bind(msg.role)
+ .bind(msg.state),
+ }
.fetch_one(&db)
.await
.map_err(|e| {
@@ -113,7 +129,7 @@ db_async_handler!(FindAccount, find_account, FullAccount);
pub(crate) async fn find_account(msg: FindAccount, db: PgPool) -> Result {
sqlx::query_as(
r#"
-SELECT id, email, login, pass_hash, role, customer_id
+SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
WHERE id = $1
"#,
@@ -140,7 +156,7 @@ pub(crate) async fn account_by_identity(msg: AccountByIdentity, db: PgPool) -> R
match (msg.login, msg.email) {
(Some(login), None) => sqlx::query_as(
r#"
-SELECT id, email, login, pass_hash, role, customer_id
+SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
WHERE login = $1
"#,
@@ -148,7 +164,7 @@ WHERE login = $1
.bind(login),
(None, Some(email)) => sqlx::query_as(
r#"
-SELECT id, email, login, pass_hash, role, customer_id
+SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
WHERE email = $1
"#,
@@ -156,7 +172,7 @@ WHERE email = $1
.bind(email),
(Some(login), Some(email)) => sqlx::query_as(
r#"
-SELECT id, email, login, pass_hash, role, customer_id
+SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
WHERE login = $1 AND email = $2
"#,
diff --git a/api/src/actors/database/products.rs b/api/src/actors/database/products.rs
index b5c39e9..1636f53 100644
--- a/api/src/actors/database/products.rs
+++ b/api/src/actors/database/products.rs
@@ -4,8 +4,8 @@ use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::model::{
- Days, PriceMajor, PriceMinor, Product, ProductCategory, ProductId, ProductLongDesc,
- ProductName, ProductShortDesc,
+ Days, Price, Product, ProductCategory, ProductId, ProductLongDesc, ProductName,
+ ProductShortDesc,
};
use crate::{database, model};
@@ -35,8 +35,7 @@ SELECT id,
short_description,
long_description,
category,
- price_major,
- price_minor,
+ price,
deliver_days_flag
FROM products
"#,
@@ -56,8 +55,7 @@ pub struct CreateProduct {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option,
- pub price_major: PriceMajor,
- pub price_minor: PriceMinor,
+ pub price: Price,
pub deliver_days_flag: Days,
}
@@ -66,15 +64,14 @@ crate::db_async_handler!(CreateProduct, create_product, Product);
pub(crate) async fn create_product(msg: CreateProduct, pool: PgPool) -> Result {
sqlx::query_as(
r#"
-INSERT INTO products (name, short_description, long_description, category, price_major, price_minor)
-VALUES ($1, $2, $3, $4, $5, $6, $7)
+INSERT INTO products (name, short_description, long_description, category, price, deliver_days_flag)
+VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
name,
short_description,
long_description,
category,
- price_major,
- price_minor,
+ price,
deliver_days_flag
"#,
)
@@ -82,8 +79,7 @@ RETURNING id,
.bind(msg.short_description)
.bind(msg.long_description)
.bind(msg.category)
- .bind(msg.price_major)
- .bind(msg.price_minor)
+ .bind(msg.price)
.bind(msg.deliver_days_flag)
.fetch_one(&pool)
.await
@@ -101,8 +97,7 @@ pub struct UpdateProduct {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option,
- pub price_major: PriceMajor,
- pub price_minor: PriceMinor,
+ pub price: Price,
pub deliver_days_flag: Days,
}
@@ -116,17 +111,15 @@ SET name = $2 AND
short_description = $3 AND
long_description = $4 AND
category = $5 AND
- price_major = $6 AND
- price_minor = $7 AND
- deliver_days_flag = $8
+ price = $6 AND
+ deliver_days_flag = $7
WHERE id = $1
RETURNING id,
name,
short_description,
long_description,
category,
- price_major,
- price_minor,
+ price,
deliver_days_flag
"#,
)
@@ -135,8 +128,7 @@ RETURNING id,
.bind(msg.short_description)
.bind(msg.long_description)
.bind(msg.category)
- .bind(msg.price_major)
- .bind(msg.price_minor)
+ .bind(msg.price)
.bind(msg.deliver_days_flag)
.fetch_one(&pool)
.await
@@ -164,8 +156,7 @@ RETURNING id,
short_description,
long_description,
category,
- price_major,
- price_minor,
+ price,
deliver_days_flag
"#,
)
diff --git a/api/src/actors/database/shopping_cart_items.rs b/api/src/actors/database/shopping_cart_items.rs
index 03bad94..8113a15 100644
--- a/api/src/actors/database/shopping_cart_items.rs
+++ b/api/src/actors/database/shopping_cart_items.rs
@@ -74,9 +74,15 @@ pub(crate) async fn account_shopping_cart_items(
) -> Result> {
sqlx::query_as(
r#"
-SELECT id, product_id, shopping_cart_id, quantity, quantity_unit
+SELECT shopping_cart_items.id as id,
+ shopping_cart_items.product_id as product_id,
+ shopping_cart_items.shopping_cart_id as shopping_cart_id,
+ shopping_cart_items.quantity as quantity,
+ shopping_cart_items.quantity_unit as quantity_unit
FROM shopping_cart_items
-WHERE buyer_id = $1
+LEFT JOIN shopping_carts
+ ON shopping_carts.id = shopping_cart_id
+WHERE shopping_carts.buyer_id = $1
"#,
)
.bind(msg.account_id)
diff --git a/api/src/actors/database/stocks.rs b/api/src/actors/database/stocks.rs
index bf5a56c..9e7cf59 100644
--- a/api/src/actors/database/stocks.rs
+++ b/api/src/actors/database/stocks.rs
@@ -52,7 +52,7 @@ crate::db_async_handler!(CreateStock, create_stock, Stock);
async fn create_stock(msg: CreateStock, pool: PgPool) -> Result {
sqlx::query_as(
r#"
-INSERT INTO stocks (product_id, quantity)
+INSERT INTO stocks (product_id, quantity, quantity_unit)
VALUES ($1, $2, $3)
RETURNING id, product_id, quantity, quantity_unit
"#,
diff --git a/api/src/actors/database/tokens.rs b/api/src/actors/database/tokens.rs
index 89b88af..445d35d 100644
--- a/api/src/actors/database/tokens.rs
+++ b/api/src/actors/database/tokens.rs
@@ -17,7 +17,7 @@ pub enum Error {
#[derive(Message)]
#[rtype(result = "Result")]
pub struct TokenByJti {
- pub jti: String,
+ pub jti: uuid::Uuid,
}
db_async_handler!(TokenByJti, token_by_jti, Token);
diff --git a/api/src/actors/token_manager.rs b/api/src/actors/token_manager.rs
index 60f9908..828ef5e 100644
--- a/api/src/actors/token_manager.rs
+++ b/api/src/actors/token_manager.rs
@@ -12,7 +12,7 @@ use crate::database::{Database, TokenByJti};
use crate::model::{AccountId, Audience, Token, TokenString};
use crate::{database, token_async_handler, Role};
-struct Jwt {
+/*struct Jwt {
/// cti (customer id): Customer uuid identifier used by payment service
pub cti: uuid::Uuid,
/// arl (account role): account role
@@ -34,7 +34,7 @@ struct Jwt {
/// 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 {
@@ -119,32 +119,51 @@ pub(crate) async fn create_token(
// 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()));
+ claims.insert("arl", String::from(token.role.as_str()));
// iss (issuer): Issuer of the JWT
- claims.insert("iss", format!("{}", token.issuer));
+ claims.insert("iss", token.issuer.to_string());
// 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()));
+ claims.insert("aud", String::from(token.audience.as_str()));
// exp (expiration time): Time after which the JWT expires
- claims.insert("exp", format!("{}", token.expiration_time.format("%+")));
+ claims.insert(
+ "exp",
+ format!(
+ "{}",
+ Utc.from_utc_datetime(&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("%+")));
+ claims.insert(
+ "nbt",
+ format!(
+ "{}",
+ Utc.from_utc_datetime(&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("%+")));
+ claims.insert(
+ "iat",
+ format!(
+ "{}",
+ Utc.from_utc_datetime(&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) {
+ let s = match claims.sign_with_key(&key) {
Ok(s) => s,
Err(e) => {
log::error!("{e:?}");
return Err(Error::SaveInternal);
}
- })
+ };
+ TokenString::from(s)
};
Ok((token, token_string))
}
@@ -178,7 +197,10 @@ pub(crate) async fn validate(
let token: Token = match db
.send(TokenByJti {
- jti: String::from(jti),
+ jti: match uuid::Uuid::from_str(jti) {
+ Ok(uid) => uid,
+ _ => return Err(Error::Validate),
+ },
})
.await
{
@@ -196,26 +218,18 @@ pub(crate) async fn validate(
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
return Ok((token, false));
}
- // if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
- // return Ok((token, false));
- // }
- match (claims.get("arl"), &token.role) {
- (Some(arl), role) if role == arl.as_str() => {}
- _ => return Ok((token, false)),
+ if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
+ return Ok((token, false));
}
- match (claims.get("iss"), &token.issuer) {
- (Some(iss), issuer) if iss == issuer => {}
- _ => return Ok((token, false)),
+ if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) {
+ return Ok((token, false));
}
if !validate_pair(&claims, "sub", token.subject, validate_num) {
return Ok((token, false));
}
-
- match (claims.get("aud"), &token.audience) {
- (Some(aud), audience) if aud == audience.as_str() => {}
- _ => return Ok((token, false)),
+ if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) {
+ return Ok((token, false));
}
-
if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) {
return Ok((token, false));
}
@@ -226,6 +240,7 @@ pub(crate) async fn validate(
return Ok((token, false));
}
+ log::info!("JWT token valid");
Ok((token, true))
}
diff --git a/api/src/main.rs b/api/src/main.rs
index e08e21e..17b357e 100644
--- a/api/src/main.rs
+++ b/api/src/main.rs
@@ -1,6 +1,4 @@
-#![feature(stdio_locked)]
-
-use std::io::{BufRead, Write};
+use std::io::Write;
use std::sync::Arc;
use actix::Actor;
@@ -201,12 +199,12 @@ async fn migrate(opts: MigrateOpts) -> Result<()> {
let res: std::result::Result<(), MigrateError> =
sqlx::migrate!("../db/migrate").run(db.pool()).await;
match res {
- Ok(()) => return Ok(()),
+ Ok(()) => Ok(()),
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
- };
+ }
}
async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> {
@@ -233,8 +231,8 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
None => {
let mut s = String::with_capacity(100);
{
- let mut std_out = std::io::stdout_locked();
- let mut std_in = std::io::stdin_locked();
+ let mut std_out = std::io::stdout();
+ let std_in = std::io::stdin();
std_out
.write_all(b"PASS > ")
@@ -253,12 +251,12 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
panic!("Password cannot be empty!");
}
let config = Config::load();
- let hash = encrypt_password(&Password(pass), &config.pass_salt).unwrap();
+ let hash = encrypt_password(&Password::from(pass), &config.pass_salt).unwrap();
db.send(database::CreateAccount {
- email: Email(opts.email),
- login: Login(opts.login),
- pass_hash: PassHash(hash),
+ email: Email::from(opts.email),
+ login: Login::from(opts.login),
+ pass_hash: PassHash::from(hash),
role,
})
.await
diff --git a/api/src/model.rs b/api/src/model.rs
index 37b2054..2f7f022 100644
--- a/api/src/model.rs
+++ b/api/src/model.rs
@@ -16,6 +16,7 @@ pub type RecordId = i32;
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum OrderStatus {
#[display(fmt = "Potwierdzone")]
Confirmed,
@@ -33,6 +34,7 @@ pub enum OrderStatus {
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)]
#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum Role {
#[display(fmt = "Adminitrator")]
Admin,
@@ -40,9 +42,9 @@ pub enum Role {
User,
}
-impl PartialEq for Role {
- fn eq(&self, other: &str) -> bool {
- self.as_str() == other
+impl PartialEq<&str> for Role {
+ fn eq(&self, other: &&str) -> bool {
+ self.as_str() == *other
}
}
@@ -56,16 +58,21 @@ impl Role {
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
-#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum QuantityUnit {
+ #[sqlx(rename = "g")]
Gram,
+ #[sqlx(rename = "dkg")]
Decagram,
+ #[sqlx(rename = "kg")]
Kilogram,
- Unit,
+ #[sqlx(rename = "piece")]
+ Piece,
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum PaymentMethod {
PayU,
PaymentOnTheSpot,
@@ -73,13 +80,15 @@ pub enum PaymentMethod {
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum ShoppingCartState {
Active,
Closed,
}
-#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
+#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)]
#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
pub enum Audience {
Web,
Mobile,
@@ -87,6 +96,12 @@ pub enum Audience {
AdminPanel,
}
+impl PartialEq<&str> for Audience {
+ fn eq(&self, other: &&str) -> bool {
+ self.as_str() == *other
+ }
+}
+
impl Audience {
pub fn as_str(&self) -> &str {
match self {
@@ -98,6 +113,15 @@ impl Audience {
}
}
+#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)]
+#[sqlx(rename_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
+pub enum AccountState {
+ Active,
+ Suspended,
+ Banned,
+}
+
impl Default for Audience {
fn default() -> Self {
Self::Web
@@ -107,14 +131,9 @@ impl Default for Audience {
#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct PriceMajor(NonNegative);
+pub struct Price(NonNegative);
-#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct PriceMinor(NonNegative);
-
-#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
+#[derive(sqlx::Type, Serialize, Deserialize, Default, Deref, From)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct Quantity(NonNegative);
@@ -127,15 +146,15 @@ impl TryFrom for Quantity {
}
}
-#[derive(sqlx::Type, Deserialize, Serialize, Deref, Debug)]
+#[derive(sqlx::Type, Deserialize, Serialize, Debug, Deref, From, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct Login(pub String);
+pub struct Login(String);
-#[derive(sqlx::Type, Serialize, Deref, Debug)]
+#[derive(sqlx::Type, Serialize, Debug, Deref, From, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct Email(pub String);
+pub struct Email(String);
impl<'de> serde::Deserialize<'de> for Email {
fn deserialize(deserializer: D) -> Result
@@ -166,7 +185,7 @@ impl<'de> serde::Deserialize<'de> for Email {
}
}
-#[derive(sqlx::Type, Serialize, Deref)]
+#[derive(sqlx::Type, Serialize, Default, Deref, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct NonNegative(i32);
@@ -176,7 +195,7 @@ impl TryFrom for NonNegative {
fn try_from(value: i32) -> Result {
if value < 0 {
- return Err(TransformError::BelowMinimal);
+ Err(TransformError::BelowMinimal)
} else {
Ok(Self(value))
}
@@ -206,6 +225,40 @@ impl<'de> serde::Deserialize<'de> for NonNegative {
Err(E::custom("Value must be equal or greater than 0"))
}
}
+
+ fn visit_i64(self, v: i64) -> Result
+ where
+ E: Error,
+ {
+ let v = v
+ .try_into()
+ .map_err(|_| E::custom("Value must be equal or greater than 0"))?;
+ if v >= 0 {
+ Ok(v)
+ } else {
+ Err(E::custom("Value must be equal or greater than 0"))
+ }
+ }
+
+ fn visit_u32(self, v: u32) -> Result
+ where
+ E: Error,
+ {
+ let v = v
+ .try_into()
+ .map_err(|_| E::custom("Value must be equal or greater than 0"))?;
+ Ok(v)
+ }
+
+ fn visit_u64(self, v: u64) -> Result
+ where
+ E: Error,
+ {
+ let v = v
+ .try_into()
+ .map_err(|_| E::custom("Value must be equal or greater than 0"))?;
+ Ok(v)
+ }
}
Ok(NonNegative(
@@ -316,20 +369,20 @@ where
}
}
-#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
+#[derive(sqlx::Type, Serialize, Deserialize, Debug, Deref, From, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct Password(pub String);
+pub struct Password(String);
-#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
+#[derive(sqlx::Type, Serialize, Deserialize, Debug, Deref, From, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct PasswordConfirmation(pub String);
+pub struct PasswordConfirmation(String);
-#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
+#[derive(sqlx::Type, Serialize, Deserialize, Debug, Deref, From, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
-pub struct PassHash(pub String);
+pub struct PassHash(String);
impl PartialEq for Password {
fn eq(&self, other: &PasswordConfirmation) -> bool {
@@ -350,6 +403,7 @@ pub struct FullAccount {
pub pass_hash: PassHash,
pub role: Role,
pub customer_id: uuid::Uuid,
+ pub state: AccountState,
}
#[derive(sqlx::FromRow, Serialize, Deserialize)]
@@ -359,6 +413,7 @@ pub struct Account {
pub login: Login,
pub role: Role,
pub customer_id: uuid::Uuid,
+ pub state: AccountState,
}
impl From for Account {
@@ -370,6 +425,7 @@ impl From for Account {
pass_hash: _,
role,
customer_id,
+ state,
}: FullAccount,
) -> Self {
Self {
@@ -378,6 +434,7 @@ impl From for Account {
login,
role,
customer_id,
+ state,
}
}
}
@@ -414,8 +471,7 @@ pub struct Product {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option,
- pub price_major: PriceMajor,
- pub price_minor: PriceMinor,
+ pub price: Price,
pub deliver_days_flag: Days,
}
diff --git a/api/src/routes/admin/api_v1.rs b/api/src/routes/admin/api_v1.rs
index 5862211..616b022 100644
--- a/api/src/routes/admin/api_v1.rs
+++ b/api/src/routes/admin/api_v1.rs
@@ -1,3 +1,4 @@
+mod accounts;
mod products;
mod stocks;
@@ -7,6 +8,7 @@ pub fn configure(config: &mut ServiceConfig) {
config.service(
scope("/api/v1")
.configure(products::configure)
- .configure(stocks::configure),
+ .configure(stocks::configure)
+ .configure(accounts::configure),
);
}
diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs
new file mode 100644
index 0000000..13c8fe0
--- /dev/null
+++ b/api/src/routes/admin/api_v1/accounts.rs
@@ -0,0 +1,128 @@
+use std::sync::Arc;
+
+use actix::Addr;
+use actix_session::Session;
+use actix_web::web::{Data, Json, ServiceConfig};
+use actix_web::{get, patch, post, HttpResponse};
+
+use crate::database::{self, Database};
+use crate::model::{AccountId, AccountState, PasswordConfirmation};
+use crate::routes::admin::Error;
+use crate::routes::RequireLogin;
+use crate::{
+ admin_send_db, encrypt_password, routes, Config, Email, Login, PassHash, Password, Role,
+};
+
+#[get("/accounts")]
+pub async fn accounts(session: Session, db: Data>) -> routes::Result {
+ session.require_admin()?;
+ let accounts = admin_send_db!(db, database::AllAccounts);
+ Ok(HttpResponse::Ok().json(accounts))
+}
+
+#[derive(serde::Deserialize)]
+pub struct UpdateAccountInput {
+ pub id: AccountId,
+ pub email: Email,
+ pub login: Login,
+ pub password: Option,
+ pub password_confirmation: Option,
+ pub role: Role,
+ pub state: AccountState,
+}
+
+#[patch("/account")]
+pub async fn update_account(
+ session: Session,
+ db: Data>,
+ Json(payload): Json,
+ config: Data>,
+) -> routes::Result {
+ session.require_admin()?;
+
+ let hash = match (payload.password, payload.password_confirmation) {
+ (None, None) => None,
+ (Some(p1), Some(p2)) => {
+ if p1 != p2 {
+ return Err(routes::Error::Admin(
+ routes::admin::Error::DifferentPasswords,
+ ));
+ }
+ let hash = match encrypt_password(&p1, &config.pass_salt) {
+ Ok(hash) => hash,
+ Err(e) => {
+ log::error!("{e:?}");
+ return Err(routes::Error::Admin(routes::admin::Error::HashPass));
+ }
+ };
+ Some(PassHash::from(hash))
+ }
+ _ => {
+ return Err(routes::Error::Admin(
+ routes::admin::Error::DifferentPasswords,
+ ))
+ }
+ };
+
+ let account = admin_send_db!(
+ db,
+ database::UpdateAccount {
+ id: payload.id,
+ email: payload.email,
+ login: payload.login,
+ pass_hash: hash,
+ role: payload.role,
+ state: payload.state,
+ }
+ );
+ Ok(HttpResponse::Ok().json(account))
+}
+
+#[derive(serde::Deserialize)]
+pub struct CreateAccountInput {
+ pub email: Email,
+ pub login: Login,
+ pub password: Password,
+ pub password_confirmation: PasswordConfirmation,
+ pub role: Role,
+}
+
+#[post("/account")]
+pub async fn create_account(
+ session: Session,
+ db: Data>,
+ Json(payload): Json,
+ config: Data>,
+) -> routes::Result {
+ session.require_admin()?;
+ if payload.password != payload.password_confirmation {
+ return Err(routes::Error::Admin(
+ routes::admin::Error::DifferentPasswords,
+ ));
+ }
+ let hash = match encrypt_password(&payload.password, &config.pass_salt) {
+ Ok(hash) => hash,
+ Err(e) => {
+ log::error!("{e:?}");
+ return Err(routes::Error::Admin(routes::admin::Error::HashPass));
+ }
+ };
+
+ let account = admin_send_db!(
+ db,
+ database::CreateAccount {
+ email: payload.email,
+ login: payload.login,
+ pass_hash: PassHash::from(hash),
+ role: payload.role,
+ }
+ );
+ Ok(HttpResponse::Ok().json(account))
+}
+
+pub fn configure(config: &mut ServiceConfig) {
+ config
+ .service(accounts)
+ .service(update_account)
+ .service(create_account);
+}
diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs
index b416457..f087180 100644
--- a/api/src/routes/admin/api_v1/products.rs
+++ b/api/src/routes/admin/api_v1/products.rs
@@ -6,8 +6,8 @@ use serde::Deserialize;
use crate::database::Database;
use crate::model::{
- Days, PriceMajor, PriceMinor, ProductCategory, ProductId, ProductLongDesc, ProductName,
- ProductShortDesc,
+ Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc,
+ Quantity, QuantityUnit,
};
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
@@ -17,7 +17,8 @@ use crate::{admin_send_db, database, routes};
async fn products(session: Session, db: Data>) -> routes::Result {
session.require_admin()?;
- admin_send_db!(db, database::AllProducts);
+ let products = admin_send_db!(db, database::AllProducts);
+ Ok(HttpResponse::Ok().json(products))
}
#[derive(Deserialize)]
@@ -27,8 +28,7 @@ pub struct UpdateProduct {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option,
- pub price_major: PriceMajor,
- pub price_minor: PriceMinor,
+ pub price: Price,
pub deliver_days_flag: Days,
}
@@ -40,7 +40,7 @@ async fn update_product(
) -> routes::Result {
session.require_admin()?;
- admin_send_db!(
+ let product = admin_send_db!(
db,
database::UpdateProduct {
id: payload.id,
@@ -48,11 +48,11 @@ async fn update_product(
short_description: payload.short_description,
long_description: payload.long_description,
category: payload.category,
- price_major: payload.price_major,
- price_minor: payload.price_minor,
+ price: payload.price,
deliver_days_flag: payload.deliver_days_flag,
}
);
+ Ok(HttpResponse::Ok().json(product))
}
#[derive(Deserialize)]
@@ -61,8 +61,7 @@ pub struct CreateProduct {
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option,
- pub price_major: PriceMajor,
- pub price_minor: PriceMinor,
+ pub price: Price,
pub deliver_days_flag: Days,
}
@@ -74,18 +73,26 @@ async fn create_product(
) -> routes::Result {
session.require_admin()?;
- admin_send_db!(
- db,
+ let product = admin_send_db!(
+ db.clone(),
database::CreateProduct {
name: payload.name,
short_description: payload.short_description,
long_description: payload.long_description,
category: payload.category,
- price_major: payload.price_major,
- price_minor: payload.price_minor,
+ price: payload.price,
deliver_days_flag: payload.deliver_days_flag,
}
);
+ let _ = admin_send_db!(
+ db,
+ database::CreateStock {
+ product_id: product.id,
+ quantity: Quantity::try_from(0).unwrap_or_default(),
+ quantity_unit: QuantityUnit::Piece,
+ }
+ );
+ Ok(HttpResponse::Created().json(product))
}
#[derive(Deserialize)]
@@ -101,12 +108,13 @@ async fn delete_product(
) -> routes::Result {
let _ = session.require_admin()?;
- admin_send_db!(
+ let product = admin_send_db!(
db,
database::DeleteProduct {
product_id: payload.id
}
);
+ Ok(HttpResponse::Ok().json(product))
}
pub fn configure(config: &mut ServiceConfig) {
diff --git a/api/src/routes/admin/api_v1/stocks.rs b/api/src/routes/admin/api_v1/stocks.rs
index 91d2ff2..1af40cc 100644
--- a/api/src/routes/admin/api_v1/stocks.rs
+++ b/api/src/routes/admin/api_v1/stocks.rs
@@ -14,7 +14,8 @@ use crate::{admin_send_db, database, routes};
async fn stocks(session: Session, db: Data>) -> routes::Result {
session.require_admin()?;
- admin_send_db!(db, database::AllStocks);
+ let stocks = admin_send_db!(db, database::AllStocks);
+ Ok(HttpResponse::Created().json(stocks))
}
#[derive(Deserialize)]
@@ -33,7 +34,7 @@ async fn update_stock(
) -> routes::Result {
session.require_admin()?;
- admin_send_db!(
+ let stock = admin_send_db!(
db,
database::UpdateStock {
id: payload.id,
@@ -42,6 +43,7 @@ async fn update_stock(
quantity_unit: payload.quantity_unit
}
);
+ Ok(HttpResponse::Created().json(stock))
}
#[derive(Deserialize)]
@@ -59,7 +61,7 @@ async fn create_stock(
) -> routes::Result {
session.require_admin()?;
- admin_send_db!(
+ let stock = admin_send_db!(
db,
database::CreateStock {
product_id: payload.product_id,
@@ -67,6 +69,7 @@ async fn create_stock(
quantity_unit: payload.quantity_unit
}
);
+ Ok(HttpResponse::Created().json(stock))
}
#[derive(Deserialize)]
@@ -82,12 +85,13 @@ async fn delete_stock(
) -> routes::Result {
session.require_admin()?;
- admin_send_db!(
+ let stock = admin_send_db!(
db,
database::DeleteStock {
stock_id: payload.id
}
);
+ Ok(HttpResponse::Created().json(stock))
}
pub fn configure(config: &mut ServiceConfig) {
diff --git a/api/src/routes/admin/mod.rs b/api/src/routes/admin/mod.rs
index e5eea04..52f8e60 100644
--- a/api/src/routes/admin/mod.rs
+++ b/api/src/routes/admin/mod.rs
@@ -18,17 +18,17 @@ use crate::{database, model, routes, Config};
macro_rules! admin_send_db {
($db: expr, $msg: expr) => {{
let db = $db;
- return match db.send($msg).await {
- Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)),
+ match db.send($msg).await {
+ Ok(Ok(res)) => res,
Ok(Err(e)) => {
log::error!("{}", e);
- Err(crate::routes::Error::Admin(Error::Database(e)))
+ return Err(crate::routes::Error::Admin(Error::Database(e)));
}
Err(e) => {
log::error!("{}", e);
- Err(crate::routes::Error::Admin(Error::DatabaseConnection))
+ return Err(crate::routes::Error::Admin(Error::DatabaseConnection));
}
- };
+ }
}};
}
@@ -40,6 +40,8 @@ pub enum Error {
HashPass,
#[error("Internal server error")]
DatabaseConnection,
+ #[error("Password and password confirmation are different")]
+ DifferentPasswords,
#[error("{0}")]
Database(#[from] database::Error),
}
@@ -146,7 +148,7 @@ async fn register(
.send(database::CreateAccount {
email: input.email,
login: input.login,
- pass_hash: PassHash(hash),
+ pass_hash: PassHash::from(hash),
role: input.role,
})
.await
diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs
index e5b4290..d6b6333 100644
--- a/api/src/routes/mod.rs
+++ b/api/src/routes/mod.rs
@@ -2,16 +2,20 @@ pub mod admin;
pub mod public;
use std::fmt::{Debug, Display, Formatter};
+use std::sync::Arc;
+use actix::Addr;
use actix_session::Session;
use actix_web::body::BoxBody;
use actix_web::web::ServiceConfig;
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
-pub use admin::Error as AdminError;
-pub use public::{Error as PublicError, V1Error, V1ShoppingCartError};
+use serde::Serialize;
-use crate::model::RecordId;
-use crate::routes;
+pub use self::admin::Error as AdminError;
+pub use self::public::{Error as PublicError, V1Error, V1ShoppingCartError};
+use crate::model::{RecordId, Token, TokenString};
+use crate::token_manager::TokenManager;
+use crate::{routes, token_manager};
pub trait RequireLogin {
fn require_admin(&self) -> Result;
@@ -65,6 +69,12 @@ impl Display for Error {
}
}
+#[derive(Serialize)]
+struct ReqFailure {
+ success: bool,
+ msg: String,
+}
+
impl ResponseError for Error {}
impl Responder for Error {
@@ -74,17 +84,34 @@ impl Responder for Error {
match self {
Error::Unauthorized => HttpResponse::Unauthorized()
.content_type("application/json")
- .body(format!("{}", self)),
+ .json(ReqFailure {
+ success: false,
+ msg: format!("{}", self),
+ }),
Error::Public(PublicError::DatabaseConnection)
| Error::Public(PublicError::Database(..))
| Error::Admin(..) => HttpResponse::InternalServerError()
.content_type("application/json")
- .body(format!("{}", self)),
+ .json(ReqFailure {
+ success: false,
+ msg: format!("{}", self),
+ }),
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
.content_type("application/json")
- .body(format!("{}", self)),
+ .json(ReqFailure {
+ success: false,
+ msg: format!("{}", self),
+ }),
},
+ Error::Public(PublicError::ApiV1(V1Error::AddItem | V1Error::RemoveItem)) => {
+ HttpResponse::BadRequest()
+ .content_type("application/json")
+ .json(ReqFailure {
+ success: false,
+ msg: format!("{}", self),
+ })
+ }
}
}
}
@@ -96,3 +123,24 @@ pub fn configure(config: &mut ServiceConfig) {
.configure(public::configure)
.configure(admin::configure);
}
+
+#[async_trait::async_trait]
+pub trait RequireUser {
+ async fn require_user(&self, tm: Arc>) -> Result<(Token, bool)>;
+}
+
+#[async_trait::async_trait]
+impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
+ async fn require_user(&self, tm: Arc>) -> Result<(Token, bool)> {
+ match tm
+ .send(token_manager::Validate {
+ token: TokenString::from(String::from(self.token())),
+ })
+ .await
+ {
+ Ok(Ok(res)) => Ok(res),
+ Ok(Err(_e)) => Err(Error::Unauthorized),
+ Err(_) => Err(Error::Unauthorized),
+ }
+ }
+}
diff --git a/api/src/routes/public/api_v1.rs b/api/src/routes/public/api_v1.rs
index 3763848..6ec4829 100644
--- a/api/src/routes/public/api_v1.rs
+++ b/api/src/routes/public/api_v1.rs
@@ -1,16 +1,7 @@
-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;
+mod restricted;
+mod unrestricted;
-use crate::database::Database;
-use crate::model::{AccountId, TokenString};
-use crate::routes::Result;
-use crate::token_manager::TokenManager;
-use crate::{database, public_send_db, token_manager};
+use actix_web::web::{scope, ServiceConfig};
#[derive(Debug, thiserror::Error)]
pub enum ShoppingCartError {
@@ -22,72 +13,17 @@ pub enum ShoppingCartError {
pub enum Error {
#[error("{0}")]
ShoppingCart(ShoppingCartError),
-}
-#[get("/products")]
-async fn products(db: Data>) -> Result {
- public_send_db!(db.into_inner(), database::AllProducts)
-}
-
-#[get("/stocks")]
-async fn stocks(db: Data>) -> Result {
- public_send_db!(db.into_inner(), database::AllStocks)
-}
-
-#[get("/shopping_cart")]
-async fn shopping_cart(db: Data>, credentials: BearerAuth) -> Result {
- 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())
- }
- }
+ #[error("Failed to remove shopping cart item")]
+ RemoveItem,
+ #[error("Failed to add shopping cart item")]
+ AddItem,
}
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)),
+ .configure(unrestricted::configure)
+ .configure(restricted::configure),
);
}
-
-async fn validator(
- req: ServiceRequest,
- credentials: BearerAuth,
-) -> std::result::Result {
- let tm = match req.app_data::>>() {
- 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::()
- .map(|data| data.clone())
- .unwrap_or_else(Default::default)
- .scope("account=user");
-
- Err(AuthenticationError::from(config).into())
-}
diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs
new file mode 100644
index 0000000..3e8f2e9
--- /dev/null
+++ b/api/src/routes/public/api_v1/restricted.rs
@@ -0,0 +1,197 @@
+use actix::Addr;
+use actix_web::web::{scope, Data, Json, ServiceConfig};
+use actix_web::{delete, get, post, HttpResponse};
+use actix_web_httpauth::extractors::bearer::BearerAuth;
+
+use crate::actors::cart_manager;
+use crate::actors::cart_manager::CartManager;
+use crate::database::Database;
+use crate::model::{
+ AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartItem,
+ ShoppingCartItemId,
+};
+use crate::routes::public::api_v1::ShoppingCartError;
+use crate::routes::public::Error as PublicError;
+use crate::routes::{RequireUser, Result};
+use crate::token_manager::TokenManager;
+use crate::{database, routes};
+
+#[get("/shopping-cart")]
+async fn shopping_cart(
+ db: Data>,
+ tm: Data>,
+ credentials: BearerAuth,
+) -> Result {
+ let (token, _) = credentials.require_user(tm.into_inner()).await?;
+ match db
+ .send(database::EnsureActiveShoppingCart {
+ buyer_id: AccountId::from(token.subject),
+ })
+ .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())
+ }
+ }
+}
+
+#[get("/shopping-cart-items")]
+async fn shopping_cart_items(
+ db: Data>,
+ tm: Data>,
+ credentials: BearerAuth,
+) -> Result {
+ let (token, _) = credentials.require_user(tm.into_inner()).await?;
+
+ let cart: ShoppingCart = match db
+ .send(database::EnsureActiveShoppingCart {
+ buyer_id: AccountId::from(token.subject),
+ })
+ .await
+ {
+ Ok(Ok(cart)) => cart,
+ Ok(Err(e)) => {
+ log::error!("{e}");
+ return Err(ShoppingCartError::Ensure.into());
+ }
+ Err(e) => {
+ log::error!("{e:?}");
+ return Err(ShoppingCartError::Ensure.into());
+ }
+ };
+ match db
+ .send(database::AccountShoppingCartItems {
+ account_id: cart.buyer_id,
+ })
+ .await
+ {
+ Ok(Ok(items)) => Ok(HttpResponse::Ok().json(items)),
+ Ok(Err(e)) => {
+ log::error!("{e}");
+ Err(ShoppingCartError::Ensure.into())
+ }
+ Err(e) => {
+ log::error!("{e:?}");
+ Err(ShoppingCartError::Ensure.into())
+ }
+ }
+}
+
+#[derive(serde::Deserialize)]
+pub struct CreateItemInput {
+ pub product_id: ProductId,
+ pub quantity: Quantity,
+ pub quantity_unit: QuantityUnit,
+}
+
+#[derive(serde::Serialize)]
+pub struct CreateItemOutput {
+ pub success: bool,
+ pub shopping_cart_item: ShoppingCartItem,
+}
+
+#[post("/shopping-cart-item")]
+async fn create_cart_item(
+ cart: Data>,
+ tm: Data>,
+ credentials: BearerAuth,
+ Json(payload): Json,
+) -> Result {
+ let (token, _) = credentials.require_user(tm.into_inner()).await?;
+
+ match cart
+ .send(cart_manager::AddItem {
+ buyer_id: AccountId::from(token.subject),
+ product_id: payload.product_id,
+ quantity: payload.quantity,
+ quantity_unit: payload.quantity_unit,
+ })
+ .await
+ {
+ Ok(Ok(item)) => Ok(HttpResponse::Created().json(CreateItemOutput {
+ success: true,
+ shopping_cart_item: item,
+ })),
+ Ok(Err(e)) => {
+ log::error!("{e:}");
+ Err(routes::Error::Public(super::Error::AddItem.into()))
+ }
+ Err(e) => {
+ log::error!("{e:?}");
+ Err(routes::Error::Public(PublicError::DatabaseConnection))
+ }
+ }
+}
+
+#[derive(serde::Deserialize)]
+pub struct DeleteItemInput {
+ pub shopping_cart_item_id: ShoppingCartItemId,
+}
+
+#[derive(serde::Serialize)]
+pub struct DeleteItemOutput {
+ pub success: bool,
+}
+
+#[delete("/shopping-cart-item")]
+async fn delete_cart_item(
+ db: Data>,
+ cart: Data>,
+ tm: Data>,
+ credentials: BearerAuth,
+ Json(payload): Json,
+) -> Result {
+ let (token, _) = credentials.require_user(tm.into_inner()).await?;
+
+ let sc: ShoppingCart = match db
+ .send(database::EnsureActiveShoppingCart {
+ buyer_id: AccountId::from(token.subject),
+ })
+ .await
+ {
+ Ok(Ok(cart)) => cart,
+ Ok(Err(e)) => {
+ log::error!("{e:}");
+ return Err(routes::Error::Public(super::Error::RemoveItem.into()));
+ }
+ Err(e) => {
+ log::error!("{e:?}");
+ return Err(routes::Error::Public(PublicError::DatabaseConnection));
+ }
+ };
+
+ match cart
+ .into_inner()
+ .send(cart_manager::RemoveProduct {
+ shopping_cart_id: sc.id,
+ shopping_cart_item_id: payload.shopping_cart_item_id,
+ })
+ .await
+ {
+ Ok(Ok(_)) => Ok(HttpResponse::Ok().json(DeleteItemOutput { success: true })),
+ Ok(Err(e)) => {
+ log::error!("{e}");
+ Ok(HttpResponse::BadRequest().json(DeleteItemOutput { success: false }))
+ }
+ Err(e) => {
+ log::error!("{e:?}");
+ Err(routes::Error::Public(PublicError::DatabaseConnection))
+ }
+ }
+}
+
+pub fn configure(config: &mut ServiceConfig) {
+ config.service(scope("")
+ .app_data(actix_web_httpauth::extractors::bearer::Config::default()
+ .realm("user api")
+ .scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
+ .service(shopping_cart)
+ .service(shopping_cart_items)
+ .service(delete_cart_item));
+}
diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs
new file mode 100644
index 0000000..0c2be4c
--- /dev/null
+++ b/api/src/routes/public/api_v1/unrestricted.rs
@@ -0,0 +1,89 @@
+use actix::Addr;
+use actix_web::web::{Data, Json, ServiceConfig};
+use actix_web::{get, post, HttpResponse};
+
+use crate::database::{self, Database};
+use crate::logic::validate_password;
+use crate::model::{Audience, FullAccount, Token, TokenString};
+use crate::routes::public::Error as PublicError;
+use crate::routes::{self, Result};
+use crate::token_manager::TokenManager;
+use crate::{public_send_db, token_manager, Login, Password};
+
+#[get("/products")]
+async fn products(db: Data>) -> Result {
+ public_send_db!(db.into_inner(), database::AllProducts)
+}
+
+#[get("/stocks")]
+async fn stocks(db: Data>) -> Result {
+ public_send_db!(db.into_inner(), database::AllStocks)
+}
+
+#[derive(serde::Deserialize)]
+pub struct SignInInput {
+ pub login: String,
+ pub password: String,
+}
+
+#[derive(serde::Serialize)]
+pub struct SignInOutput {
+ pub token: TokenString,
+}
+
+#[post("/sign-in")]
+async fn sign_in(
+ Json(payload): Json,
+ db: Data>,
+ tm: Data>,
+) -> Result {
+ let db = db.into_inner();
+ let tm = tm.into_inner();
+
+ let account: FullAccount = match db
+ .send(database::AccountByIdentity {
+ login: Some(Login::from(payload.login)),
+ email: None,
+ })
+ .await
+ {
+ Ok(Ok(account)) => account,
+ Ok(Err(db_err)) => {
+ log::error!("{db_err}");
+ return Err(routes::Error::Public(PublicError::DatabaseConnection));
+ }
+ Err(db_err) => {
+ log::error!("{db_err}");
+ return Err(routes::Error::Public(PublicError::DatabaseConnection));
+ }
+ };
+ if validate_password(&Password::from(payload.password), &account.pass_hash).is_err() {
+ return Err(routes::Error::Unauthorized);
+ }
+
+ let (_token, string): (Token, TokenString) = match tm
+ .send(token_manager::CreateToken {
+ customer_id: account.customer_id,
+ role: account.role,
+ subject: account.id,
+ audience: Some(Audience::Web),
+ })
+ .await
+ {
+ Ok(Ok(token)) => token,
+ Ok(Err(token_err)) => {
+ log::error!("{token_err}");
+ return Err(routes::Error::Public(PublicError::DatabaseConnection));
+ }
+ Err(db_err) => {
+ log::error!("{db_err}");
+ return Err(routes::Error::Public(PublicError::DatabaseConnection));
+ }
+ };
+
+ Ok(HttpResponse::Created().json(SignInOutput { token: string }))
+}
+
+pub fn configure(config: &mut ServiceConfig) {
+ config.service(products).service(stocks).service(sign_in);
+}
diff --git a/db/migrate/20220418215_add_uniq_add_time_format.sql b/db/migrate/202204182135_add_uniq_add_time_format.sql
similarity index 50%
rename from db/migrate/20220418215_add_uniq_add_time_format.sql
rename to db/migrate/202204182135_add_uniq_add_time_format.sql
index e7d68fc..d7970e2 100644
--- a/db/migrate/20220418215_add_uniq_add_time_format.sql
+++ b/db/migrate/202204182135_add_uniq_add_time_format.sql
@@ -1,4 +1,4 @@
ALTER TABLE tokens
-ADD CONSTRAINT unit_jit UNIQUE (jti);
+ADD CONSTRAINT unit_jit UNIQUE (jwt_id);
--SET datestyle = '';
diff --git a/db/migrate/202204191430_change_price.sql b/db/migrate/202204191430_change_price.sql
new file mode 100644
index 0000000..ff817e9
--- /dev/null
+++ b/db/migrate/202204191430_change_price.sql
@@ -0,0 +1,14 @@
+ALTER TABLE products
+ADD COLUMN price integer NOT NULL;
+
+ALTER TABLE products
+ADD CONSTRAINT non_negative CHECK (price >= 0);
+
+UPDATE products
+SET price = price_major * 100 + price_minor;
+
+ALTER TABLE products
+DROP COLUMN price_minor;
+
+ALTER TABLE products
+DROP COLUMN price_major;
diff --git a/db/migrate/202204191555_add_account_state.sql b/db/migrate/202204191555_add_account_state.sql
new file mode 100644
index 0000000..2e9197c
--- /dev/null
+++ b/db/migrate/202204191555_add_account_state.sql
@@ -0,0 +1,8 @@
+CREATE TYPE "AccountState" AS ENUM (
+ 'active',
+ 'suspended',
+ 'banned'
+ );
+
+ALTER TABLE accounts
+ ADD COLUMN state "AccountState" NOT NULL DEFAULT 'active';