Add lots of API endpoints

This commit is contained in:
Adrian Woźniak 2022-04-19 16:49:30 +02:00
parent 1667270d73
commit b5dfb64f89
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
23 changed files with 819 additions and 231 deletions

1
Cargo.lock generated
View File

@ -976,6 +976,7 @@ dependencies = [
"actix-web-httpauth", "actix-web-httpauth",
"actix-web-opentelemetry", "actix-web-opentelemetry",
"argon2", "argon2",
"async-trait",
"chrono", "chrono",
"derive_more", "derive_more",
"dotenv", "dotenv",

View File

@ -59,3 +59,5 @@ hmac = { version = "0.12.1" }
sha2 = { version = "0.10.2" } sha2 = { version = "0.10.2" }
oauth2 = { version = "4.1.0" } oauth2 = { version = "4.1.0" }
async-trait = { version = "0.1.53" }

View File

@ -15,7 +15,6 @@
fieldset { fieldset {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 600px;
} }
fieldset > label { fieldset > label {
@ -36,14 +35,27 @@
<form style="width: 100%"> <form style="width: 100%">
<fieldset> <fieldset>
<label for="bearer">bearer</label> <label for="bearer">bearer</label>
<input name="bearer" id="bearer" /> <input name="bearer" id="bearer"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="op">Operation</label> <label for="op">Operation</label>
<select name="op" id="op"> <select name="op" id="op">
<option value="auto-login">Auto login</option> <option></option>
<option value="get-products">Get products</option> <optgroup label="Api V1">
<option value="create-product">Create product</option> <option value="api-v1-sign-in">Api Sign In</option>
<option value="api-v1-products">Products</option>
<option value="api-v1-stocks">Stocks</option>
<option value="api-v1-shopping-cart">Shopping cart</option>
<option value="api-v1-shopping-cart-items">Shopping cart items</option>
</optgroup>
<optgroup label="Admin">
<option value="admin-auto-login">Auto login</option>
<option value="admin-get-products">Get products</option>
<option value="admin-create-product">Create product</option>
<option value="admin-create-stock">Create stock</option>
</optgroup>
</select> </select>
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -59,7 +71,8 @@
<label for="path">Path</label><input id="path" type="text"> <label for="path">Path</label><input id="path" type="text">
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="params">Params</label><textarea id="params"></textarea> <label for="params">Params</label>
<textarea id="params" rows="40"></textarea>
</fieldset> </fieldset>
<input type="submit"> <input type="submit">
</form> </form>
@ -92,7 +105,7 @@
if (bearer.length) { if (bearer.length) {
if (!rest.headers) rest.headers = {}; if (!rest.headers) rest.headers = {};
rest.headers["Authorization"] = `Bearer ${bearer}`; rest.headers["Authorization"] = `Bearer ${ bearer }`;
} }
path = method === 'GET' path = method === 'GET'
@ -112,32 +125,82 @@
opEL.addEventListener('change', () => { opEL.addEventListener('change', () => {
switch (opEL.value) { switch (opEL.value) {
case 'auto-login': { case 'api-v1-sign-in': {
paramsEl.value = `login=Eraden\npassword=text` 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'; mthEl.value = 'POST';
urlEl.value = '/admin/sign-in'; urlEl.value = '/admin/sign-in';
break; break;
} }
case 'get-products': { case 'admin-get-products': {
mthEl.value = 'GET'; mthEl.value = 'GET';
urlEl.value = '/admin/api/v1/products'; urlEl.value = '/admin/api/v1/products';
paramsEl.value = '';
break; break;
} }
case 'create-product': { case 'admin-create-product': {
const p = { paramsEl.value = serializeParams({
name: 'Foo', name: 'Foo',
short_description: 'asd', short_description: 'asd',
long_description: 'asjdoiajd ajio djaso idja s', long_description: 'asjdoiajd ajio djaso idja s',
price_major: 12, price: 1200,
price_minor: 0, deliver_days_flag: ["monday"]
}; });
paramsEl.value = Object.entries(p).map(([k, v]) => `${ k }=${ v }`).join('\n');
mthEl.value = 'POST'; mthEl.value = 'POST';
urlEl.value = '/admin/api/v1/product'; urlEl.value = '/admin/api/v1/product';
break; 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) => { form.addEventListener('submit', (ev) => {
ev.preventDefault(); ev.preventDefault();
@ -152,7 +215,11 @@
let [k, ...v] = s.split("="); let [k, ...v] = s.split("=");
v = Array(v).join('=') v = Array(v).join('=')
try { 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 (_) { } catch (_) {
} }
params[k] = v; params[k] = v;

View File

@ -3,7 +3,7 @@ use sqlx::PgPool;
use super::Result; use super::Result;
use crate::database::Database; use crate::database::Database;
use crate::db_async_handler; 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)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -26,7 +26,7 @@ db_async_handler!(AllAccounts, all_accounts, Vec<FullAccount>);
pub(crate) async fn all_accounts(_msg: AllAccounts, pool: PgPool) -> Result<Vec<FullAccount>> { pub(crate) async fn all_accounts(_msg: AllAccounts, pool: PgPool) -> Result<Vec<FullAccount>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
"#, "#,
) )
@ -54,7 +54,7 @@ pub(crate) async fn create_account(msg: CreateAccount, db: PgPool) -> Result<Ful
r#" r#"
INSERT INTO accounts (login, email, role, pass_hash) INSERT INTO accounts (login, email, role, pass_hash)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, email, login, pass_hash, role, customer_id RETURNING id, email, login, pass_hash, role, customer_id, state
"#, "#,
) )
.bind(msg.login) .bind(msg.login)
@ -75,25 +75,41 @@ pub struct UpdateAccount {
pub id: AccountId, pub id: AccountId,
pub email: Email, pub email: Email,
pub login: Login, pub login: Login,
pub pass_hash: PassHash, pub pass_hash: Option<PassHash>,
pub role: Role, pub role: Role,
pub state: AccountState,
} }
db_async_handler!(UpdateAccount, update_account, FullAccount); db_async_handler!(UpdateAccount, update_account, FullAccount);
pub(crate) async fn update_account(msg: UpdateAccount, db: PgPool) -> Result<FullAccount> { pub(crate) async fn update_account(msg: UpdateAccount, db: PgPool) -> Result<FullAccount> {
sqlx::query_as( match msg.pass_hash {
r#" 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 UPDATE accounts
SET login = $2 AND email = $3 AND role = $4 AND pass_hash = $5 SET login = $2 AND email = $3 AND role = $4 AND pass_hash = $5
WHERE id = $1 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.login)
.bind(msg.email) .bind(msg.email)
.bind(msg.role) .bind(msg.role)
.bind(msg.pass_hash) .bind(msg.state),
}
.fetch_one(&db) .fetch_one(&db)
.await .await
.map_err(|e| { .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<FullAccount> { pub(crate) async fn find_account(msg: FindAccount, db: PgPool) -> Result<FullAccount> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
WHERE id = $1 WHERE id = $1
"#, "#,
@ -140,7 +156,7 @@ pub(crate) async fn account_by_identity(msg: AccountByIdentity, db: PgPool) -> R
match (msg.login, msg.email) { match (msg.login, msg.email) {
(Some(login), None) => sqlx::query_as( (Some(login), None) => sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
WHERE login = $1 WHERE login = $1
"#, "#,
@ -148,7 +164,7 @@ WHERE login = $1
.bind(login), .bind(login),
(None, Some(email)) => sqlx::query_as( (None, Some(email)) => sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
WHERE email = $1 WHERE email = $1
"#, "#,
@ -156,7 +172,7 @@ WHERE email = $1
.bind(email), .bind(email),
(Some(login), Some(email)) => sqlx::query_as( (Some(login), Some(email)) => sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
WHERE login = $1 AND email = $2 WHERE login = $1 AND email = $2
"#, "#,

View File

@ -4,8 +4,8 @@ use sqlx::PgPool;
use super::Result; use super::Result;
use crate::database::Database; use crate::database::Database;
use crate::model::{ use crate::model::{
Days, PriceMajor, PriceMinor, Product, ProductCategory, ProductId, ProductLongDesc, Days, Price, Product, ProductCategory, ProductId, ProductLongDesc, ProductName,
ProductName, ProductShortDesc, ProductShortDesc,
}; };
use crate::{database, model}; use crate::{database, model};
@ -35,8 +35,7 @@ SELECT id,
short_description, short_description,
long_description, long_description,
category, category,
price_major, price,
price_minor,
deliver_days_flag deliver_days_flag
FROM products FROM products
"#, "#,
@ -56,8 +55,7 @@ pub struct CreateProduct {
pub short_description: ProductShortDesc, pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc, pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>, pub category: Option<ProductCategory>,
pub price_major: PriceMajor, pub price: Price,
pub price_minor: PriceMinor,
pub deliver_days_flag: Days, 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<model::Product> { pub(crate) async fn create_product(msg: CreateProduct, pool: PgPool) -> Result<model::Product> {
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO products (name, short_description, long_description, category, price_major, price_minor) INSERT INTO products (name, short_description, long_description, category, price, deliver_days_flag)
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, RETURNING id,
name, name,
short_description, short_description,
long_description, long_description,
category, category,
price_major, price,
price_minor,
deliver_days_flag deliver_days_flag
"#, "#,
) )
@ -82,8 +79,7 @@ RETURNING id,
.bind(msg.short_description) .bind(msg.short_description)
.bind(msg.long_description) .bind(msg.long_description)
.bind(msg.category) .bind(msg.category)
.bind(msg.price_major) .bind(msg.price)
.bind(msg.price_minor)
.bind(msg.deliver_days_flag) .bind(msg.deliver_days_flag)
.fetch_one(&pool) .fetch_one(&pool)
.await .await
@ -101,8 +97,7 @@ pub struct UpdateProduct {
pub short_description: ProductShortDesc, pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc, pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>, pub category: Option<ProductCategory>,
pub price_major: PriceMajor, pub price: Price,
pub price_minor: PriceMinor,
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }
@ -116,17 +111,15 @@ SET name = $2 AND
short_description = $3 AND short_description = $3 AND
long_description = $4 AND long_description = $4 AND
category = $5 AND category = $5 AND
price_major = $6 AND price = $6 AND
price_minor = $7 AND deliver_days_flag = $7
deliver_days_flag = $8
WHERE id = $1 WHERE id = $1
RETURNING id, RETURNING id,
name, name,
short_description, short_description,
long_description, long_description,
category, category,
price_major, price,
price_minor,
deliver_days_flag deliver_days_flag
"#, "#,
) )
@ -135,8 +128,7 @@ RETURNING id,
.bind(msg.short_description) .bind(msg.short_description)
.bind(msg.long_description) .bind(msg.long_description)
.bind(msg.category) .bind(msg.category)
.bind(msg.price_major) .bind(msg.price)
.bind(msg.price_minor)
.bind(msg.deliver_days_flag) .bind(msg.deliver_days_flag)
.fetch_one(&pool) .fetch_one(&pool)
.await .await
@ -164,8 +156,7 @@ RETURNING id,
short_description, short_description,
long_description, long_description,
category, category,
price_major, price,
price_minor,
deliver_days_flag deliver_days_flag
"#, "#,
) )

View File

@ -74,9 +74,15 @@ pub(crate) async fn account_shopping_cart_items(
) -> Result<Vec<ShoppingCartItem>> { ) -> Result<Vec<ShoppingCartItem>> {
sqlx::query_as( sqlx::query_as(
r#" 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 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) .bind(msg.account_id)

View File

@ -52,7 +52,7 @@ crate::db_async_handler!(CreateStock, create_stock, Stock);
async fn create_stock(msg: CreateStock, pool: PgPool) -> Result<model::Stock> { async fn create_stock(msg: CreateStock, pool: PgPool) -> Result<model::Stock> {
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO stocks (product_id, quantity) INSERT INTO stocks (product_id, quantity, quantity_unit)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, product_id, quantity, quantity_unit RETURNING id, product_id, quantity, quantity_unit
"#, "#,

View File

@ -17,7 +17,7 @@ pub enum Error {
#[derive(Message)] #[derive(Message)]
#[rtype(result = "Result<Token>")] #[rtype(result = "Result<Token>")]
pub struct TokenByJti { pub struct TokenByJti {
pub jti: String, pub jti: uuid::Uuid,
} }
db_async_handler!(TokenByJti, token_by_jti, Token); db_async_handler!(TokenByJti, token_by_jti, Token);

View File

@ -12,7 +12,7 @@ use crate::database::{Database, TokenByJti};
use crate::model::{AccountId, Audience, Token, TokenString}; use crate::model::{AccountId, Audience, Token, TokenString};
use crate::{database, token_async_handler, Role}; use crate::{database, token_async_handler, Role};
struct Jwt { /*struct Jwt {
/// cti (customer id): Customer uuid identifier used by payment service /// cti (customer id): Customer uuid identifier used by payment service
pub cti: uuid::Uuid, pub cti: uuid::Uuid,
/// arl (account role): account role /// arl (account role): account role
@ -34,7 +34,7 @@ struct Jwt {
/// jti (JWT ID): Unique identifier; can be used to prevent the JWT from /// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
/// being replayed (allows a token to be used only once) /// being replayed (allows a token to be used only once)
pub jti: uuid::Uuid, pub jti: uuid::Uuid,
} }*/
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -119,32 +119,51 @@ pub(crate) async fn create_token(
// cti (customer id): Customer uuid identifier used by payment service // cti (customer id): Customer uuid identifier used by payment service
claims.insert("cti", format!("{}", token.customer_id)); claims.insert("cti", format!("{}", token.customer_id));
// arl (account role): account role // 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 // 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) // sub (subject): Subject of the JWT (the user)
claims.insert("sub", format!("{}", token.subject)); claims.insert("sub", format!("{}", token.subject));
// aud (audience): Recipient for which the JWT is intended // 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 // 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 // nbt (not before time): Time before which the JWT must not be accepted
// for processing // 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 // iat (issued at time): Time at which the JWT was issued; can be used
// to determine age of the JWT, // 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 // jti (JWT ID): Unique identifier; can be used to prevent the JWT from
// being replayed (allows a token to be used only once) // being replayed (allows a token to be used only once)
claims.insert("jti", format!("{}", token.jwt_id)); 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, Ok(s) => s,
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");
return Err(Error::SaveInternal); return Err(Error::SaveInternal);
} }
}) };
TokenString::from(s)
}; };
Ok((token, token_string)) Ok((token, token_string))
} }
@ -178,7 +197,10 @@ pub(crate) async fn validate(
let token: Token = match db let token: Token = match db
.send(TokenByJti { .send(TokenByJti {
jti: String::from(jti), jti: match uuid::Uuid::from_str(jti) {
Ok(uid) => uid,
_ => return Err(Error::Validate),
},
}) })
.await .await
{ {
@ -196,26 +218,18 @@ pub(crate) async fn validate(
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) { if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
return Ok((token, false)); return Ok((token, false));
} }
// if !validate_pair(&claims, "arl", token.role, |left, right| right == left) { if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
// return Ok((token, false)); return Ok((token, false));
// }
match (claims.get("arl"), &token.role) {
(Some(arl), role) if role == arl.as_str() => {}
_ => return Ok((token, false)),
} }
match (claims.get("iss"), &token.issuer) { if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) {
(Some(iss), issuer) if iss == issuer => {} return Ok((token, false));
_ => return Ok((token, false)),
} }
if !validate_pair(&claims, "sub", token.subject, validate_num) { if !validate_pair(&claims, "sub", token.subject, validate_num) {
return Ok((token, false)); return Ok((token, false));
} }
if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) {
match (claims.get("aud"), &token.audience) { return Ok((token, false));
(Some(aud), audience) if aud == audience.as_str() => {}
_ => return Ok((token, false)),
} }
if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) { if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) {
return Ok((token, false)); return Ok((token, false));
} }
@ -226,6 +240,7 @@ pub(crate) async fn validate(
return Ok((token, false)); return Ok((token, false));
} }
log::info!("JWT token valid");
Ok((token, true)) Ok((token, true))
} }

View File

@ -1,6 +1,4 @@
#![feature(stdio_locked)] use std::io::Write;
use std::io::{BufRead, Write};
use std::sync::Arc; use std::sync::Arc;
use actix::Actor; use actix::Actor;
@ -201,12 +199,12 @@ async fn migrate(opts: MigrateOpts) -> Result<()> {
let res: std::result::Result<(), MigrateError> = let res: std::result::Result<(), MigrateError> =
sqlx::migrate!("../db/migrate").run(db.pool()).await; sqlx::migrate!("../db/migrate").run(db.pool()).await;
match res { match res {
Ok(()) => return Ok(()), Ok(()) => Ok(()),
Err(e) => { Err(e) => {
eprintln!("{e}"); eprintln!("{e}");
std::process::exit(1); std::process::exit(1);
} }
}; }
} }
async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> { async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> {
@ -233,8 +231,8 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
None => { None => {
let mut s = String::with_capacity(100); let mut s = String::with_capacity(100);
{ {
let mut std_out = std::io::stdout_locked(); let mut std_out = std::io::stdout();
let mut std_in = std::io::stdin_locked(); let std_in = std::io::stdin();
std_out std_out
.write_all(b"PASS > ") .write_all(b"PASS > ")
@ -253,12 +251,12 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
panic!("Password cannot be empty!"); panic!("Password cannot be empty!");
} }
let config = Config::load(); 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 { db.send(database::CreateAccount {
email: Email(opts.email), email: Email::from(opts.email),
login: Login(opts.login), login: Login::from(opts.login),
pass_hash: PassHash(hash), pass_hash: PassHash::from(hash),
role, role,
}) })
.await .await

View File

@ -16,6 +16,7 @@ pub type RecordId = i32;
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum OrderStatus { pub enum OrderStatus {
#[display(fmt = "Potwierdzone")] #[display(fmt = "Potwierdzone")]
Confirmed, Confirmed,
@ -33,6 +34,7 @@ pub enum OrderStatus {
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Role { pub enum Role {
#[display(fmt = "Adminitrator")] #[display(fmt = "Adminitrator")]
Admin, Admin,
@ -40,9 +42,9 @@ pub enum Role {
User, User,
} }
impl PartialEq<str> for Role { impl PartialEq<&str> for Role {
fn eq(&self, other: &str) -> bool { fn eq(&self, other: &&str) -> bool {
self.as_str() == other self.as_str() == *other
} }
} }
@ -56,16 +58,21 @@ impl Role {
} }
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum QuantityUnit { pub enum QuantityUnit {
#[sqlx(rename = "g")]
Gram, Gram,
#[sqlx(rename = "dkg")]
Decagram, Decagram,
#[sqlx(rename = "kg")]
Kilogram, Kilogram,
Unit, #[sqlx(rename = "piece")]
Piece,
} }
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum PaymentMethod { pub enum PaymentMethod {
PayU, PayU,
PaymentOnTheSpot, PaymentOnTheSpot,
@ -73,13 +80,15 @@ pub enum PaymentMethod {
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
#[sqlx(rename_all = "snake_case")] #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ShoppingCartState { pub enum ShoppingCartState {
Active, Active,
Closed, 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")] #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Audience { pub enum Audience {
Web, Web,
Mobile, Mobile,
@ -87,6 +96,12 @@ pub enum Audience {
AdminPanel, AdminPanel,
} }
impl PartialEq<&str> for Audience {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl Audience { impl Audience {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { 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 { impl Default for Audience {
fn default() -> Self { fn default() -> Self {
Self::Web Self::Web
@ -107,14 +131,9 @@ impl Default for Audience {
#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)] #[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct PriceMajor(NonNegative); pub struct Price(NonNegative);
#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)] #[derive(sqlx::Type, Serialize, Deserialize, Default, Deref, From)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct PriceMinor(NonNegative);
#[derive(sqlx::Type, Serialize, Deserialize, Deref, From)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct Quantity(NonNegative); pub struct Quantity(NonNegative);
@ -127,15 +146,15 @@ impl TryFrom<i32> for Quantity {
} }
} }
#[derive(sqlx::Type, Deserialize, Serialize, Deref, Debug)] #[derive(sqlx::Type, Deserialize, Serialize, Debug, Deref, From, Display)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct Email(pub String); pub struct Email(String);
impl<'de> serde::Deserialize<'de> for Email { impl<'de> serde::Deserialize<'de> for Email {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct NonNegative(i32); pub struct NonNegative(i32);
@ -176,7 +195,7 @@ impl TryFrom<i32> for NonNegative {
fn try_from(value: i32) -> Result<Self, Self::Error> { fn try_from(value: i32) -> Result<Self, Self::Error> {
if value < 0 { if value < 0 {
return Err(TransformError::BelowMinimal); Err(TransformError::BelowMinimal)
} else { } else {
Ok(Self(value)) 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")) Err(E::custom("Value must be equal or greater than 0"))
} }
} }
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
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<E>(self, v: u32) -> Result<Self::Value, E>
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<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
let v = v
.try_into()
.map_err(|_| E::custom("Value must be equal or greater than 0"))?;
Ok(v)
}
} }
Ok(NonNegative( 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)] #[sqlx(transparent)]
#[serde(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)] #[sqlx(transparent)]
#[serde(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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct PassHash(pub String); pub struct PassHash(String);
impl PartialEq<PasswordConfirmation> for Password { impl PartialEq<PasswordConfirmation> for Password {
fn eq(&self, other: &PasswordConfirmation) -> bool { fn eq(&self, other: &PasswordConfirmation) -> bool {
@ -350,6 +403,7 @@ pub struct FullAccount {
pub pass_hash: PassHash, pub pass_hash: PassHash,
pub role: Role, pub role: Role,
pub customer_id: uuid::Uuid, pub customer_id: uuid::Uuid,
pub state: AccountState,
} }
#[derive(sqlx::FromRow, Serialize, Deserialize)] #[derive(sqlx::FromRow, Serialize, Deserialize)]
@ -359,6 +413,7 @@ pub struct Account {
pub login: Login, pub login: Login,
pub role: Role, pub role: Role,
pub customer_id: uuid::Uuid, pub customer_id: uuid::Uuid,
pub state: AccountState,
} }
impl From<FullAccount> for Account { impl From<FullAccount> for Account {
@ -370,6 +425,7 @@ impl From<FullAccount> for Account {
pass_hash: _, pass_hash: _,
role, role,
customer_id, customer_id,
state,
}: FullAccount, }: FullAccount,
) -> Self { ) -> Self {
Self { Self {
@ -378,6 +434,7 @@ impl From<FullAccount> for Account {
login, login,
role, role,
customer_id, customer_id,
state,
} }
} }
} }
@ -414,8 +471,7 @@ pub struct Product {
pub short_description: ProductShortDesc, pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc, pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>, pub category: Option<ProductCategory>,
pub price_major: PriceMajor, pub price: Price,
pub price_minor: PriceMinor,
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }

View File

@ -1,3 +1,4 @@
mod accounts;
mod products; mod products;
mod stocks; mod stocks;
@ -7,6 +8,7 @@ pub fn configure(config: &mut ServiceConfig) {
config.service( config.service(
scope("/api/v1") scope("/api/v1")
.configure(products::configure) .configure(products::configure)
.configure(stocks::configure), .configure(stocks::configure)
.configure(accounts::configure),
); );
} }

View File

@ -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<Addr<Database>>) -> routes::Result<HttpResponse> {
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<Password>,
pub password_confirmation: Option<PasswordConfirmation>,
pub role: Role,
pub state: AccountState,
}
#[patch("/account")]
pub async fn update_account(
session: Session,
db: Data<Addr<Database>>,
Json(payload): Json<UpdateAccountInput>,
config: Data<Arc<Config>>,
) -> routes::Result<HttpResponse> {
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<Addr<Database>>,
Json(payload): Json<CreateAccountInput>,
config: Data<Arc<Config>>,
) -> routes::Result<HttpResponse> {
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);
}

View File

@ -6,8 +6,8 @@ use serde::Deserialize;
use crate::database::Database; use crate::database::Database;
use crate::model::{ use crate::model::{
Days, PriceMajor, PriceMinor, ProductCategory, ProductId, ProductLongDesc, ProductName, Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc,
ProductShortDesc, Quantity, QuantityUnit,
}; };
use crate::routes::admin::Error; use crate::routes::admin::Error;
use crate::routes::RequireLogin; use crate::routes::RequireLogin;
@ -17,7 +17,8 @@ use crate::{admin_send_db, database, routes};
async fn products(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> { async fn products(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!(db, database::AllProducts); let products = admin_send_db!(db, database::AllProducts);
Ok(HttpResponse::Ok().json(products))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -27,8 +28,7 @@ pub struct UpdateProduct {
pub short_description: ProductShortDesc, pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc, pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>, pub category: Option<ProductCategory>,
pub price_major: PriceMajor, pub price: Price,
pub price_minor: PriceMinor,
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }
@ -40,7 +40,7 @@ async fn update_product(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!( let product = admin_send_db!(
db, db,
database::UpdateProduct { database::UpdateProduct {
id: payload.id, id: payload.id,
@ -48,11 +48,11 @@ async fn update_product(
short_description: payload.short_description, short_description: payload.short_description,
long_description: payload.long_description, long_description: payload.long_description,
category: payload.category, category: payload.category,
price_major: payload.price_major, price: payload.price,
price_minor: payload.price_minor,
deliver_days_flag: payload.deliver_days_flag, deliver_days_flag: payload.deliver_days_flag,
} }
); );
Ok(HttpResponse::Ok().json(product))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -61,8 +61,7 @@ pub struct CreateProduct {
pub short_description: ProductShortDesc, pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc, pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>, pub category: Option<ProductCategory>,
pub price_major: PriceMajor, pub price: Price,
pub price_minor: PriceMinor,
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }
@ -74,18 +73,26 @@ async fn create_product(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!( let product = admin_send_db!(
db, db.clone(),
database::CreateProduct { database::CreateProduct {
name: payload.name, name: payload.name,
short_description: payload.short_description, short_description: payload.short_description,
long_description: payload.long_description, long_description: payload.long_description,
category: payload.category, category: payload.category,
price_major: payload.price_major, price: payload.price,
price_minor: payload.price_minor,
deliver_days_flag: payload.deliver_days_flag, 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)] #[derive(Deserialize)]
@ -101,12 +108,13 @@ async fn delete_product(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
let _ = session.require_admin()?; let _ = session.require_admin()?;
admin_send_db!( let product = admin_send_db!(
db, db,
database::DeleteProduct { database::DeleteProduct {
product_id: payload.id product_id: payload.id
} }
); );
Ok(HttpResponse::Ok().json(product))
} }
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {

View File

@ -14,7 +14,8 @@ use crate::{admin_send_db, database, routes};
async fn stocks(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> { async fn stocks(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!(db, database::AllStocks); let stocks = admin_send_db!(db, database::AllStocks);
Ok(HttpResponse::Created().json(stocks))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -33,7 +34,7 @@ async fn update_stock(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!( let stock = admin_send_db!(
db, db,
database::UpdateStock { database::UpdateStock {
id: payload.id, id: payload.id,
@ -42,6 +43,7 @@ async fn update_stock(
quantity_unit: payload.quantity_unit quantity_unit: payload.quantity_unit
} }
); );
Ok(HttpResponse::Created().json(stock))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -59,7 +61,7 @@ async fn create_stock(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!( let stock = admin_send_db!(
db, db,
database::CreateStock { database::CreateStock {
product_id: payload.product_id, product_id: payload.product_id,
@ -67,6 +69,7 @@ async fn create_stock(
quantity_unit: payload.quantity_unit quantity_unit: payload.quantity_unit
} }
); );
Ok(HttpResponse::Created().json(stock))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -82,12 +85,13 @@ async fn delete_stock(
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
admin_send_db!( let stock = admin_send_db!(
db, db,
database::DeleteStock { database::DeleteStock {
stock_id: payload.id stock_id: payload.id
} }
); );
Ok(HttpResponse::Created().json(stock))
} }
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {

View File

@ -18,17 +18,17 @@ use crate::{database, model, routes, Config};
macro_rules! admin_send_db { macro_rules! admin_send_db {
($db: expr, $msg: expr) => {{ ($db: expr, $msg: expr) => {{
let db = $db; let db = $db;
return match db.send($msg).await { match db.send($msg).await {
Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)), Ok(Ok(res)) => res,
Ok(Err(e)) => { Ok(Err(e)) => {
log::error!("{}", e); log::error!("{}", e);
Err(crate::routes::Error::Admin(Error::Database(e))) return Err(crate::routes::Error::Admin(Error::Database(e)));
} }
Err(e) => { Err(e) => {
log::error!("{}", 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, HashPass,
#[error("Internal server error")] #[error("Internal server error")]
DatabaseConnection, DatabaseConnection,
#[error("Password and password confirmation are different")]
DifferentPasswords,
#[error("{0}")] #[error("{0}")]
Database(#[from] database::Error), Database(#[from] database::Error),
} }
@ -146,7 +148,7 @@ async fn register(
.send(database::CreateAccount { .send(database::CreateAccount {
email: input.email, email: input.email,
login: input.login, login: input.login,
pass_hash: PassHash(hash), pass_hash: PassHash::from(hash),
role: input.role, role: input.role,
}) })
.await .await

View File

@ -2,16 +2,20 @@ pub mod admin;
pub mod public; pub mod public;
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::sync::Arc;
use actix::Addr;
use actix_session::Session; use actix_session::Session;
use actix_web::body::BoxBody; use actix_web::body::BoxBody;
use actix_web::web::ServiceConfig; use actix_web::web::ServiceConfig;
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError}; use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
pub use admin::Error as AdminError; use serde::Serialize;
pub use public::{Error as PublicError, V1Error, V1ShoppingCartError};
use crate::model::RecordId; pub use self::admin::Error as AdminError;
use crate::routes; 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 { pub trait RequireLogin {
fn require_admin(&self) -> Result<RecordId>; fn require_admin(&self) -> Result<RecordId>;
@ -65,6 +69,12 @@ impl Display for Error {
} }
} }
#[derive(Serialize)]
struct ReqFailure {
success: bool,
msg: String,
}
impl ResponseError for Error {} impl ResponseError for Error {}
impl Responder for Error { impl Responder for Error {
@ -74,17 +84,34 @@ impl Responder for Error {
match self { match self {
Error::Unauthorized => HttpResponse::Unauthorized() Error::Unauthorized => HttpResponse::Unauthorized()
.content_type("application/json") .content_type("application/json")
.body(format!("{}", self)), .json(ReqFailure {
success: false,
msg: format!("{}", self),
}),
Error::Public(PublicError::DatabaseConnection) Error::Public(PublicError::DatabaseConnection)
| Error::Public(PublicError::Database(..)) | Error::Public(PublicError::Database(..))
| Error::Admin(..) => HttpResponse::InternalServerError() | Error::Admin(..) => HttpResponse::InternalServerError()
.content_type("application/json") .content_type("application/json")
.body(format!("{}", self)), .json(ReqFailure {
success: false,
msg: format!("{}", self),
}),
Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e { Error::Public(PublicError::ApiV1(V1Error::ShoppingCart(ref e))) => match e {
V1ShoppingCartError::Ensure => HttpResponse::InternalServerError() V1ShoppingCartError::Ensure => HttpResponse::InternalServerError()
.content_type("application/json") .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(public::configure)
.configure(admin::configure); .configure(admin::configure);
} }
#[async_trait::async_trait]
pub trait RequireUser {
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> Result<(Token, bool)>;
}
#[async_trait::async_trait]
impl RequireUser for actix_web_httpauth::extractors::bearer::BearerAuth {
async fn require_user(&self, tm: Arc<Addr<TokenManager>>) -> 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),
}
}
}

View File

@ -1,16 +1,7 @@
use actix::Addr; mod restricted;
use actix_web::dev::ServiceRequest; mod unrestricted;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::{get, HttpResponse};
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
use actix_web_httpauth::middleware::HttpAuthentication;
use crate::database::Database; use actix_web::web::{scope, ServiceConfig};
use crate::model::{AccountId, TokenString};
use crate::routes::Result;
use crate::token_manager::TokenManager;
use crate::{database, public_send_db, token_manager};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ShoppingCartError { pub enum ShoppingCartError {
@ -22,72 +13,17 @@ pub enum ShoppingCartError {
pub enum Error { pub enum Error {
#[error("{0}")] #[error("{0}")]
ShoppingCart(ShoppingCartError), ShoppingCart(ShoppingCartError),
}
#[get("/products")] #[error("Failed to remove shopping cart item")]
async fn products(db: Data<Addr<Database>>) -> Result<HttpResponse> { RemoveItem,
public_send_db!(db.into_inner(), database::AllProducts) #[error("Failed to add shopping cart item")]
} AddItem,
#[get("/stocks")]
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database::AllStocks)
}
#[get("/shopping_cart")]
async fn shopping_cart(db: Data<Addr<Database>>, credentials: BearerAuth) -> Result<HttpResponse> {
let _t = credentials.token();
match db
.send(database::EnsureActiveShoppingCart {
buyer_id: AccountId::from(1),
})
.await
{
Ok(Ok(cart)) => Ok(HttpResponse::Ok().json(cart)),
Ok(Err(e)) => {
log::error!("{e}");
Err(ShoppingCartError::Ensure.into())
}
Err(e) => {
log::error!("{e:?}");
Err(ShoppingCartError::Ensure.into())
}
}
} }
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
let bearer_auth = HttpAuthentication::bearer(validator);
config.service( config.service(
scope("/api/v1") scope("/api/v1")
.service(products) .configure(unrestricted::configure)
.service(stocks) .configure(restricted::configure),
.service(scope("").wrap(bearer_auth).service(shopping_cart)),
); );
} }
async fn validator(
req: ServiceRequest,
credentials: BearerAuth,
) -> std::result::Result<ServiceRequest, actix_web::Error> {
let tm = match req.app_data::<Data<Addr<TokenManager>>>() {
Some(db) => db,
_ => panic!("DB must be configured"),
};
if let Ok(Ok((_, true))) = tm
.send(token_manager::Validate {
token: TokenString::from(String::from(credentials.token())),
})
.await
{
return Ok(req);
};
let config = req
.app_data::<Config>()
.map(|data| data.clone())
.unwrap_or_else(Default::default)
.scope("account=user");
Err(AuthenticationError::from(config).into())
}

View File

@ -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<Addr<Database>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
) -> Result<HttpResponse> {
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<Addr<Database>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
) -> Result<HttpResponse> {
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<Addr<CartManager>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
Json(payload): Json<CreateItemInput>,
) -> Result<HttpResponse> {
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<Addr<Database>>,
cart: Data<Addr<CartManager>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
Json(payload): Json<DeleteItemInput>,
) -> Result<HttpResponse> {
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));
}

View File

@ -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<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database::AllProducts)
}
#[get("/stocks")]
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
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<SignInInput>,
db: Data<Addr<Database>>,
tm: Data<Addr<TokenManager>>,
) -> Result<HttpResponse> {
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);
}

View File

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

View File

@ -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;

View File

@ -0,0 +1,8 @@
CREATE TYPE "AccountState" AS ENUM (
'active',
'suspended',
'banned'
);
ALTER TABLE accounts
ADD COLUMN state "AccountState" NOT NULL DEFAULT 'active';