Seed products with photos

This commit is contained in:
eraden 2022-05-08 09:47:05 +02:00
parent ff86e9929c
commit 01d5dde052
34 changed files with 933 additions and 95 deletions

82
Cargo.lock generated
View File

@ -413,6 +413,15 @@ dependencies = [
"syn",
]
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@ -573,6 +582,21 @@ dependencies = [
"rand",
]
[[package]]
name = "backtrace"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base-x"
version = "0.2.10"
@ -607,7 +631,6 @@ dependencies = [
"actix-web",
"actix-web-httpauth",
"actix-web-opentelemetry",
"argon2",
"async-trait",
"cart_manager",
"chrono",
@ -620,6 +643,7 @@ dependencies = [
"futures",
"futures-util",
"gumdrop",
"human-panic",
"jemallocator",
"log",
"messagebus",
@ -627,10 +651,8 @@ dependencies = [
"oauth2",
"order_manager",
"parking_lot 0.12.0",
"password-hash",
"payment_manager",
"pretty_env_logger",
"rand_core",
"search_manager",
"serde",
"serde_json",
@ -1116,10 +1138,15 @@ dependencies = [
"database_manager",
"dotenv",
"fake",
"fs_manager",
"human-panic",
"log",
"model",
"password-hash",
"pretty_env_logger",
"rand",
"thiserror",
"tokio",
]
[[package]]
@ -1519,6 +1546,12 @@ dependencies = [
"polyval",
]
[[package]]
name = "gimli"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
[[package]]
name = "git2"
version = "0.13.25"
@ -1751,6 +1784,21 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "human-panic"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39f357a500abcbd7c5f967c1d45c8838585b36743823b9d43488f24850534e36"
dependencies = [
"backtrace",
"os_type",
"serde",
"serde_derive",
"termcolor",
"toml",
"uuid",
]
[[package]]
name = "humansize"
version = "1.1.1"
@ -2184,10 +2232,14 @@ dependencies = [
name = "model"
version = "0.1.0"
dependencies = [
"argon2",
"chrono",
"derive_more",
"fake",
"log",
"password-hash",
"rand",
"rand_core",
"serde",
"sqlx",
"sqlx-core",
@ -2315,6 +2367,15 @@ dependencies = [
"url",
]
[[package]]
name = "object"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.10.0"
@ -2412,6 +2473,15 @@ dependencies = [
"uuid",
]
[[package]]
name = "os_type"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3df761f6470298359f84fcfb60d86db02acc22c251c37265c07a3d1057d2389"
dependencies = [
"regex",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -2887,6 +2957,12 @@ dependencies = [
"serde",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustc_version"
version = "0.2.3"

View File

@ -3,8 +3,7 @@ use fake::Fake;
use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role};
use sqlx::PgPool;
use super::Result;
use crate::db_async_handler;
use crate::{db_async_handler, Result};
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -40,12 +39,10 @@ FROM accounts
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(actix::Message)]
#[derive(actix::Message, Debug)]
#[rtype(result = "Result<FullAccount>")]
pub struct CreateAccount {
#[cfg_attr(feature = "dummy", dummy("fake::faker::internet::en::FreeEmail"))]
pub email: Email,
#[cfg_attr(feature = "dummy", dummy("fake::faker::internet::en::Username"))]
pub login: Login,
pub pass_hash: PassHash,
pub role: Role,

View File

@ -4,12 +4,40 @@ use crate::Result;
pub enum Error {
#[error("Failed to create photo")]
Create,
#[error("Failed to fetch all photo")]
All,
}
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::Photo>>")]
pub struct AllPhotos;
crate::db_async_handler!(AllPhotos, all_photos, Vec<model::Photo>, inner_all_photos);
pub(crate) async fn all_photos<'e, E>(_msg: AllPhotos, pool: E) -> Result<Vec<model::Photo>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
SELECT id, local_path, file_name
FROM photos
"#,
)
.fetch_all(pool)
.await
.map_err(|e| {
log::error!("{e:?}");
crate::Error::Photo(Error::All)
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::Photo>")]
pub struct CreatePhoto {
/// Local FILE path
pub local_path: model::LocalPath,
/// Only file name, this part should be also included in `local_path`
pub file_name: model::FileName,
}

View File

@ -1,2 +1,79 @@
use crate::{db_async_handler, Result};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
pub enum Error {
#[error("Failed to attach photo to product")]
Create,
#[error("Failed to load all product photos")]
All,
}
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::ProductPhoto>>")]
pub struct AllProductPhotos;
crate::db_async_handler!(
AllProductPhotos,
all_product_photos,
Vec<model::ProductPhoto>,
inner_all_product_photos
);
pub(crate) async fn all_product_photos<'e, E>(
_msg: AllProductPhotos,
pool: E,
) -> Result<Vec<model::ProductPhoto>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
SELECT id, product_id, photo_id
FROM product_photos
"#,
)
.fetch_all(pool)
.await
.map_err(|e| {
log::error!("{e:?}");
crate::Error::ProductPhoto(Error::All)
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::ProductPhoto>")]
pub struct CreateProductPhoto {
pub product_id: model::ProductId,
pub photo_id: model::PhotoId,
}
db_async_handler!(
CreateProductPhoto,
create_product_photo,
model::ProductPhoto,
inner_create_product_photo
);
pub(crate) async fn create_product_photo<'e, E>(
msg: CreateProductPhoto,
pool: E,
) -> Result<model::ProductPhoto>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
INSERT INTO product_photos(product_id, photo_id)
VALUES ($1, $2)
RETURNING id, product_id, photo_id
"#,
)
.bind(msg.product_id)
.bind(msg.photo_id)
.fetch_one(pool)
.await
.map_err(|e| {
log::error!("{:?}", e);
crate::Error::ProductPhoto(Error::Create)
})
}

View File

@ -53,7 +53,7 @@ FROM products
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Message)]
#[derive(Message, Debug)]
#[rtype(result = "Result<model::Product>")]
pub struct CreateProduct {
pub name: ProductName,

View File

@ -21,6 +21,54 @@ macro_rules! fs_async_handler {
};
}
#[macro_export]
macro_rules! query_fs {
($fs: expr, $msg: expr, default $fail: expr) => {
match $fs.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{e}");
$fail
}
Err(e) => {
log::error!("{e:?}");
$fail
}
}
};
($fs: expr, $msg: expr, panic) => {
match $fs.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{e}");
panic!("{:?}", e);
}
Err(e) => {
log::error!("{e:?}");
panic!("{:?}", e);
}
}
};
($fs: expr, $msg: expr, $fail: expr) => {
$crate::query_db!($fs, $msg, $fail, $fail)
};
($fs: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
match $fs.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{e}");
return Err($db_fail);
}
Err(e) => {
log::error!("{e:?}");
return Err($act_fail);
}
}
};
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Can't access file system. Please check privileges")]
@ -98,7 +146,9 @@ pub(crate) async fn remove_file(msg: RemoveFile, config: SharedAppConfig) -> Res
}
pub struct WriteResult {
pub file_name: FileName,
/// Unique file name created with UUID and original file extension
pub unique_name: FileName,
/// Path to file in local storage
pub local_path: LocalPath,
}
@ -117,6 +167,8 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
mut stream,
} = msg;
log::debug!("Writing file {:?}", file_name);
let p = std::path::Path::new(&file_name);
let ext = p
.file_name()
@ -126,7 +178,7 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
.and_then(OsStr::to_str)
.map(String::from);
let file_name = format!(
let unique_name = format!(
"{}{}",
uuid::Uuid::new_v4(),
ext.map(|s| format!(".{s}")).unwrap_or_default()
@ -139,21 +191,34 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
let path = std::path::PathBuf::new()
.join(&output_path)
.join(&file_name);
.join(&unique_name);
log::debug!(
"File {:?} will be written as {:?} at {:?}",
file_name,
unique_name,
path
);
let mut file = match std::fs::File::create(&path) {
Ok(f) => f,
Err(e) => return Err(Error::CantWrite(e)),
};
let mut counter = 0;
while let Some(b) = stream.recv().await {
counter += 1;
if counter % 100_000 == 0 {
log::debug!("Wrote {} for {:?}", counter, file_name);
}
match file.write(&[b]) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::StorageFull => return Err(Error::NoSpace),
Err(e) => return Err(Error::CantWrite(e)),
}
}
log::debug!("File {:?} successfully written", unique_name);
Ok(WriteResult {
file_name: FileName::from(file_name),
unique_name: FileName::from(unique_name),
local_path: LocalPath::from(path.to_str().unwrap_or_default().to_string()),
})
}

View File

@ -15,6 +15,8 @@ search_manager = { path = "../actors/search_manager" }
token_manager = { path = "../actors/token_manager" }
fs_manager = { path = "../actors/fs_manager" }
human-panic = { version = "1.0.3" }
actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", features = [] }
@ -56,10 +58,6 @@ dotenv = { version = "0.15", features = [] }
derive_more = { version = "0.99", features = [] }
parking_lot = { version = "0.12", features = [] }
password-hash = { version = "0.4", features = ["alloc"] }
argon2 = { version = "0.4", features = ["parallel", "password-hash"] }
rand_core = { version = "0.6", features = ["std"] }
tokio = { version = "1.17", features = ["full"] }
futures = { version = "0.3", features = [] }
futures-util = { version = "0.3", features = [] }

View File

@ -1,25 +1 @@
use argon2::{Algorithm, Argon2, Params, Version};
use model::Password;
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use crate::PassHash;
mod order_state;
pub fn encrypt_password(pass: &Password, salt: &SaltString) -> password_hash::Result<String> {
log::debug!("Hashing password {:?}", pass);
Ok(
Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
.hash_password(pass.as_bytes(), &salt)?
.to_string(),
)
}
pub fn validate_password(pass: &Password, pass_hash: &PassHash) -> password_hash::Result<()> {
log::debug!("Validating password {:?} {:?}", pass, pass_hash);
Argon2::default().verify_password(
pass.as_bytes(),
&PasswordHash::new(pass_hash.as_str()).expect("Invalid hashed password"),
)
}
pub mod order_state;

View File

@ -11,16 +11,13 @@ use actix_web::{App, HttpServer};
use config::UpdateConfig;
use email_manager::TestMail;
use jemallocator::Jemalloc;
use model::{Email, Login, PassHash, Password, Role};
use model::{Email, Encrypt, Login, PassHash, Password, Role};
use opts::{
Command, CreateAccountCmd, CreateAccountOpts, GenerateHashOpts, MigrateOpts, Opts, ServerOpts,
TestMailerOpts,
};
use password_hash::SaltString;
use validator::{validate_email, validate_length};
use crate::logic::encrypt_password;
pub mod logic;
mod opts;
pub mod routes;
@ -119,9 +116,7 @@ async fn migrate(opts: MigrateOpts) -> Result<()> {
}
async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> {
use argon2::password_hash::rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
println!("{salt}");
model::print_hash();
Ok(())
}
@ -164,7 +159,9 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
if pass.trim().is_empty() {
panic!("Password cannot be empty!");
}
let hash = encrypt_password(&Password::from(pass), &config.lock().web().pass_salt()).unwrap();
let hash = Password::from(pass)
.encrypt(&config.lock().web().pass_salt())
.unwrap();
db.send(database_manager::CreateAccount {
email: Email::from(opts.email),
@ -204,6 +201,8 @@ async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
#[actix_web::main]
async fn main() -> Result<()> {
human_panic::setup_panic!();
dotenv::dotenv().ok();
pretty_env_logger::init();

View File

@ -6,10 +6,9 @@ use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_web::{delete, get, post, HttpResponse};
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
use model::{Account, Email, Encrypt, Login, PassHash, Password, PasswordConfirmation, Role};
use serde::{Deserialize, Serialize};
use crate::logic::encrypt_password;
use crate::routes;
use crate::routes::{RequireLogin, Result};
@ -79,7 +78,7 @@ async fn sign_in(
},
routes::Error::Unauthorized
);
if let Err(e) = crate::logic::validate_password(&payload.password, &user.pass_hash) {
if let Err(e) = payload.password.validate(&user.pass_hash) {
log::error!("Password validation failed. {}", e);
Err(routes::Error::Unauthorized)
} else {
@ -126,7 +125,7 @@ async fn register(
response.errors.push(RegisterError::PasswordDiffer);
}
let hash = match encrypt_password(&input.password, &config.lock().web().pass_salt()) {
let hash = match input.password.encrypt(&config.lock().web().pass_salt()) {
Ok(s) => s,
Err(e) => {
log::error!("{e:?}");

View File

@ -4,11 +4,11 @@ use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{get, patch, post, HttpResponse};
use config::SharedAppConfig;
use database_manager::Database;
use model::{AccountId, AccountState, PasswordConfirmation};
use model::{AccountId, AccountState, Encrypt, PasswordConfirmation};
use crate::routes::admin::Error;
use crate::routes::RequireLogin;
use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Password, Role};
use crate::{admin_send_db, routes, Email, Login, PassHash, Password, Role};
#[get("/accounts")]
pub async fn accounts(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
@ -45,7 +45,7 @@ pub async fn update_account(
routes::admin::Error::DifferentPasswords,
));
}
let hash = match encrypt_password(&p1, &config.lock().web().pass_salt()) {
let hash = match p1.encrypt(&config.lock().web().pass_salt()) {
Ok(hash) => hash,
Err(e) => {
log::error!("{e:?}");
@ -97,7 +97,7 @@ pub async fn create_account(
routes::admin::Error::DifferentPasswords,
));
}
let hash = match encrypt_password(&payload.password, &config.lock().web().pass_salt()) {
let hash = match payload.password.encrypt(&config.lock().web().pass_salt()) {
Ok(hash) => hash,
Err(e) => {
log::error!("{e:?}");

View File

@ -64,7 +64,7 @@ async fn upload_product_image(
let write = async {
let fs_manager::WriteResult {
local_path,
file_name,
unique_name: file_name,
} = match fs.send(msg).await {
Ok(Ok(file_name)) => file_name,
Ok(Err(e)) => {

View File

@ -1,12 +1,12 @@
use actix::Addr;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{get, post, HttpResponse};
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use model::{Audience, FullAccount, Token, TokenString};
use model::{Audience, Encrypt, FullAccount, Token, TokenString};
use payment_manager::{PaymentManager, PaymentNotification};
use token_manager::TokenManager;
use crate::logic::validate_password;
use crate::routes::public::Error as PublicError;
use crate::routes::{self, Result};
use crate::{public_send_db, Login, Password};
@ -21,6 +21,44 @@ async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
public_send_db!(db.into_inner(), database_manager::AllStocks)
}
#[derive(serde::Deserialize)]
pub struct CreateAccountInput {
pub email: model::Email,
pub login: Login,
pub password: Password,
pub password_confirmation: model::PasswordConfirmation,
}
#[post("/register")]
pub async fn create_account(
db: Data<Addr<Database>>,
Json(payload): Json<CreateAccountInput>,
config: Data<SharedAppConfig>,
) -> routes::Result<HttpResponse> {
if payload.password != payload.password_confirmation {
return Err(routes::Error::Admin(
routes::admin::Error::DifferentPasswords,
));
}
let hash = match payload.password.encrypt(&config.lock().web().pass_salt()) {
Ok(hash) => hash,
Err(e) => {
log::error!("{e:?}");
return Err(routes::Error::Admin(routes::admin::Error::HashPass));
}
};
public_send_db!(
db,
database_manager::CreateAccount {
email: payload.email,
login: payload.login,
pass_hash: model::PassHash::from(hash),
role: model::Role::User,
}
);
}
#[derive(serde::Deserialize)]
pub struct SignInInput {
pub login: String,
@ -50,7 +88,10 @@ async fn sign_in(
routes::Error::Public(PublicError::DatabaseConnection),
routes::Error::Public(PublicError::DatabaseConnection)
);
if validate_password(&Password::from(payload.password), &account.pass_hash).is_err() {
if Password::from(payload.password)
.validate(&account.pass_hash)
.is_err()
{
return Err(routes::Error::Unauthorized);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -7,11 +7,14 @@ edition = "2021"
model = { path = "../shared/model", version = "0.1", features = ["db", "dummy"] }
config = { path = "../shared/config" }
database_manager = { path = "../actors/database_manager", features = ["dummy"] }
fs_manager = { path = "../actors/fs_manager", features = [] }
actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", features = [] }
tokio = { version = "1.18.1", features = ["full"] }
fake = { version = "2.4.3", features = ["derive", "chrono", "http"] }
rand = { version = "0.8.5" }
@ -19,3 +22,9 @@ dotenv = { version = "0.15", features = [] }
log = { version = "0.4", features = [] }
pretty_env_logger = { version = "0.4", features = [] }
password-hash = { version = "0.4", features = ["alloc"] }
thiserror = { version = "1.0.31" }
human-panic = { version = "1.0.3" }

37
db-seed/src/accounts.rs Normal file
View File

@ -0,0 +1,37 @@
use actix::Addr;
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use fake::{Fake, Faker};
use crate::{Result, SharedState};
pub(crate) async fn create_accounts(
db: Addr<Database>,
seed: SharedState,
_config: SharedAppConfig,
) -> Result<()> {
let accounts: Vec<model::FullAccount> =
query_db!(db, database_manager::AllAccounts, default vec![]);
if accounts.len() >= 10 {
seed.lock().unwrap().accounts = accounts;
return Ok(());
}
let mut accounts = Vec::with_capacity(10);
for _ in 0..10 {
let msg: database_manager::CreateAccount = Faker.fake::<database_manager::CreateAccount>();
match db.send(msg).await {
Ok(Ok(account)) => accounts.push(account),
Ok(Err(e)) => {
log::error!("{e}")
}
Err(e) => {
log::error!("{e}")
}
}
}
seed.lock().unwrap().accounts = accounts;
Ok(())
}

View File

@ -1,37 +1,63 @@
mod accounts;
mod photos;
mod product_photos;
mod products;
use std::sync::{Arc, Mutex};
use actix::Actor;
use config::{AppConfig, UpdateConfig};
use fake::{Fake, Faker};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Photo with name {0:?} was not found")]
PhotoNotFound(String),
#[error("Failed to create product record in database")]
DbProduct,
#[error("Failed to attach photo {1:?} to product {0:?} in database")]
DbProductPhoto(model::ProductId, model::PhotoId),
#[error("Product with name {0:?} does not exists")]
NoProduct(String),
#[error("Failed to create photo record for file {0:?}")]
WritePhoto(String),
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Opts;
impl UpdateConfig for Opts {
fn update_config(&self, _config: &mut AppConfig) {}
}
#[derive(Default)]
pub(crate) struct DbSeed {
pub accounts: Vec<model::FullAccount>,
pub products: Vec<model::Product>,
pub photos: Vec<model::Photo>,
}
pub(crate) type SharedState = Arc<Mutex<DbSeed>>;
#[actix_web::main]
async fn main() {
human_panic::setup_panic!();
dotenv::dotenv().ok();
std::env::set_var("RUST_LOG", "DEBUG");
pretty_env_logger::init();
let db_seed = Arc::new(Mutex::new(DbSeed::default()));
let config = config::default_load(&Opts);
let db = database_manager::Database::build(config)
let db = database_manager::Database::build(config.clone())
.await
.unwrap()
.start();
let mut users = Vec::with_capacity(10);
for _ in 0..10 {
match db
.send(Faker.fake::<database_manager::CreateAccount>())
.await
{
Ok(Ok(user)) => users.push(user),
Ok(Err(e)) => {
log::error!("{e}")
}
Err(e) => {
log::error!("{e}")
}
}
}
let res = tokio::join!(
accounts::create_accounts(db.clone(), db_seed.clone(), config.clone()),
products::create_products(db.clone(), db_seed.clone(), config.clone())
);
res.0.unwrap();
res.1.unwrap();
}

152
db-seed/src/photos.rs Normal file
View File

@ -0,0 +1,152 @@
use actix::{Actor, Addr};
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use fs_manager::query_fs;
use tokio::io::AsyncReadExt;
use crate::{Result, SharedState};
async fn create_photo(
db: Addr<Database>,
seed: SharedState,
fs: Addr<fs_manager::FsManager>,
file: &'static str,
) -> crate::Result<()> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let read = async move {
let mut file =
tokio::fs::File::open(std::path::Path::new("./assets/examples/images").join(file))
.await
.unwrap();
while let Ok(b) = file.read_u8().await {
tx.send(b).unwrap();
}
};
let write = async {
let fs_manager::WriteResult {
unique_name: _,
local_path,
} = query_fs!(
fs,
fs_manager::WriteFile {
file_name: file.to_string(),
stream: rx,
},
panic
);
let photo: model::Photo = query_db!(
db,
database_manager::CreatePhoto {
local_path,
file_name: model::FileName::new(file)
},
crate::Error::WritePhoto(file.into())
);
seed.lock().unwrap().photos.push(photo);
Ok(())
};
let (_, res) = tokio::join!(read, write);
log::debug!("Photo {:?} done", file);
res
}
pub(crate) async fn create_photos(
db: Addr<Database>,
seed: SharedState,
config: SharedAppConfig,
) -> Result<()> {
log::debug!("Creating photos");
let photos: Vec<model::Photo> = query_db!(db, database_manager::AllPhotos, default vec![]);
if photos.len() >= 10 {
seed.lock().unwrap().photos = photos;
return Ok(());
}
let fs = fs_manager::FsManager::build(config.clone())
.await
.unwrap()
.start();
let results = tokio::join!(
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-alex-azabache-3907507.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-binoid-cbd-3612182.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-caio-1279107.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-eprism-studio-335257.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-gabriel-freytez-341523.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-jess-bailey-designs-913135.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-luis-quintero-1738641.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-math-90946.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-mike-380954.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-pixabay-279906.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-Venus-HD-Make-up-and-perfume-2587370.webp"
)
);
results.0.unwrap();
results.1.unwrap();
results.2.unwrap();
results.3.unwrap();
results.4.unwrap();
results.5.unwrap();
results.6.unwrap();
results.7.unwrap();
results.8.unwrap();
results.9.unwrap();
results.10.unwrap();
Ok(())
}

View File

@ -0,0 +1,139 @@
use actix::Addr;
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use crate::{Result, SharedState};
async fn create_product_photo(
db: Addr<Database>,
seed: SharedState,
name: &'static str,
file_name: &'static str,
) -> crate::Result<()> {
let product_id = {
seed.lock()
.unwrap()
.products
.iter()
.find(|p| {
let _x = 1;
p.name.as_str() == name
})
.map(|p| p.id)
.ok_or_else(|| {
let _x = 1;
crate::Error::NoProduct(name.into())
})?
};
let photo_id = {
seed.lock()
.unwrap()
.photos
.iter()
.find(|p| p.file_name.as_str() == file_name)
.map(|p| p.id)
.ok_or_else(|| crate::Error::PhotoNotFound(file_name.into()))?
};
query_db!(
db,
database_manager::CreateProductPhoto {
product_id,
photo_id,
},
crate::Error::DbProductPhoto(product_id, photo_id)
);
Ok(())
}
pub(crate) async fn create_product_photos(
db: Addr<Database>,
seed: SharedState,
_config: SharedAppConfig,
) -> Result<()> {
let product_photos = query_db!(db, database_manager::AllProductPhotos, default vec![]);
if product_photos.len() >= 10 {
return Ok(());
}
let results = tokio::join!(
create_product_photo(
db.clone(),
seed.clone(),
"Nikon",
"pexels-alex-azabache-3907507.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Bonoid CBD",
"pexels-binoid-cbd-3612182.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Casio Speaker",
"pexels-caio-1279107.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Eprism Studio",
"pexels-eprism-studio-335257.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Best Phones 2022",
"pexels-gabriel-freytez-341523.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Sweet cake",
"pexels-jess-bailey-designs-913135.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Lexal 128G",
"pexels-luis-quintero-1738641.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Fujifilm X-T10",
"pexels-math-90946.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Sweet Tower",
"pexels-mike-380954.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Nikon Lenses",
"pexels-pixabay-279906.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Venus HD Professional",
"pexels-Venus-HD-Make-up-and-perfume-2587370.webp"
)
);
results.0.unwrap();
results.1.unwrap();
results.2.unwrap();
results.3.unwrap();
results.4.unwrap();
results.5.unwrap();
results.6.unwrap();
results.7.unwrap();
results.8.unwrap();
results.9.unwrap();
results.10.unwrap();
Ok(())
}

87
db-seed/src/products.rs Normal file
View File

@ -0,0 +1,87 @@
use actix::Addr;
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use fake::{Fake, Faker};
use model::{ProductCategory, ProductName};
use crate::product_photos::create_product_photos;
use crate::{Result, SharedState};
async fn create_product(
db: Addr<Database>,
seed: SharedState,
name: &'static str,
category: &'static str,
) -> crate::Result<()> {
let seed = seed.clone();
let db = db.clone();
let product = query_db!(
db,
database_manager::CreateProduct {
name: ProductName::from(String::from(name)),
short_description: Faker.fake(),
long_description: Faker.fake(),
category: Some(ProductCategory::new(category)),
price: Faker.fake(),
deliver_days_flag: Faker.fake(),
},
crate::Error::DbProduct
);
seed.lock().unwrap().products.push(product);
Ok(())
}
pub(crate) async fn create_products(
db: Addr<Database>,
seed: SharedState,
config: SharedAppConfig,
) -> Result<()> {
crate::photos::create_photos(db.clone(), seed.clone(), config.clone())
.await
.unwrap();
let products = query_db!(db, database_manager::AllProducts, default vec![]);
if products.len() >= 10 {
{
seed.lock().unwrap().products = products;
}
if let Err(e) = create_product_photos(db.clone(), seed.clone(), config.clone()).await {
log::error!("{e:?}");
}
return Ok(());
}
let results = tokio::join!(
create_product(db.clone(), seed.clone(), "Nikon", "Cameras",),
create_product(db.clone(), seed.clone(), "Bonoid CBD", "Drugstore",),
create_product(db.clone(), seed.clone(), "Casio Speaker", "Speakers",),
create_product(db.clone(), seed.clone(), "Eprism Studio", "Drugstore",),
create_product(db.clone(), seed.clone(), "Best Phones 2022", "Phones",),
create_product(db.clone(), seed.clone(), "Sweet cake", "Sweets",),
create_product(db.clone(), seed.clone(), "Lexal 128G", "Memory",),
create_product(db.clone(), seed.clone(), "Fujifilm X-T10", "Cameras",),
create_product(db.clone(), seed.clone(), "Sweet Tower", "Sweets",),
create_product(db.clone(), seed.clone(), "Nikon Lenses", "Cameras",),
create_product(
db.clone(),
seed.clone(),
"Venus HD Professional",
"Drugstore",
)
);
results.0.unwrap();
results.1.unwrap();
results.2.unwrap();
results.3.unwrap();
results.4.unwrap();
results.5.unwrap();
results.6.unwrap();
results.7.unwrap();
results.8.unwrap();
results.9.unwrap();
results.10.unwrap();
create_product_photos(db.clone(), seed.clone(), config.clone()).await
}

View File

@ -22,5 +22,11 @@ thiserror = { version = "1.0.31" }
validator = { version = "0.15.0" }
fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid"], optional = true }
fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid", "dummy"], optional = true }
rand = { version = "0.8.5", optional = true }
password-hash = { version = "0.4", features = ["alloc"] }
argon2 = { version = "0.4", features = ["parallel", "password-hash"] }
rand_core = { version = "0.6", features = ["std"] }
log = { version = "0.4.17" }

78
shared/model/src/dummy.rs Normal file
View File

@ -0,0 +1,78 @@
use fake::faker::internet::en::{FreeEmail, Password as FakePass, Username};
use fake::Fake;
use crate::*;
impl<T> fake::Dummy<T> for Login {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
Self(Username().fake())
}
}
impl<T> fake::Dummy<T> for Email {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
Self(FreeEmail().fake())
}
}
impl<T> fake::Dummy<T> for ProductShortDesc {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::lorem::en::Words;
let words: Vec<String> = Words(5..20).fake();
Self(words.join(" "))
}
}
impl<T> fake::Dummy<T> for ProductLongDesc {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::lorem::en::Paragraphs;
let words: Vec<String> = Paragraphs(10..20).fake();
Self(words.join("\n"))
}
}
impl<T> fake::Dummy<T> for Password {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
let pass: String = FakePass(6..20).fake();
Self(pass)
}
}
impl<T> fake::Dummy<T> for PassHash {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
let pass: Password = fake::Faker.fake();
Self(
pass.encrypt(
&password_hash::SaltString::new("a7sydd98asd98ahsda9shdahd98ahsd9aysd9aysd9y")
.unwrap(),
)
.unwrap(),
)
}
}
impl<T> fake::Dummy<T> for ProductName {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::lorem::en::Words;
let name: Vec<String> = Words(5..8).fake();
Self(name.join(" "))
}
}
impl<T> fake::Dummy<T> for Price {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
let price = rand::random::<u8>() as i32 * 100i32 + rand::random::<u8>() as i32;
let price: i32 = price as i32;
Self(NonNegative(price))
}
}
impl<T> fake::Dummy<T> for NonNegative {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
let price = rand::random::<u8>() as i32 * 100i32 + rand::random::<u8>() as i32;
let price: i32 = price as i32;
Self(price)
}
}

View File

@ -0,0 +1,34 @@
use argon2::{Algorithm, Argon2, Params, Version};
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
pub trait Encrypt {
fn encrypt(&self, salt: &SaltString) -> password_hash::Result<String>;
fn validate(&self, pass_hash: &crate::PassHash) -> password_hash::Result<()>;
}
impl Encrypt for crate::Password {
fn encrypt(&self, salt: &SaltString) -> password_hash::Result<String> {
log::debug!("Hashing password {:?}", self);
Ok(
Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
.hash_password(self.as_bytes(), &salt)?
.to_string(),
)
}
fn validate(&self, pass_hash: &crate::PassHash) -> password_hash::Result<()> {
log::debug!("Validating password {:?} {:?}", self, pass_hash);
Argon2::default().verify_password(
self.as_bytes(),
&PasswordHash::new(pass_hash.as_str()).expect("Invalid hashed password"),
)
}
}
pub fn print_hash() {
use argon2::password_hash::rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
println!("{salt}");
}

View File

@ -2,15 +2,22 @@
pub mod api;
#[cfg(feature = "dummy")]
mod dummy;
pub mod encrypt;
use std::fmt::Formatter;
use std::str::FromStr;
use derive_more::{Deref, Display, From};
#[cfg(feature = "dummy")]
use fake::Fake;
use rand::Rng;
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
pub use crate::encrypt::*;
#[derive(Debug, thiserror::Error)]
pub enum TransformError {
#[error("Given value is below minimal value")]
@ -151,7 +158,6 @@ impl Default for Audience {
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)]
@ -173,16 +179,12 @@ impl TryFrom<i32> for Quantity {
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "dummy", dummy("fake::faker::internet::en::Username"))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Deserialize, Serialize, Debug, Deref, From, Display)]
#[serde(transparent)]
pub struct Login(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "dummy", dummy("fake::faker::internet::en::FreeEmail"))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Debug, Deref, From, Display)]
@ -230,7 +232,6 @@ impl<'de> serde::Deserialize<'de> for Email {
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Default, Debug, Copy, Clone, Deref, Display)]
@ -421,13 +422,18 @@ where
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
#[serde(transparent)]
pub struct Password(String);
impl Password {
pub fn new<S: Into<String>>(pass: S) -> Self {
Self(pass.into())
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
@ -435,7 +441,6 @@ pub struct Password(String);
#[serde(transparent)]
pub struct PasswordConfirmation(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
@ -457,7 +462,7 @@ pub struct AccountId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct FullAccount {
pub id: AccountId,
pub email: Email,
@ -510,21 +515,18 @@ impl From<FullAccount> for Account {
#[serde(transparent)]
pub struct ProductId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
#[serde(transparent)]
pub struct ProductName(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
#[serde(transparent)]
pub struct ProductShortDesc(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)]
@ -538,9 +540,15 @@ pub struct ProductLongDesc(String);
#[serde(transparent)]
pub struct ProductCategory(String);
impl ProductCategory {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Product {
pub id: ProductId,
pub name: ProductName,
@ -732,10 +740,16 @@ pub struct LocalPath(String);
#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
pub struct FileName(String);
impl FileName {
pub fn new<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Deref, Display, From)]
pub struct PhotoId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]