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.
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]]
name = "actix"
version = "0.13.0"
@ -45,7 +68,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f396220495e64a3dd22e1f8cd16345fa494f8d9c3e79bfd92c74c7911b811c19"
dependencies = [
"actix",
"actix 0.13.0",
"ahash",
"log",
]
@ -307,6 +330,27 @@ dependencies = [
"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]]
name = "actix-router"
version = "0.2.7"
@ -419,6 +463,28 @@ dependencies = [
"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]]
name = "actix-testing"
version = "1.0.1"
@ -464,6 +530,22 @@ dependencies = [
"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]]
name = "actix-utils"
version = "1.0.6"
@ -527,7 +609,7 @@ dependencies = [
"actix-service 1.0.6",
"actix-testing",
"actix-threadpool",
"actix-tls",
"actix-tls 1.0.0",
"actix-utils 1.0.6",
"actix-web-codegen 0.2.2",
"awc",
@ -721,6 +803,24 @@ dependencies = [
"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]]
name = "arrayvec"
version = "0.7.2"
@ -793,6 +893,17 @@ dependencies = [
"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]]
name = "backtrace"
version = "0.3.64"
@ -836,30 +947,37 @@ checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179"
name = "bazzar"
version = "0.1.0"
dependencies = [
"actix",
"actix 0.13.0",
"actix-auth",
"actix-broker",
"actix-cors",
"actix-files",
"actix-identity 0.4.0",
"actix-multipart",
"actix-redis",
"actix-rt 2.7.0",
"actix-session",
"actix-web 4.0.1",
"actix-web-opentelemetry",
"argon2",
"chrono",
"derive_more",
"dotenv",
"gumdrop",
"log",
"parking_lot 0.12.0",
"password-hash",
"pretty_env_logger",
"rand_core 0.6.3",
"serde",
"serde_json",
"sqlx",
"tera",
"thiserror",
"toml",
"tracing",
"uuid",
"validator",
]
[[package]]
@ -885,6 +1003,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "block-buffer"
version = "0.7.3"
@ -1158,6 +1285,31 @@ dependencies = [
"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]]
name = "crossbeam-queue"
version = "0.3.5"
@ -1614,6 +1766,26 @@ dependencies = [
"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]]
name = "h2"
version = "0.2.7"
@ -2672,6 +2844,45 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.2.13"
@ -3478,6 +3689,20 @@ dependencies = [
"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]]
name = "tokio-util"
version = "0.7.1"
@ -3749,6 +3974,32 @@ dependencies = [
"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]]
name = "vcpkg"
version = "0.2.15"

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-identity = { version = "0.4.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" }
@ -28,6 +32,10 @@ toml = { version = "0.5.8" }
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" }
pretty_env_logger = { version = "0.4.0" }
@ -36,4 +44,6 @@ dotenv = { version = "0.15.0" }
derive_more = { version = "0.99.17" }
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,12 @@
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;
mod logic;
mod model;
mod routes;
use std::sync::Arc;
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]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().configure(routes::configure))
.bind(("127.0.0.1", 8080))?
use crate::actors::database;
use crate::logic::hash_pass;
use crate::model::{Email, Login, PassHash, Role};
pub mod actors;
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 {
#[display(fmt = "Potwierdzone")]
Confirmed,
@ -16,10 +22,178 @@ pub enum OrderStatus {
Refunded,
}
#[derive(sqlx::Type, Copy, Clone, Debug, Display)]
#[derive(sqlx::Type, Copy, Clone, Debug, Display, Deserialize, Serialize)]
pub enum Role {
#[display(fmt = "Adminitrator")]
Admin,
#[display(fmt = "Użytkownik")]
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;
mod public;
pub mod admin;
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::{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) {
config
.configure(public::configure)
.configure(admin::configure);
config.configure(public::configure).configure(admin::configure);
}