All some logic
This commit is contained in:
parent
5bef67cc1e
commit
93fc994d1d
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = [
|
||||
"-C", "link-arg=-fuse-ld=mold",
|
||||
]
|
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
DATABASE_URL=postgres://postgres@localhost/bazzar
|
||||
PASS_SALT=18CHwV7eGFAea16z+qMKZg
|
257
Cargo.lock
generated
257
Cargo.lock
generated
@ -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"
|
||||
|
@ -61,9 +61,9 @@ CREATE TABLE order_items
|
||||
|
||||
CREATE TABLE statistics
|
||||
(
|
||||
id serial not null primary key,
|
||||
url varchar not null,
|
||||
clicks int not null default 0,
|
||||
date DATE not null default now(),
|
||||
id serial not null primary key,
|
||||
url varchar not null,
|
||||
clicks int not null default 0,
|
||||
date DATE not null default now(),
|
||||
CONSTRAINT positive_clicks check ( clicks >= 0 )
|
||||
);
|
||||
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
||||
max_width = 100
|
||||
use_small_heuristics = "Max"
|
@ -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"] }
|
||||
|
@ -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>;
|
||||
}
|
83
web/src/actors/database/accounts.rs
Normal file
83
web/src/actors/database/accounts.rs
Normal 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)
|
||||
})
|
||||
}
|
163
web/src/actors/database/products.rs
Normal file
163
web/src/actors/database/products.rs
Normal 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)
|
||||
})
|
||||
}
|
@ -1 +1 @@
|
||||
pub mod database;
|
||||
pub mod database;
|
||||
|
@ -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)?)
|
||||
}
|
||||
|
250
web/src/main.rs
250
web/src/main.rs
@ -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))?
|
||||
.run()
|
||||
.await
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
180
web/src/model.rs
180
web/src/model.rs
@ -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,
|
||||
}
|
||||
|
@ -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
138
web/src/routes/admin/mod.rs
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user