Seed products with photos
82
Cargo.lock
generated
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
})
|
||||
}
|
||||
|
@ -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 = [] }
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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:?}");
|
||||
|
@ -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:?}");
|
||||
|
@ -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)) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/examples/images/pexels-alex-azabache-3907507.webp
Normal file
After Width: | Height: | Size: 523 KiB |
BIN
assets/examples/images/pexels-binoid-cbd-3612182.webp
Normal file
After Width: | Height: | Size: 331 KiB |
BIN
assets/examples/images/pexels-caio-1279107.webp
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/examples/images/pexels-eprism-studio-335257.webp
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
assets/examples/images/pexels-gabriel-freytez-341523.webp
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
assets/examples/images/pexels-jess-bailey-designs-913135.webp
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
assets/examples/images/pexels-luis-quintero-1738641.webp
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/examples/images/pexels-math-90946.webp
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
assets/examples/images/pexels-mike-380954.webp
Normal file
After Width: | Height: | Size: 373 KiB |
BIN
assets/examples/images/pexels-pixabay-279906.webp
Normal file
After Width: | Height: | Size: 163 KiB |
@ -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
@ -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(())
|
||||
}
|
@ -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
@ -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(())
|
||||
}
|
139
db-seed/src/product_photos.rs
Normal 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
@ -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
|
||||
}
|
@ -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
@ -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)
|
||||
}
|
||||
}
|
34
shared/model/src/encrypt.rs
Normal 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}");
|
||||
}
|
@ -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))]
|
||||
|