Add endpoints, creating accounts and so on
This commit is contained in:
parent
93fc994d1d
commit
d2634598d5
1
.env
1
.env
@ -1,2 +1,3 @@
|
|||||||
DATABASE_URL=postgres://postgres@localhost/bazzar
|
DATABASE_URL=postgres://postgres@localhost/bazzar
|
||||||
PASS_SALT=18CHwV7eGFAea16z+qMKZg
|
PASS_SALT=18CHwV7eGFAea16z+qMKZg
|
||||||
|
RUST_LOG=debug
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["web"]
|
members = ["api"]
|
||||||
|
0
web/Cargo.lock → api/Cargo.lock
generated
0
web/Cargo.lock → api/Cargo.lock
generated
62
api/assets/index.html
Normal file
62
api/assets/index.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bazzar</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex">
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<select id="method">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
<input id="path" type="text">
|
||||||
|
<textarea id="params"></textarea>
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<pre><code id="output"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const out = document.querySelector('#output');
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
const urlEl = form.querySelector('#path');
|
||||||
|
const paramsEl = form.querySelector('#params');
|
||||||
|
const mthEl = form.querySelector('#method');
|
||||||
|
|
||||||
|
form.addEventListener('submit', (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
let path = urlEl.value;
|
||||||
|
let params = {};
|
||||||
|
const method = mthEl.value;
|
||||||
|
|
||||||
|
paramsEl.textContent.split("\n").forEach(s => {
|
||||||
|
if (!s.length) return;
|
||||||
|
let [k, v] = s.split("=");
|
||||||
|
params[k] = v;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rest = method === 'GET'
|
||||||
|
? {}
|
||||||
|
: { body: JSON.stringify(params), headers: { 'Content-Type': 'application/json' } };
|
||||||
|
path = method === 'GET'
|
||||||
|
? `${ path }?${ JSON.stringify(params) }`
|
||||||
|
: path;
|
||||||
|
|
||||||
|
fetch(`/${ path }`, { ...rest, method })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
out.textContent = JSON.stringify(json, null, 4);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -3,9 +3,11 @@ use sqlx::PgPool;
|
|||||||
|
|
||||||
pub use accounts::*;
|
pub use accounts::*;
|
||||||
pub use products::*;
|
pub use products::*;
|
||||||
|
pub use stocks::*;
|
||||||
|
|
||||||
mod accounts;
|
mod accounts;
|
||||||
mod products;
|
mod products;
|
||||||
|
mod stocks;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! async_handler {
|
macro_rules! async_handler {
|
||||||
@ -29,6 +31,8 @@ pub enum Error {
|
|||||||
Account(accounts::Error),
|
Account(accounts::Error),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Product(products::Error),
|
Product(products::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Stock(stocks::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
120
api/src/actors/database/accounts.rs
Normal file
120
api/src/actors/database/accounts.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
use crate::async_handler;
|
||||||
|
use actix::{Handler, ResponseActFuture, WrapFuture};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::model::{AccountId, Email, FullAccount, Login, PassHash, Role};
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Can't create account")]
|
||||||
|
CantCreate,
|
||||||
|
#[error("Can't find account does to lack of identity")]
|
||||||
|
NoIdentity,
|
||||||
|
#[error("Account does not exists")]
|
||||||
|
NotExists,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(actix::Message)]
|
||||||
|
#[rtype(result = "Result<FullAccount>")]
|
||||||
|
pub struct CreateAccount {
|
||||||
|
pub email: Email,
|
||||||
|
pub login: Login,
|
||||||
|
pub pass_hash: PassHash,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
async_handler!(CreateAccount, create_account, FullAccount);
|
||||||
|
|
||||||
|
async fn create_account(msg: CreateAccount, db: PgPool) -> Result<FullAccount> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
INSERT INTO accounts (login, email, role, pass_hash)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, email, login, pass_hash, role
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(msg.login)
|
||||||
|
.bind(msg.email)
|
||||||
|
.bind(msg.role)
|
||||||
|
.bind(msg.pass_hash)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
super::Error::Account(Error::CantCreate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(actix::Message)]
|
||||||
|
#[rtype(result = "Result<FullAccount>")]
|
||||||
|
pub struct FindAccount {
|
||||||
|
pub account_id: AccountId,
|
||||||
|
}
|
||||||
|
|
||||||
|
async_handler!(FindAccount, find_account, FullAccount);
|
||||||
|
|
||||||
|
async fn find_account(msg: FindAccount, db: PgPool) -> Result<FullAccount> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, email, login, pass_hash, role
|
||||||
|
FROM accounts
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(msg.account_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
super::Error::Account(Error::NotExists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(actix::Message)]
|
||||||
|
#[rtype(result = "Result<FullAccount>")]
|
||||||
|
pub struct AccountByIdentity {
|
||||||
|
pub login: Option<Login>,
|
||||||
|
pub email: Option<Email>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async_handler!(AccountByIdentity, account_by_identity, FullAccount);
|
||||||
|
|
||||||
|
async fn account_by_identity(msg: AccountByIdentity, db: PgPool) -> Result<FullAccount> {
|
||||||
|
match (msg.login, msg.email) {
|
||||||
|
(Some(login), None) => sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, email, login, pass_hash, role
|
||||||
|
FROM accounts
|
||||||
|
WHERE login = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(login),
|
||||||
|
(None, Some(email)) => sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, email, login, pass_hash, role
|
||||||
|
FROM accounts
|
||||||
|
WHERE email = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(email),
|
||||||
|
(Some(login), Some(email)) => sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, email, login, pass_hash, role
|
||||||
|
FROM accounts
|
||||||
|
WHERE login = $1 AND email = $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(login)
|
||||||
|
.bind(email),
|
||||||
|
_ => return Err(super::Error::Account(Error::NoIdentity)),
|
||||||
|
}
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
super::Error::Account(Error::CantCreate)
|
||||||
|
})
|
||||||
|
}
|
128
api/src/actors/database/stocks.rs
Normal file
128
api/src/actors/database/stocks.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use actix::{Handler, Message, ResponseActFuture, WrapFuture};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
|
||||||
|
use crate::{database, model};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Unable to load all products")]
|
||||||
|
All,
|
||||||
|
#[error("Unable to create product")]
|
||||||
|
Create,
|
||||||
|
#[error("Unable to update product")]
|
||||||
|
Update,
|
||||||
|
#[error("Unable to delete product")]
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<Vec<model::Stock>>")]
|
||||||
|
pub struct AllStocks;
|
||||||
|
|
||||||
|
crate::async_handler!(AllStocks, all, Vec<Stock>);
|
||||||
|
|
||||||
|
async fn all(_msg: AllStocks, pool: PgPool) -> Result<Vec<model::Stock>> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, product_id, quantity, quantity_unit
|
||||||
|
FROM stocks
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
database::Error::Stock(Error::All)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<model::Stock>")]
|
||||||
|
pub struct CreateStock {
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::async_handler!(CreateStock, create_product, Stock);
|
||||||
|
|
||||||
|
async fn create_product(msg: CreateStock, pool: PgPool) -> Result<model::Stock> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
INSERT INTO stocks (product_id, quantity)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, product_id, quantity, quantity_unit
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(msg.product_id)
|
||||||
|
.bind(msg.quantity)
|
||||||
|
.bind(msg.quantity_unit)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
database::Error::Stock(Error::Create)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<model::Stock>")]
|
||||||
|
pub struct UpdateStock {
|
||||||
|
pub id: StockId,
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::async_handler!(UpdateStock, update_product, Stock);
|
||||||
|
|
||||||
|
async fn update_product(msg: UpdateStock, pool: PgPool) -> Result<model::Stock> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
UPDATE stocks
|
||||||
|
SET product_id = $1 AND
|
||||||
|
quantity = $2
|
||||||
|
quantity_unit = $3
|
||||||
|
WHERE id = $4
|
||||||
|
RETURNING id, product_id, quantity, quantity_unit
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(msg.product_id)
|
||||||
|
.bind(msg.quantity)
|
||||||
|
.bind(msg.quantity_unit)
|
||||||
|
.bind(msg.id)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
database::Error::Stock(Error::Update)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<Option<model::Stock>>")]
|
||||||
|
pub struct DeleteStock {
|
||||||
|
pub stock_id: StockId,
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::async_handler!(DeleteStock, delete_product, Option<model::Stock>);
|
||||||
|
|
||||||
|
async fn delete_product(msg: DeleteStock, pool: PgPool) -> Result<Option<Stock>> {
|
||||||
|
sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
DELETE FROM stocks
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, product_id, quantity, quantity_unit
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(msg.stock_id)
|
||||||
|
.fetch_optional(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
database::Error::Stock(Error::Delete)
|
||||||
|
})
|
||||||
|
}
|
23
api/src/logic/mod.rs
Normal file
23
api/src/logic/mod.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
|
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||||
|
|
||||||
|
use crate::model::Password;
|
||||||
|
use crate::PassHash;
|
||||||
|
|
||||||
|
mod order_state;
|
||||||
|
|
||||||
|
pub fn encrypt_password(pass: &Password, salt: &SaltString) -> password_hash::Result<String> {
|
||||||
|
log::debug!("Hashing password {:?}", pass);
|
||||||
|
Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
|
||||||
|
.hash_password(pass.as_bytes(), &salt)?
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_password(pass: &Password, pass_hash: &PassHash) -> password_hash::Result<()> {
|
||||||
|
log::debug!("Validating password {:?} {:?}", pass, pass_hash);
|
||||||
|
|
||||||
|
Argon2::default().verify_password(
|
||||||
|
pass.as_bytes(),
|
||||||
|
&PasswordHash::new(pass_hash.as_str()).expect("Invalid hashed password"),
|
||||||
|
)
|
||||||
|
}
|
@ -11,14 +11,24 @@ use password_hash::SaltString;
|
|||||||
use validator::{validate_email, validate_length};
|
use validator::{validate_email, validate_length};
|
||||||
|
|
||||||
use crate::actors::database;
|
use crate::actors::database;
|
||||||
use crate::logic::hash_pass;
|
use crate::logic::encrypt_password;
|
||||||
use crate::model::{Email, Login, PassHash, Role};
|
use crate::model::{Email, Login, PassHash, Password, Role};
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod logic;
|
pub mod logic;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
|
trait ResolveDbUrl {
|
||||||
|
fn own_db_url(&self) -> Option<String>;
|
||||||
|
|
||||||
|
fn db_url(&self) -> String {
|
||||||
|
self.own_db_url()
|
||||||
|
.or_else(|| std::env::var("DATABASE_URL").ok())
|
||||||
|
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Failed to boot. {0:?}")]
|
#[error("Failed to boot. {0:?}")]
|
||||||
@ -77,13 +87,9 @@ impl Default for ServerOpts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerOpts {
|
impl ResolveDbUrl for ServerOpts {
|
||||||
fn db_url(&self) -> String {
|
fn own_db_url(&self) -> Option<String> {
|
||||||
self.db_url
|
self.db_url.as_deref().map(String::from)
|
||||||
.as_deref()
|
|
||||||
.map(String::from)
|
|
||||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
||||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,13 +99,9 @@ struct MigrateOpts {
|
|||||||
db_url: Option<String>,
|
db_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MigrateOpts {
|
impl ResolveDbUrl for MigrateOpts {
|
||||||
fn db_url(&self) -> String {
|
fn own_db_url(&self) -> Option<String> {
|
||||||
self.db_url
|
self.db_url.as_deref().map(String::from)
|
||||||
.as_deref()
|
|
||||||
.map(String::from)
|
|
||||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
||||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,13 +131,9 @@ struct CreateAccountDefinition {
|
|||||||
db_url: Option<String>,
|
db_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateAccountDefinition {
|
impl ResolveDbUrl for CreateAccountDefinition {
|
||||||
fn db_url(&self) -> String {
|
fn own_db_url(&self) -> Option<String> {
|
||||||
self.db_url
|
self.db_url.as_deref().map(String::from)
|
||||||
.as_deref()
|
|
||||||
.map(String::from)
|
|
||||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
||||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +161,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(actix_web::middleware::Compress::default())
|
.wrap(actix_web::middleware::Compress::default())
|
||||||
|
.wrap(actix_web::middleware::NormalizePath::default())
|
||||||
.wrap(SessionMiddleware::new(
|
.wrap(SessionMiddleware::new(
|
||||||
RedisActorSessionStore::new(redis_connection_string),
|
RedisActorSessionStore::new(redis_connection_string),
|
||||||
secret_key.clone(),
|
secret_key.clone(),
|
||||||
@ -170,7 +169,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
|||||||
.app_data(Data::new(config.clone()))
|
.app_data(Data::new(config.clone()))
|
||||||
.app_data(Data::new(db.clone()))
|
.app_data(Data::new(db.clone()))
|
||||||
.configure(routes::configure)
|
.configure(routes::configure)
|
||||||
.default_service(web::to(HttpResponse::Ok))
|
// .default_service(web::to(HttpResponse::Ok))
|
||||||
})
|
})
|
||||||
.bind((opts.bind, opts.port))
|
.bind((opts.bind, opts.port))
|
||||||
.map_err(Error::Boot)?
|
.map_err(Error::Boot)?
|
||||||
@ -209,11 +208,14 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
|||||||
None => {
|
None => {
|
||||||
let mut s = String::with_capacity(100);
|
let mut s = String::with_capacity(100);
|
||||||
std::io::stdin().read_line(&mut s).map_err(Error::ReadPass)?;
|
std::io::stdin().read_line(&mut s).map_err(Error::ReadPass)?;
|
||||||
|
if let Some(pos) = s.chars().position(|c| c == '\n') {
|
||||||
|
s.remove(pos);
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
let hash = hash_pass(&pass, &config.pass_salt).unwrap();
|
let hash = encrypt_password(&Password(pass), &config.pass_salt).unwrap();
|
||||||
|
|
||||||
db.send(database::CreateAccount {
|
db.send(database::CreateAccount {
|
||||||
email: Email(opts.email),
|
email: Email(opts.email),
|
@ -1,12 +1,13 @@
|
|||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
|
|
||||||
use derive_more::Display;
|
use derive_more::{Deref, Display};
|
||||||
use serde::de::{Error, Visitor};
|
use serde::de::{Error, Visitor};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
pub type RecordId = i32;
|
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 = "lowercase")]
|
||||||
pub enum OrderStatus {
|
pub enum OrderStatus {
|
||||||
#[display(fmt = "Potwierdzone")]
|
#[display(fmt = "Potwierdzone")]
|
||||||
Confirmed,
|
Confirmed,
|
||||||
@ -23,6 +24,7 @@ pub enum OrderStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
|
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
|
||||||
|
#[sqlx(rename_all = "lowercase")]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
#[display(fmt = "Adminitrator")]
|
#[display(fmt = "Adminitrator")]
|
||||||
Admin,
|
Admin,
|
||||||
@ -30,12 +32,36 @@ pub enum Role {
|
|||||||
User,
|
User,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type, Deserialize, Serialize)]
|
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
|
||||||
|
#[sqlx(rename_all = "lowercase")]
|
||||||
|
pub enum QuantityUnit {
|
||||||
|
Gram,
|
||||||
|
Decagram,
|
||||||
|
Kilogram,
|
||||||
|
Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct PriceMajor(NonNegative);
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct PriceMinor(NonNegative);
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Quantity(NonNegative);
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Deserialize, Serialize, Deref, Debug)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Login(pub String);
|
pub struct Login(pub String);
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize)]
|
#[derive(sqlx::Type, Serialize, Deref, Debug)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Email(pub String);
|
pub struct Email(pub String);
|
||||||
@ -69,7 +95,7 @@ impl<'de> serde::Deserialize<'de> for Email {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize)]
|
#[derive(sqlx::Type, Serialize, Deref)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct NonNegative(pub i32);
|
pub struct NonNegative(pub i32);
|
||||||
@ -103,17 +129,17 @@ impl<'de> serde::Deserialize<'de> for NonNegative {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Password(pub String);
|
pub struct Password(pub String);
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct PasswordConfirmation(pub String);
|
pub struct PasswordConfirmation(pub String);
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct PassHash(pub String);
|
pub struct PassHash(pub String);
|
||||||
@ -124,7 +150,7 @@ impl PartialEq<PasswordConfirmation> for Password {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct AccountId(pub RecordId);
|
pub struct AccountId(pub RecordId);
|
||||||
@ -177,16 +203,6 @@ pub struct ProductLongDesc(pub String);
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ProductCategory(pub String);
|
pub struct ProductCategory(pub String);
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
|
||||||
#[sqlx(transparent)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct PriceMajor(NonNegative);
|
|
||||||
|
|
||||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
|
||||||
#[sqlx(transparent)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct PriceMinor(NonNegative);
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Serialize, Deserialize)]
|
#[derive(sqlx::FromRow, Serialize, Deserialize)]
|
||||||
pub struct Product {
|
pub struct Product {
|
||||||
pub id: ProductId,
|
pub id: ProductId,
|
||||||
@ -197,3 +213,16 @@ pub struct Product {
|
|||||||
pub price_major: PriceMajor,
|
pub price_major: PriceMajor,
|
||||||
pub price_minor: PriceMinor,
|
pub price_minor: PriceMinor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Serialize, Deserialize)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct StockId(pub RecordId);
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Serialize, Deserialize)]
|
||||||
|
pub struct Stock {
|
||||||
|
pub id: StockId,
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
8
api/src/routes/admin/api_v1.rs
Normal file
8
api/src/routes/admin/api_v1.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mod products;
|
||||||
|
mod stocks;
|
||||||
|
|
||||||
|
use actix_web::web::{scope, ServiceConfig};
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config.service(scope("/api/v1").configure(products::configure).configure(stocks::configure));
|
||||||
|
}
|
111
api/src/routes/admin/api_v1/products.rs
Normal file
111
api/src/routes/admin/api_v1/products.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use crate::database;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::model::{
|
||||||
|
PriceMajor, PriceMinor, ProductCategory, ProductId, ProductLongDesc, ProductName,
|
||||||
|
ProductShortDesc,
|
||||||
|
};
|
||||||
|
use crate::routes::admin::Error;
|
||||||
|
use crate::routes::RequireLogin;
|
||||||
|
use crate::{admin_send_db, routes};
|
||||||
|
|
||||||
|
use actix::Addr;
|
||||||
|
use actix_session::Session;
|
||||||
|
use actix_web::web::{Data, Json, ServiceConfig};
|
||||||
|
use actix_web::{delete, get, patch, post, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[get("products")]
|
||||||
|
async fn products(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(db, database::AllProducts);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateProduct {
|
||||||
|
pub id: ProductId,
|
||||||
|
pub name: ProductName,
|
||||||
|
pub short_description: ProductShortDesc,
|
||||||
|
pub long_description: ProductLongDesc,
|
||||||
|
pub category: Option<ProductCategory>,
|
||||||
|
pub price_major: PriceMajor,
|
||||||
|
pub price_minor: PriceMinor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("product")]
|
||||||
|
async fn update_product(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<UpdateProduct>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(
|
||||||
|
db,
|
||||||
|
database::UpdateProduct {
|
||||||
|
id: payload.id,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateProduct {
|
||||||
|
pub id: ProductId,
|
||||||
|
pub name: ProductName,
|
||||||
|
pub short_description: ProductShortDesc,
|
||||||
|
pub long_description: ProductLongDesc,
|
||||||
|
pub category: Option<ProductCategory>,
|
||||||
|
pub price_major: PriceMajor,
|
||||||
|
pub price_minor: PriceMinor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("product")]
|
||||||
|
async fn create_product(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<CreateProduct>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(
|
||||||
|
db,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DeleteProduct {
|
||||||
|
pub id: ProductId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("product")]
|
||||||
|
async fn delete_product(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<DeleteProduct>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(db, database::DeleteProduct { product_id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config
|
||||||
|
.service(products)
|
||||||
|
.service(update_product)
|
||||||
|
.service(create_product)
|
||||||
|
.service(delete_product);
|
||||||
|
}
|
92
api/src/routes/admin/api_v1/stocks.rs
Normal file
92
api/src/routes/admin/api_v1/stocks.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::database;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::model::{ProductId, Quantity, QuantityUnit, StockId};
|
||||||
|
use crate::routes::admin::Error;
|
||||||
|
use crate::routes::RequireLogin;
|
||||||
|
use crate::{admin_send_db, routes};
|
||||||
|
|
||||||
|
use actix::Addr;
|
||||||
|
use actix_session::Session;
|
||||||
|
use actix_web::web::{Data, Json, ServiceConfig};
|
||||||
|
use actix_web::{delete, get, patch, post, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[get("stocks")]
|
||||||
|
async fn stocks(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(db, database::AllStocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateStock {
|
||||||
|
pub id: StockId,
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("stock")]
|
||||||
|
async fn update_stock(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<UpdateStock>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(
|
||||||
|
db,
|
||||||
|
database::UpdateStock {
|
||||||
|
id: payload.id,
|
||||||
|
product_id: payload.product_id,
|
||||||
|
quantity: payload.quantity,
|
||||||
|
quantity_unit: payload.quantity_unit
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateStock {
|
||||||
|
pub id: StockId,
|
||||||
|
pub product_id: ProductId,
|
||||||
|
pub quantity: Quantity,
|
||||||
|
pub quantity_unit: QuantityUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("stock")]
|
||||||
|
async fn create_stock(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<CreateStock>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(
|
||||||
|
db,
|
||||||
|
database::CreateStock {
|
||||||
|
product_id: payload.product_id,
|
||||||
|
quantity: payload.quantity,
|
||||||
|
quantity_unit: payload.quantity_unit
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DeleteStock {
|
||||||
|
pub id: StockId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("stock")]
|
||||||
|
async fn delete_stock(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<DeleteStock>,
|
||||||
|
) -> routes::Result<HttpResponse> {
|
||||||
|
session.require_admin()?;
|
||||||
|
|
||||||
|
admin_send_db!(db, database::DeleteStock { stock_id: payload.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config.service(stocks).service(create_stock).service(update_stock).service(delete_stock);
|
||||||
|
}
|
@ -1,15 +1,35 @@
|
|||||||
|
mod api_v1;
|
||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::web::{Data, Json, ServiceConfig};
|
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||||
use actix_web::{delete, get, post, HttpResponse};
|
use actix_web::{delete, get, post, HttpResponse};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::database::{AccountByIdentity, Database};
|
||||||
use crate::logic::hash_pass;
|
use crate::logic::encrypt_password;
|
||||||
use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
|
use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
|
||||||
use crate::routes::{RequireLogin, Result};
|
use crate::routes::{RequireLogin, Result};
|
||||||
use crate::{database, routes, Config};
|
use crate::{database, model, routes, Config};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
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)),
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
Err(crate::routes::Error::Admin(Error::Database(e)))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
Err(crate::routes::Error::Admin(Error::DatabaseConnection))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -26,15 +46,50 @@ pub enum Error {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct LogoutResponse {}
|
pub struct LogoutResponse {}
|
||||||
|
|
||||||
#[delete("/admin/logout")]
|
#[delete("logout")]
|
||||||
async fn logout(session: Session) -> Result<HttpResponse> {
|
async fn logout(session: Session) -> Result<HttpResponse> {
|
||||||
session.require_admin()?;
|
session.require_admin()?;
|
||||||
|
session.clear();
|
||||||
|
|
||||||
Ok(HttpResponse::NotImplemented().body(""))
|
Ok(HttpResponse::NotImplemented().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/admin/sign-in")]
|
#[derive(Deserialize, Debug)]
|
||||||
async fn sign_in(_session: Session) -> Result<HttpResponse> {
|
pub struct SignInInput {
|
||||||
Ok(HttpResponse::NotImplemented().body(""))
|
login: Option<Login>,
|
||||||
|
email: Option<Email>,
|
||||||
|
password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("sign-in")]
|
||||||
|
async fn sign_in(
|
||||||
|
session: Session,
|
||||||
|
db: Data<Addr<Database>>,
|
||||||
|
Json(payload): Json<SignInInput>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
log::debug!("{:?}", payload);
|
||||||
|
let db = db.into_inner();
|
||||||
|
let user: model::FullAccount =
|
||||||
|
match db.send(AccountByIdentity { email: payload.email, login: payload.login }).await {
|
||||||
|
Ok(Ok(user)) => user,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
return Err(routes::Error::Unauthorized);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
return Err(routes::Error::Unauthorized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = crate::logic::validate_password(&payload.password, &user.pass_hash) {
|
||||||
|
log::error!("Password validation failed. {}", e);
|
||||||
|
Err(routes::Error::Unauthorized)
|
||||||
|
} else {
|
||||||
|
if let Err(e) = session.insert("admin_id", *user.id) {
|
||||||
|
log::error!("{:?}", e);
|
||||||
|
}
|
||||||
|
Ok(HttpResponse::Ok().json(model::Account::from(user)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -59,7 +114,7 @@ pub enum RegisterError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// login_required
|
// login_required
|
||||||
#[post("/admin/register")]
|
#[post("register")]
|
||||||
async fn register(
|
async fn register(
|
||||||
session: Session,
|
session: Session,
|
||||||
Json(input): Json<RegisterInput>,
|
Json(input): Json<RegisterInput>,
|
||||||
@ -73,7 +128,7 @@ async fn register(
|
|||||||
response.errors.push(RegisterError::PasswordDiffer);
|
response.errors.push(RegisterError::PasswordDiffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = match hash_pass(&input.password.0, &config.pass_salt) {
|
let hash = match encrypt_password(&input.password, &config.pass_salt) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
@ -105,34 +160,27 @@ async fn register(
|
|||||||
|
|
||||||
response.success = response.errors.is_empty();
|
response.success = response.errors.is_empty();
|
||||||
Ok(if response.success {
|
Ok(if response.success {
|
||||||
HttpResponse::NotImplemented().json(response)
|
HttpResponse::Ok().json(response)
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::BadRequest().json(response)
|
HttpResponse::BadRequest().json(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin/api/v1/products")]
|
|
||||||
async fn api_v1_products(session: Session, db: Data<Addr<Database>>) -> Result<HttpResponse> {
|
|
||||||
session.require_admin()?;
|
|
||||||
|
|
||||||
match db.send(database::AllProducts).await {
|
|
||||||
Ok(Ok(products)) => Ok(HttpResponse::Ok().json(products)),
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
Err(super::Error::Admin(Error::Database(e)))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
Err(super::Error::Admin(Error::DatabaseConnection))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/admin")]
|
#[get("/admin")]
|
||||||
async fn landing() -> Result<HttpResponse> {
|
async fn landing() -> Result<HttpResponse> {
|
||||||
Ok(HttpResponse::NotImplemented().body(""))
|
Ok(HttpResponse::NotImplemented()
|
||||||
|
.append_header(("Content-Type", "text/html"))
|
||||||
|
.body(include_str!("../../../assets/index.html")))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configure(config: &mut ServiceConfig) {
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
config.service(landing).service(sign_in).service(logout).service(register);
|
config
|
||||||
|
.service(
|
||||||
|
scope("/admin")
|
||||||
|
.service(sign_in)
|
||||||
|
.service(logout)
|
||||||
|
.service(register)
|
||||||
|
.service(actix_web::web::scope("/api/v1").configure(api_v1::configure)),
|
||||||
|
)
|
||||||
|
.service(landing);
|
||||||
}
|
}
|
@ -17,7 +17,10 @@ impl RequireLogin for Session {
|
|||||||
fn require_admin(&self) -> Result<RecordId> {
|
fn require_admin(&self) -> Result<RecordId> {
|
||||||
match self.get("admin_id") {
|
match self.get("admin_id") {
|
||||||
Ok(Some(id)) => Ok(id),
|
Ok(Some(id)) => Ok(id),
|
||||||
_ => Err(Error::Unauthorized),
|
_ => {
|
||||||
|
log::debug!("User is not logged as admin");
|
||||||
|
Err(Error::Unauthorized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
31
api/src/routes/public.rs
Normal file
31
api/src/routes/public.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
mod api_v1;
|
||||||
|
|
||||||
|
use actix_web::web::ServiceConfig;
|
||||||
|
use actix_web::{get, HttpResponse};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! public_send_db {
|
||||||
|
($db: expr, $msg: expr) => {{
|
||||||
|
let db = $db;
|
||||||
|
return match db.send($msg).await {
|
||||||
|
Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)),
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
Err(crate::routes::Error::Admin(Error::Database(e)))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
Err(crate::routes::Error::Admin(Error::DatabaseConnection))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn landing() -> HttpResponse {
|
||||||
|
HttpResponse::NotImplemented().body("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config.service(landing).configure(api_v1::configure);
|
||||||
|
}
|
23
api/src/routes/public/api_v1.rs
Normal file
23
api/src/routes/public/api_v1.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use actix::Addr;
|
||||||
|
use actix_web::web::{scope, Data, ServiceConfig};
|
||||||
|
use actix_web::{get, HttpResponse};
|
||||||
|
|
||||||
|
use crate::database;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::public_send_db;
|
||||||
|
use crate::routes::admin::Error;
|
||||||
|
use crate::routes::Result;
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
config.service(scope("/api/v1").service(products).service(stocks));
|
||||||
|
}
|
@ -1,17 +1,24 @@
|
|||||||
CREATE EXTENSION "uuid-ossp";
|
CREATE EXTENSION "uuid-ossp";
|
||||||
|
|
||||||
CREATE TYPE "Role" AS ENUM (
|
CREATE TYPE "Role" AS ENUM (
|
||||||
'Admin',
|
'admin',
|
||||||
'User'
|
'user'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE "OrderStatus" AS ENUM (
|
CREATE TYPE "OrderStatus" AS ENUM (
|
||||||
'Confirmed',
|
'confirmed',
|
||||||
'Cancelled',
|
'cancelled',
|
||||||
'Delivered',
|
'delivered',
|
||||||
'Payed',
|
'payed',
|
||||||
'RequireRefund',
|
'require_refund',
|
||||||
'Refunded'
|
'refunded'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "QuantityUnit" AS ENUM (
|
||||||
|
'g',
|
||||||
|
'dkg',
|
||||||
|
'kg',
|
||||||
|
'piece'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE accounts
|
CREATE TABLE accounts
|
||||||
@ -20,7 +27,7 @@ CREATE TABLE accounts
|
|||||||
email varchar not null unique,
|
email varchar not null unique,
|
||||||
login varchar not null unique,
|
login varchar not null unique,
|
||||||
pass_hash varchar not null,
|
pass_hash varchar not null,
|
||||||
role "Role" not null default 'User'
|
role "Role" not null default 'user'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE products
|
CREATE TABLE products
|
||||||
@ -37,9 +44,10 @@ CREATE TABLE products
|
|||||||
|
|
||||||
CREATE TABLE stocks
|
CREATE TABLE stocks
|
||||||
(
|
(
|
||||||
id serial not null primary key,
|
id serial not null primary key,
|
||||||
product_id int references products (id) not null unique,
|
product_id int references products (id) not null unique,
|
||||||
quantity int not null default 0,
|
quantity int not null default 0,
|
||||||
|
quantity_unit "QuantityUnit" not null,
|
||||||
CONSTRAINT positive_quantity check ( quantity >= 0 )
|
CONSTRAINT positive_quantity check ( quantity >= 0 )
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -47,15 +55,16 @@ CREATE TABLE account_orders
|
|||||||
(
|
(
|
||||||
id serial not null primary key,
|
id serial not null primary key,
|
||||||
buyer_id int references accounts (id) not null,
|
buyer_id int references accounts (id) not null,
|
||||||
status "OrderStatus" not null default 'Confirmed'
|
status "OrderStatus" not null default 'confirmed'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE order_items
|
CREATE TABLE order_items
|
||||||
(
|
(
|
||||||
id serial not null primary key,
|
id serial not null primary key,
|
||||||
product_id int references products (id) not null,
|
product_id int references products (id) not null,
|
||||||
order_id int references account_orders (id),
|
order_id int references account_orders (id),
|
||||||
quantity int not null default 0,
|
quantity int not null default 0,
|
||||||
|
quantity_unit "QuantityUnit" not null,
|
||||||
CONSTRAINT positive_quantity check ( quantity >= 0 )
|
CONSTRAINT positive_quantity check ( quantity >= 0 )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
12
web/index.html
Normal file
12
web/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Bazzar</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,83 +0,0 @@
|
|||||||
use actix::{ActorFutureExt, Handler, ResponseActFuture, WrapFuture};
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
use crate::database::Database;
|
|
||||||
use crate::model::{AccountId, Email, FullAccount, Login, PassHash, Role};
|
|
||||||
|
|
||||||
use super::Result;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Can't create account")]
|
|
||||||
CantCreate,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(actix::Message)]
|
|
||||||
#[rtype(result = "Result<FullAccount>")]
|
|
||||||
pub struct CreateAccount {
|
|
||||||
pub email: Email,
|
|
||||||
pub login: Login,
|
|
||||||
pub pass_hash: PassHash,
|
|
||||||
pub role: Role,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler<CreateAccount> for Database {
|
|
||||||
type Result = ResponseActFuture<Self, Result<FullAccount>>;
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: CreateAccount, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
let db = self.pool.clone();
|
|
||||||
Box::pin(async { create_account(msg, db).await }.into_actor(self).map(|res, _, _| res))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_account(msg: CreateAccount, db: PgPool) -> Result<FullAccount> {
|
|
||||||
sqlx::query_as(
|
|
||||||
r#"
|
|
||||||
INSERT INTO accounts (login, email, role, pass_hash)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING id, email, login, pass_hash, role
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(msg.login)
|
|
||||||
.bind(msg.email)
|
|
||||||
.bind(msg.role)
|
|
||||||
.bind(msg.pass_hash)
|
|
||||||
.fetch_one(&db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("{e:?}");
|
|
||||||
super::Error::Account(Error::CantCreate)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(actix::Message)]
|
|
||||||
#[rtype(result = "Result<FullAccount>")]
|
|
||||||
pub struct FindAccount {
|
|
||||||
pub account_id: AccountId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler<FindAccount> for Database {
|
|
||||||
type Result = ResponseActFuture<Self, Result<FullAccount>>;
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: FindAccount, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
Box::pin(async { find_account(msg, pool).await }.into_actor(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_account(msg: FindAccount, db: PgPool) -> Result<FullAccount> {
|
|
||||||
sqlx::query_as(
|
|
||||||
r#"
|
|
||||||
SELECT id, email, login, pass_hash, role
|
|
||||||
FROM accounts
|
|
||||||
WHERE id = $1
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(msg.account_id)
|
|
||||||
.fetch_one(&db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("{e:?}");
|
|
||||||
super::Error::Account(Error::CantCreate)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
use argon2::Argon2;
|
|
||||||
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
|
||||||
|
|
||||||
mod order_state;
|
|
||||||
|
|
||||||
pub fn hash_pass(pass: &str, salt: &SaltString) -> password_hash::Result<String> {
|
|
||||||
Ok(Argon2::default().hash_password(pass.as_bytes(), &salt)?.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_password(pass: &str, pass_hash: &str) -> password_hash::Result<()> {
|
|
||||||
Argon2::default().verify_password(pass.as_bytes(), &PasswordHash::new(pass_hash)?)
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
use actix_web::web::ServiceConfig;
|
|
||||||
use actix_web::{get, HttpResponse};
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
async fn landing() -> HttpResponse {
|
|
||||||
HttpResponse::NotImplemented().body("")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(config: &mut ServiceConfig) {
|
|
||||||
config.service(landing);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user