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
|
||||
PASS_SALT=18CHwV7eGFAea16z+qMKZg
|
||||
RUST_LOG=debug
|
||||
|
@ -1,2 +1,2 @@
|
||||
[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 products::*;
|
||||
pub use stocks::*;
|
||||
|
||||
mod accounts;
|
||||
mod products;
|
||||
mod stocks;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! async_handler {
|
||||
@ -29,6 +31,8 @@ pub enum Error {
|
||||
Account(accounts::Error),
|
||||
#[error("{0}")]
|
||||
Product(products::Error),
|
||||
#[error("{0}")]
|
||||
Stock(stocks::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 crate::actors::database;
|
||||
use crate::logic::hash_pass;
|
||||
use crate::model::{Email, Login, PassHash, Role};
|
||||
use crate::logic::encrypt_password;
|
||||
use crate::model::{Email, Login, PassHash, Password, Role};
|
||||
|
||||
pub mod actors;
|
||||
pub mod logic;
|
||||
pub mod model;
|
||||
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)]
|
||||
pub enum Error {
|
||||
#[error("Failed to boot. {0:?}")]
|
||||
@ -77,13 +87,9 @@ impl Default for ServerOpts {
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerOpts {
|
||||
fn db_url(&self) -> String {
|
||||
self.db_url
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
||||
impl ResolveDbUrl for ServerOpts {
|
||||
fn own_db_url(&self) -> Option<String> {
|
||||
self.db_url.as_deref().map(String::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,13 +99,9 @@ struct MigrateOpts {
|
||||
db_url: Option<String>,
|
||||
}
|
||||
|
||||
impl MigrateOpts {
|
||||
fn db_url(&self) -> String {
|
||||
self.db_url
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
||||
impl ResolveDbUrl for MigrateOpts {
|
||||
fn own_db_url(&self) -> Option<String> {
|
||||
self.db_url.as_deref().map(String::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,13 +131,9 @@ struct CreateAccountDefinition {
|
||||
db_url: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateAccountDefinition {
|
||||
fn db_url(&self) -> String {
|
||||
self.db_url
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
||||
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
||||
impl ResolveDbUrl for CreateAccountDefinition {
|
||||
fn own_db_url(&self) -> Option<String> {
|
||||
self.db_url.as_deref().map(String::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,6 +161,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.wrap(actix_web::middleware::NormalizePath::default())
|
||||
.wrap(SessionMiddleware::new(
|
||||
RedisActorSessionStore::new(redis_connection_string),
|
||||
secret_key.clone(),
|
||||
@ -170,7 +169,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
.app_data(Data::new(config.clone()))
|
||||
.app_data(Data::new(db.clone()))
|
||||
.configure(routes::configure)
|
||||
.default_service(web::to(HttpResponse::Ok))
|
||||
// .default_service(web::to(HttpResponse::Ok))
|
||||
})
|
||||
.bind((opts.bind, opts.port))
|
||||
.map_err(Error::Boot)?
|
||||
@ -209,11 +208,14 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
||||
None => {
|
||||
let mut s = String::with_capacity(100);
|
||||
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
|
||||
}
|
||||
};
|
||||
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 {
|
||||
email: Email(opts.email),
|
@ -1,12 +1,13 @@
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use derive_more::Display;
|
||||
use derive_more::{Deref, Display};
|
||||
use serde::de::{Error, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub type RecordId = i32;
|
||||
|
||||
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
pub enum OrderStatus {
|
||||
#[display(fmt = "Potwierdzone")]
|
||||
Confirmed,
|
||||
@ -23,6 +24,7 @@ pub enum OrderStatus {
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
#[display(fmt = "Adminitrator")]
|
||||
Admin,
|
||||
@ -30,12 +32,36 @@ pub enum Role {
|
||||
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)]
|
||||
#[serde(transparent)]
|
||||
pub struct Login(pub String);
|
||||
|
||||
#[derive(sqlx::Type, Serialize)]
|
||||
#[derive(sqlx::Type, Serialize, Deref, Debug)]
|
||||
#[sqlx(transparent)]
|
||||
#[serde(transparent)]
|
||||
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)]
|
||||
#[serde(transparent)]
|
||||
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)]
|
||||
#[serde(transparent)]
|
||||
pub struct Password(pub String);
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
|
||||
#[sqlx(transparent)]
|
||||
#[serde(transparent)]
|
||||
pub struct PasswordConfirmation(pub String);
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Deref, Debug)]
|
||||
#[sqlx(transparent)]
|
||||
#[serde(transparent)]
|
||||
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)]
|
||||
#[serde(transparent)]
|
||||
pub struct AccountId(pub RecordId);
|
||||
@ -177,16 +203,6 @@ pub struct ProductLongDesc(pub String);
|
||||
#[serde(transparent)]
|
||||
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)]
|
||||
pub struct Product {
|
||||
pub id: ProductId,
|
||||
@ -197,3 +213,16 @@ pub struct Product {
|
||||
pub price_major: PriceMajor,
|
||||
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_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 serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::logic::hash_pass;
|
||||
use crate::database::{AccountByIdentity, Database};
|
||||
use crate::logic::encrypt_password;
|
||||
use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
|
||||
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)]
|
||||
pub enum Error {
|
||||
@ -26,15 +46,50 @@ pub enum Error {
|
||||
#[derive(Serialize)]
|
||||
pub struct LogoutResponse {}
|
||||
|
||||
#[delete("/admin/logout")]
|
||||
#[delete("logout")]
|
||||
async fn logout(session: Session) -> Result<HttpResponse> {
|
||||
session.require_admin()?;
|
||||
session.clear();
|
||||
|
||||
Ok(HttpResponse::NotImplemented().body(""))
|
||||
}
|
||||
|
||||
#[post("/admin/sign-in")]
|
||||
async fn sign_in(_session: Session) -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::NotImplemented().body(""))
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SignInInput {
|
||||
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)]
|
||||
@ -59,7 +114,7 @@ pub enum RegisterError {
|
||||
}
|
||||
|
||||
// login_required
|
||||
#[post("/admin/register")]
|
||||
#[post("register")]
|
||||
async fn register(
|
||||
session: Session,
|
||||
Json(input): Json<RegisterInput>,
|
||||
@ -73,7 +128,7 @@ async fn register(
|
||||
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,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
@ -105,34 +160,27 @@ async fn register(
|
||||
|
||||
response.success = response.errors.is_empty();
|
||||
Ok(if response.success {
|
||||
HttpResponse::NotImplemented().json(response)
|
||||
HttpResponse::Ok().json(response)
|
||||
} else {
|
||||
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")]
|
||||
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) {
|
||||
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> {
|
||||
match self.get("admin_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 TYPE "Role" AS ENUM (
|
||||
'Admin',
|
||||
'User'
|
||||
'admin',
|
||||
'user'
|
||||
);
|
||||
|
||||
CREATE TYPE "OrderStatus" AS ENUM (
|
||||
'Confirmed',
|
||||
'Cancelled',
|
||||
'Delivered',
|
||||
'Payed',
|
||||
'RequireRefund',
|
||||
'Refunded'
|
||||
'confirmed',
|
||||
'cancelled',
|
||||
'delivered',
|
||||
'payed',
|
||||
'require_refund',
|
||||
'refunded'
|
||||
);
|
||||
|
||||
CREATE TYPE "QuantityUnit" AS ENUM (
|
||||
'g',
|
||||
'dkg',
|
||||
'kg',
|
||||
'piece'
|
||||
);
|
||||
|
||||
CREATE TABLE accounts
|
||||
@ -20,7 +27,7 @@ CREATE TABLE accounts
|
||||
email varchar not null unique,
|
||||
login varchar not null unique,
|
||||
pass_hash varchar not null,
|
||||
role "Role" not null default 'User'
|
||||
role "Role" not null default 'user'
|
||||
);
|
||||
|
||||
CREATE TABLE products
|
||||
@ -40,6 +47,7 @@ CREATE TABLE stocks
|
||||
id serial not null primary key,
|
||||
product_id int references products (id) not null unique,
|
||||
quantity int not null default 0,
|
||||
quantity_unit "QuantityUnit" not null,
|
||||
CONSTRAINT positive_quantity check ( quantity >= 0 )
|
||||
);
|
||||
|
||||
@ -47,7 +55,7 @@ CREATE TABLE account_orders
|
||||
(
|
||||
id serial not null primary key,
|
||||
buyer_id int references accounts (id) not null,
|
||||
status "OrderStatus" not null default 'Confirmed'
|
||||
status "OrderStatus" not null default 'confirmed'
|
||||
);
|
||||
|
||||
CREATE TABLE order_items
|
||||
@ -56,6 +64,7 @@ CREATE TABLE order_items
|
||||
product_id int references products (id) not null,
|
||||
order_id int references account_orders (id),
|
||||
quantity int not null default 0,
|
||||
quantity_unit "QuantityUnit" not null,
|
||||
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