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 @@
- +
@@ -59,7 +71,8 @@
- + +
@@ -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';