All some logic

This commit is contained in:
Adrian Woźniak 2022-04-14 21:40:26 +02:00
parent 5bef67cc1e
commit 93fc994d1d
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
16 changed files with 1217 additions and 52 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = [
"-C", "link-arg=-fuse-ld=mold",
]

2
.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL=postgres://postgres@localhost/bazzar
PASS_SALT=18CHwV7eGFAea16z+qMKZg

257
Cargo.lock generated
View File

@ -2,6 +2,29 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "actix"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3720d0064a0ce5c0de7bd93bdb0a6caebab2a9b5668746145d7b3b0c5da02914"
dependencies = [
"actix-rt 2.7.0",
"bitflags",
"bytes 1.1.0",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot 0.11.2",
"pin-project-lite 0.2.8",
"smallvec",
"tokio 1.17.0",
"tokio-util 0.6.9",
]
[[package]] [[package]]
name = "actix" name = "actix"
version = "0.13.0" version = "0.13.0"
@ -45,7 +68,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f396220495e64a3dd22e1f8cd16345fa494f8d9c3e79bfd92c74c7911b811c19" checksum = "f396220495e64a3dd22e1f8cd16345fa494f8d9c3e79bfd92c74c7911b811c19"
dependencies = [ dependencies = [
"actix", "actix 0.13.0",
"ahash", "ahash",
"log", "log",
] ]
@ -307,6 +330,27 @@ dependencies = [
"twoway", "twoway",
] ]
[[package]]
name = "actix-redis"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dde9fa8bde15d084d459eb59f766c08d00a6f550e7054187878fc9cbaa19115"
dependencies = [
"actix 0.12.0",
"actix-rt 2.7.0",
"actix-service 2.0.2",
"actix-tls 3.0.3",
"actix-web 4.0.1",
"backoff",
"derive_more",
"futures-core",
"log",
"redis-async",
"time 0.3.9",
"tokio 1.17.0",
"tokio-util 0.6.9",
]
[[package]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.2.7" version = "0.2.7"
@ -419,6 +463,28 @@ dependencies = [
"pin-project-lite 0.2.8", "pin-project-lite 0.2.8",
] ]
[[package]]
name = "actix-session"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9138a66462f1e65da829f9c0de81b44a96dfe193a4f19bfea32ee2be312368"
dependencies = [
"actix 0.12.0",
"actix-redis",
"actix-service 2.0.2",
"actix-utils 3.0.0",
"actix-web 4.0.1",
"anyhow",
"async-trait",
"derive_more",
"futures-core",
"rand 0.8.5",
"serde",
"serde_json",
"time 0.3.9",
"tracing",
]
[[package]] [[package]]
name = "actix-testing" name = "actix-testing"
version = "1.0.1" version = "1.0.1"
@ -464,6 +530,22 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "actix-tls"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297"
dependencies = [
"actix-codec 0.5.0",
"actix-rt 2.7.0",
"actix-service 2.0.2",
"actix-utils 3.0.0",
"futures-core",
"log",
"pin-project-lite 0.2.8",
"tokio-util 0.7.1",
]
[[package]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "1.0.6" version = "1.0.6"
@ -527,7 +609,7 @@ dependencies = [
"actix-service 1.0.6", "actix-service 1.0.6",
"actix-testing", "actix-testing",
"actix-threadpool", "actix-threadpool",
"actix-tls", "actix-tls 1.0.0",
"actix-utils 1.0.6", "actix-utils 1.0.6",
"actix-web-codegen 0.2.2", "actix-web-codegen 0.2.2",
"awc", "awc",
@ -721,6 +803,24 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "anyhow"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
[[package]]
name = "argon2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
dependencies = [
"base64ct",
"blake2",
"password-hash",
"rayon",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.2" version = "0.7.2"
@ -793,6 +893,17 @@ dependencies = [
"serde_urlencoded 0.6.1", "serde_urlencoded 0.6.1",
] ]
[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
dependencies = [
"getrandom 0.2.6",
"instant",
"rand 0.8.5",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.64" version = "0.3.64"
@ -836,30 +947,37 @@ checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179"
name = "bazzar" name = "bazzar"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix", "actix 0.13.0",
"actix-auth", "actix-auth",
"actix-broker", "actix-broker",
"actix-cors", "actix-cors",
"actix-files", "actix-files",
"actix-identity 0.4.0", "actix-identity 0.4.0",
"actix-multipart", "actix-multipart",
"actix-redis",
"actix-rt 2.7.0", "actix-rt 2.7.0",
"actix-session",
"actix-web 4.0.1", "actix-web 4.0.1",
"actix-web-opentelemetry", "actix-web-opentelemetry",
"argon2",
"chrono", "chrono",
"derive_more", "derive_more",
"dotenv", "dotenv",
"gumdrop",
"log", "log",
"parking_lot 0.12.0", "parking_lot 0.12.0",
"password-hash", "password-hash",
"pretty_env_logger", "pretty_env_logger",
"rand_core 0.6.3",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tera", "tera",
"thiserror",
"toml", "toml",
"tracing", "tracing",
"uuid", "uuid",
"validator",
] ]
[[package]] [[package]]
@ -885,6 +1003,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388"
dependencies = [
"digest 0.10.3",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.7.3" version = "0.7.3"
@ -1158,6 +1285,31 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.5" version = "0.3.5"
@ -1614,6 +1766,26 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "gumdrop"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3"
dependencies = [
"gumdrop_derive",
]
[[package]]
name = "gumdrop_derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.2.7" version = "0.2.7"
@ -2672,6 +2844,45 @@ dependencies = [
"rand_core 0.5.1", "rand_core 0.5.1",
] ]
[[package]]
name = "rayon"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redis-async"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76b00c604527d485d7a146d1e324ec1cf0a5ec522acb3d05bf7d51a9c28d7c0c"
dependencies = [
"bytes 1.1.0",
"futures-channel",
"futures-sink",
"futures-util",
"log",
"tokio 1.17.0",
"tokio-util 0.6.9",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.13" version = "0.2.13"
@ -3478,6 +3689,20 @@ dependencies = [
"tokio 0.2.25", "tokio 0.2.25",
] ]
[[package]]
name = "tokio-util"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
dependencies = [
"bytes 1.1.0",
"futures-core",
"futures-sink",
"log",
"pin-project-lite 0.2.8",
"tokio 1.17.0",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.1" version = "0.7.1"
@ -3749,6 +3974,32 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "validator"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0f08911ab0fee2c5009580f04615fa868898ee57de10692a45da0c3bcc3e5e"
dependencies = [
"idna",
"lazy_static",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_types",
]
[[package]]
name = "validator_types"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@ -61,9 +61,9 @@ CREATE TABLE order_items
CREATE TABLE statistics CREATE TABLE statistics
( (
id serial not null primary key, id serial not null primary key,
url varchar not null, url varchar not null,
clicks int not null default 0, clicks int not null default 0,
date DATE not null default now(), date DATE not null default now(),
CONSTRAINT positive_clicks check ( clicks >= 0 ) CONSTRAINT positive_clicks check ( clicks >= 0 )
); );

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
max_width = 100
use_small_heuristics = "Max"

View File

@ -14,6 +14,10 @@ actix-multipart = { version = "0.4.0" }
actix-broker = { version = "0.4.2" } actix-broker = { version = "0.4.2" }
actix-identity = { version = "0.4.0" } actix-identity = { version = "0.4.0" }
actix-web-opentelemetry = { version = "0.12.0" } actix-web-opentelemetry = { version = "0.12.0" }
actix-session = { version = "0.6.2", features = ["actix-redis", "redis-actor-session"] }
actix-redis = { version = "0.11.0" }
gumdrop = { version = "0.8.1" }
tera = { version = "1.15.0" } tera = { version = "1.15.0" }
@ -28,6 +32,10 @@ toml = { version = "0.5.8" }
sqlx = { version = "0.5.11", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } sqlx = { version = "0.5.11", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] }
thiserror = { version = "1.0.30" }
validator = { version = "0.14.0" }
log = { version = "0.4.16" } log = { version = "0.4.16" }
pretty_env_logger = { version = "0.4.0" } pretty_env_logger = { version = "0.4.0" }
@ -36,4 +44,6 @@ dotenv = { version = "0.15.0" }
derive_more = { version = "0.99.17" } derive_more = { version = "0.99.17" }
parking_lot = { version = "0.12.0" } parking_lot = { version = "0.12.0" }
password-hash = { version = "0.4.0" } password-hash = { version = "0.4.0", features = ["alloc"] }
argon2 = { version = "0.4.0", features = ["parallel", "password-hash"] }
rand_core = { version = "0.6", features = ["std"] }

View File

@ -0,0 +1,59 @@
use actix::{Actor, Context};
use sqlx::PgPool;
pub use accounts::*;
pub use products::*;
mod accounts;
mod products;
#[macro_export]
macro_rules! async_handler {
($msg: ty, $async: ident, $res: ty) => {
impl Handler<$msg> for Database {
type Result = ResponseActFuture<Self, Result<$res>>;
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
let pool = self.pool.clone();
Box::pin(async { $async(msg, pool).await }.into_actor(self))
}
}
};
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to connect to database. {0:?}")]
Connect(sqlx::Error),
#[error("{0}")]
Account(accounts::Error),
#[error("{0}")]
Product(products::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Database {
pool: PgPool,
}
impl Clone for Database {
fn clone(&self) -> Self {
Self { pool: self.pool.clone() }
}
}
impl Database {
pub(crate) async fn build(url: &str) -> Result<Self> {
let pool = sqlx::PgPool::connect(url).await.map_err(Error::Connect)?;
Ok(Database { pool })
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
}
impl Actor for Database {
type Context = Context<Self>;
}

View File

@ -0,0 +1,83 @@
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)
})
}

View File

@ -0,0 +1,163 @@
use actix::{Handler, Message, ResponseActFuture, WrapFuture};
use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::model::{
PriceMajor, PriceMinor, Product, ProductCategory, ProductId, ProductLongDesc, ProductName,
ProductShortDesc,
};
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::Product>>")]
pub struct AllProducts;
crate::async_handler!(AllProducts, all, Vec<Product>);
async fn all(_msg: AllProducts, pool: PgPool) -> Result<Vec<model::Product>> {
sqlx::query_as(
r#"
SELECT id,
name,
short_description,
long_description,
category,
price_major,
price_minor
FROM products
"#,
)
.fetch_all(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Product(Error::All)
})
}
#[derive(Message)]
#[rtype(result = "Result<model::Product>")]
pub struct CreateProduct {
pub name: ProductName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>,
pub price_major: PriceMajor,
pub price_minor: PriceMinor,
}
crate::async_handler!(CreateProduct, create_product, Product);
async fn create_product(msg: CreateProduct, pool: PgPool) -> Result<model::Product> {
sqlx::query_as(
r#"
INSERT INTO products (name, short_description, long_description, category, price_major, price_minor)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
name,
short_description,
long_description,
category,
price_major,
price_minor
"#,
)
.bind(msg.name)
.bind(msg.short_description)
.bind(msg.long_description)
.bind(msg.category)
.bind(msg.price_major)
.bind(msg.price_minor)
.fetch_one(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Product(Error::Create)
})
}
#[derive(Message)]
#[rtype(result = "Result<model::Product>")]
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,
}
crate::async_handler!(UpdateProduct, update_product, Product);
async fn update_product(msg: UpdateProduct, pool: PgPool) -> Result<model::Product> {
sqlx::query_as(
r#"
UPDATE products
SET name = $1 AND
short_description = $2 AND
long_description = $3 AND
category = $4 AND
price_major = $5 AND
price_minor = $6
WHERE id = $7
RETURNING id,
name,
short_description,
long_description,
category,
price_major,
price_minor
"#,
)
.bind(msg.name)
.bind(msg.short_description)
.bind(msg.long_description)
.bind(msg.category)
.bind(msg.price_major)
.bind(msg.price_minor)
.bind(msg.id)
.fetch_one(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Product(Error::Update)
})
}
#[derive(Message)]
#[rtype(result = "Result<Option<model::Product>>")]
pub struct DeleteProduct {
pub product_id: ProductId,
}
crate::async_handler!(DeleteProduct, delete_product, Option<model::Product>);
async fn delete_product(msg: DeleteProduct, pool: PgPool) -> Result<Option<Product>> {
sqlx::query_as(
r#"
DELETE FROM products
WHERE id = $1
"#,
)
.bind(msg.product_id)
.fetch_optional(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Product(Error::Delete)
})
}

View File

@ -1 +1 @@
pub mod database; pub mod database;

View File

@ -1 +1,12 @@
mod order_state; 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)?)
}

View File

@ -1,14 +1,242 @@
mod actors; use std::sync::Arc;
mod logic;
mod model;
mod routes;
use actix_web::{App, HttpServer}; use actix::Actor;
use actix_session::{storage::RedisActorSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use actix_web::middleware::Logger;
use actix_web::web::Data;
use actix_web::{web, App, HttpResponse, HttpServer};
use gumdrop::Options;
use password_hash::SaltString;
use validator::{validate_email, validate_length};
#[actix_web::main] // or #[tokio::main] use crate::actors::database;
async fn main() -> std::io::Result<()> { use crate::logic::hash_pass;
HttpServer::new(|| App::new().configure(routes::configure)) use crate::model::{Email, Login, PassHash, Role};
.bind(("127.0.0.1", 8080))?
.run() pub mod actors;
.await pub mod logic;
pub mod model;
pub mod routes;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to boot. {0:?}")]
Boot(std::io::Error),
#[error("Unable to read password file. {0:?}")]
PassFile(std::io::Error),
#[error("Unable to read password from STDIN. {0:?}")]
ReadPass(std::io::Error),
#[error("{0}")]
Database(#[from] database::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Options, Debug)]
struct Opts {
help: bool,
#[options(command)]
cmd: Option<Command>,
}
#[derive(Options, Debug)]
enum Command {
#[options(help = "Run server")]
Server(ServerOpts),
#[options(help = "Migrate database")]
Migrate(MigrateOpts),
#[options(help = "Generate new salt for passwords")]
GenerateHash(GenerateHashOpts),
#[options(help = "Create new account")]
CreateAccount(CreateAccountOpts),
}
impl Default for Command {
fn default() -> Self {
Command::Server(ServerOpts::default())
}
}
#[derive(Options, Debug)]
struct GenerateHashOpts {
help: bool,
}
#[derive(Options, Debug)]
struct ServerOpts {
help: bool,
bind: String,
port: u16,
db_url: Option<String>,
}
impl Default for ServerOpts {
fn default() -> Self {
Self { help: false, bind: "0.0.0.0".to_string(), port: 8080, db_url: None }
}
}
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"))
}
}
#[derive(Options, Debug)]
struct MigrateOpts {
help: bool,
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"))
}
}
#[derive(Debug, Options)]
struct CreateAccountOpts {
help: bool,
#[options(command)]
cmd: Option<CreateAccountCmd>,
}
#[derive(Debug, Options)]
enum CreateAccountCmd {
Admin(CreateAccountDefinition),
User(CreateAccountDefinition),
}
#[derive(Debug, Options)]
struct CreateAccountDefinition {
help: bool,
#[options(free)]
login: String,
#[options(free)]
email: String,
#[options(free)]
pass_file: Option<String>,
#[options(help = "Database url, it will also look for DATABASE_URL env")]
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"))
}
}
pub struct Config {
pass_salt: SaltString,
}
impl Config {
fn load() -> Self {
let pass_salt =
SaltString::new(&std::env::var("PASS_SALT").expect("PASS_SALT is required"))
.expect("Invalid password salt");
Self { pass_salt }
}
}
async fn server(opts: ServerOpts) -> Result<()> {
let secret_key = Key::generate();
let redis_connection_string = "127.0.0.1:6379";
let config = Arc::new(Config::load());
let db = database::Database::build(&opts.db_url()).await?.start();
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.wrap(actix_web::middleware::Compress::default())
.wrap(SessionMiddleware::new(
RedisActorSessionStore::new(redis_connection_string),
secret_key.clone(),
))
.app_data(Data::new(config.clone()))
.app_data(Data::new(db.clone()))
.configure(routes::configure)
.default_service(web::to(HttpResponse::Ok))
})
.bind((opts.bind, opts.port))
.map_err(Error::Boot)?
.run()
.await
.map_err(Error::Boot)
}
async fn migrate(opts: MigrateOpts) -> Result<()> {
let db = database::Database::build(&opts.db_url()).await?;
sqlx::migrate!("../db/migrate").run(db.pool()).await.expect("Failed to migrate");
Ok(())
}
async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> {
use argon2::password_hash::rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
println!("{salt}");
Ok(())
}
async fn create_account(opts: CreateAccountOpts) -> Result<()> {
let (role, opts) = match opts.cmd.expect("Account type is mandatory") {
CreateAccountCmd::Admin(opts) => (Role::Admin, opts),
CreateAccountCmd::User(opts) => (Role::User, opts),
};
if !validate_email(&opts.email) {
panic!("Invalid email address");
}
if !validate_length(&opts.login, Some(4), Some(100), None) {
panic!("Login must have at least 4 characters and no more than 100");
}
let db = database::Database::build(&opts.db_url()).await?.start();
let pass = match opts.pass_file {
Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?,
None => {
let mut s = String::with_capacity(100);
std::io::stdin().read_line(&mut s).map_err(Error::ReadPass)?;
s
}
};
let config = Config::load();
let hash = hash_pass(&pass, &config.pass_salt).unwrap();
db.send(database::CreateAccount {
email: Email(opts.email),
login: Login(opts.login),
pass_hash: PassHash(hash),
role,
})
.await
.unwrap()
.unwrap();
Ok(())
}
#[actix_web::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
pretty_env_logger::init();
let opts: Opts = gumdrop::Options::parse_args_default_or_exit();
match opts.cmd.unwrap_or_default() {
Command::Migrate(opts) => migrate(opts).await,
Command::Server(opts) => server(opts).await,
Command::GenerateHash(opts) => generate_hash(opts).await,
Command::CreateAccount(opts) => create_account(opts).await,
}
} }

View File

@ -1,6 +1,12 @@
use derive_more::Display; use std::fmt::Formatter;
#[derive(sqlx::Type, Copy, Clone, Debug, Display)] use derive_more::Display;
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
pub type RecordId = i32;
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
pub enum OrderStatus { pub enum OrderStatus {
#[display(fmt = "Potwierdzone")] #[display(fmt = "Potwierdzone")]
Confirmed, Confirmed,
@ -16,10 +22,178 @@ pub enum OrderStatus {
Refunded, Refunded,
} }
#[derive(sqlx::Type, Copy, Clone, Debug, Display)] #[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
pub enum Role { pub enum Role {
#[display(fmt = "Adminitrator")] #[display(fmt = "Adminitrator")]
Admin, Admin,
#[display(fmt = "Użytkownik")] #[display(fmt = "Użytkownik")]
User, User,
} }
#[derive(sqlx::Type, Deserialize, Serialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct Login(pub String);
#[derive(sqlx::Type, Serialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct Email(pub String);
impl<'de> serde::Deserialize<'de> for Email {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct EmailVisitor {}
impl<'v> Visitor<'v> for EmailVisitor {
type Value = String;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("this is not valid e-mail address")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
if validator::validate_email(s) {
Ok(String::from(s))
} else {
Err(E::custom("this is not email address"))
}
}
}
Ok(Email(deserializer.deserialize_str(EmailVisitor {})?))
}
}
#[derive(sqlx::Type, Serialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct NonNegative(pub i32);
impl<'de> serde::Deserialize<'de> for NonNegative {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct NonNegativeVisitor;
impl<'v> Visitor<'v> for NonNegativeVisitor {
type Value = i32;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("this is not valid e-mail address")
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: Error,
{
if v >= 0 {
Ok(v)
} else {
Err(E::custom("Value must be equal or greater than 0"))
}
}
}
Ok(NonNegative(deserializer.deserialize_i32(NonNegativeVisitor)?))
}
}
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct Password(pub String);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct PasswordConfirmation(pub String);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct PassHash(pub String);
impl PartialEq<PasswordConfirmation> for Password {
fn eq(&self, other: &PasswordConfirmation) -> bool {
self.0 == other.0
}
}
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct AccountId(pub RecordId);
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct FullAccount {
pub id: AccountId,
pub email: Email,
pub login: Login,
pub pass_hash: PassHash,
pub role: Role,
}
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct Account {
pub id: AccountId,
pub email: Email,
pub login: Login,
pub role: Role,
}
impl From<FullAccount> for Account {
fn from(FullAccount { id, email, login, pass_hash: _, role }: FullAccount) -> Self {
Self { id, email, login, role }
}
}
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ProductId(pub RecordId);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ProductName(pub String);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ProductShortDesc(pub String);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ProductLongDesc(pub String);
#[derive(sqlx::Type, Serialize, Deserialize)]
#[sqlx(transparent)]
#[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,
pub name: ProductName,
pub short_description: ProductShortDesc,
pub long_description: ProductLongDesc,
pub category: Option<ProductCategory>,
pub price_major: PriceMajor,
pub price_minor: PriceMinor,
}

View File

@ -1,23 +0,0 @@
// use actix_auth::login_required;
use actix_identity::Identity;
use actix_web::web::ServiceConfig;
use actix_web::{delete, get, post, HttpResponse};
#[delete("/admin/logout")]
async fn logout() -> HttpResponse {
HttpResponse::NotImplemented().body("")
}
#[post("/admin/sign-in")]
async fn sign_in(_id: Identity) -> HttpResponse {
HttpResponse::NotImplemented().body("")
}
#[get("/admin")]
async fn landing() -> HttpResponse {
HttpResponse::NotImplemented().body("")
}
pub fn configure(config: &mut ServiceConfig) {
config.service(landing).service(sign_in).service(logout);
}

138
web/src/routes/admin/mod.rs Normal file
View File

@ -0,0 +1,138 @@
use actix::Addr;
use actix_session::Session;
use actix_web::web::{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::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
use crate::routes::{RequireLogin, Result};
use crate::{database, routes, Config};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Can't register new account")]
Register,
#[error("Can't hash password")]
HashPass,
#[error("Internal server error")]
DatabaseConnection,
#[error("{0}")]
Database(#[from] database::Error),
}
#[derive(Serialize)]
pub struct LogoutResponse {}
#[delete("/admin/logout")]
async fn logout(session: Session) -> Result<HttpResponse> {
session.require_admin()?;
Ok(HttpResponse::NotImplemented().body(""))
}
#[post("/admin/sign-in")]
async fn sign_in(_session: Session) -> Result<HttpResponse> {
Ok(HttpResponse::NotImplemented().body(""))
}
#[derive(Deserialize)]
pub struct RegisterInput {
pub login: Login,
pub email: Email,
pub password: Password,
pub password_confirmation: PasswordConfirmation,
pub role: Role,
}
#[derive(Serialize, Default)]
pub struct RegisterResponse {
pub success: bool,
pub errors: Vec<RegisterError>,
pub account: Option<Account>,
}
#[derive(Serialize)]
pub enum RegisterError {
PasswordDiffer,
}
// login_required
#[post("/admin/register")]
async fn register(
session: Session,
Json(input): Json<RegisterInput>,
db: Data<Addr<Database>>,
config: Data<Arc<Config>>,
) -> Result<HttpResponse> {
let mut response = RegisterResponse::default();
session.require_admin()?;
if input.password != input.password_confirmation {
response.errors.push(RegisterError::PasswordDiffer);
}
let hash = match hash_pass(&input.password.0, &config.pass_salt) {
Ok(s) => s,
Err(e) => {
log::error!("{e:?}");
return Err(routes::Error::Admin(Error::HashPass));
}
};
match db
.send(database::CreateAccount {
email: input.email,
login: input.login,
pass_hash: PassHash(hash),
role: input.role,
})
.await
{
Ok(Ok(account)) => {
response.account = Some(account.into());
}
Ok(Err(e)) => {
log::error!("{}", e);
return Err(super::Error::Admin(Error::Register));
}
Err(e) => {
log::error!("{}", e);
return Err(super::Error::Admin(Error::Register));
}
};
response.success = response.errors.is_empty();
Ok(if response.success {
HttpResponse::NotImplemented().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(""))
}
pub fn configure(config: &mut ServiceConfig) {
config.service(landing).service(sign_in).service(logout).service(register);
}

View File

@ -1,10 +1,72 @@
mod admin; pub mod admin;
mod public; pub mod public;
use crate::model::RecordId;
use crate::routes;
use actix_session::Session;
use actix_web::body::BoxBody;
use actix_web::web::ServiceConfig; use actix_web::web::ServiceConfig;
use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError};
use std::fmt::{Debug, Display, Formatter};
pub trait RequireLogin {
fn require_admin(&self) -> Result<RecordId>;
}
impl RequireLogin for Session {
fn require_admin(&self) -> Result<RecordId> {
match self.get("admin_id") {
Ok(Some(id)) => Ok(id),
_ => Err(Error::Unauthorized),
}
}
}
pub enum Error {
Unauthorized,
Admin(routes::admin::Error),
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
#[derive(serde::Serialize)]
pub struct Failure {
pub errors: Vec<String>,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Error::Unauthorized => String::from("Unauthorized"),
Error::Admin(e) => format!("{}", e),
};
f.write_str(&serde_json::to_string(&Failure { errors: vec![msg] }).unwrap())
}
}
impl ResponseError for Error {}
impl Responder for Error {
type Body = BoxBody;
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
match self {
Error::Unauthorized => HttpResponse::Unauthorized()
.content_type("application/json")
.body(format!("{}", self)),
Error::Admin(_) => HttpResponse::InternalServerError()
.content_type("application/json")
.body(format!("{}", self)),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
config config.configure(public::configure).configure(admin::configure);
.configure(public::configure)
.configure(admin::configure);
} }