Database seed tool

This commit is contained in:
Brandon Thomas committed 2023-10-06 00:09:42 -04:00
1 parent d07a147db9
commit aac31f77ad
14 files changed
+266

No files matched your search

Generated
+18
View File
@@ -1798,6 +1798,22 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "dev-database-seed"
version = "0.0.1"
dependencies = [
"config",
"easyenv",
"enums",
"errors",
"hashing",
"log",
"mysql_queries",
"sqlx",
"tokens",
"tokio 1.25.0",
]
[[package]]
name = "diesel"
version = "1.4.6"
@@ -2538,8 +2554,10 @@ dependencies = [
name = "hashing"
version = "0.0.1"
dependencies = [
"bcrypt",
"data-encoding",
"errors",
"md-5",
"ring 0.17.0-alpha.11",
]
+1
View File
@@ -11,6 +11,7 @@ members = [
# CLI tools for operations, etc.
"crates/cli/database_backup",
"crates/cli/database_cleanup",
"crates/cli/dev_database_seed",
"crates/cli/scrape_test",
# Shared libraries
+39
View File
@@ -0,0 +1,39 @@
[package]
name = "dev-database-seed"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[[bin]]
name = "dev-database-seed"
path = "src/main.rs"
[dependencies]
# Internal
config = { path = "../../lib/deprecated/config" }
easyenv = { path = "../../lib/easyenv" }
enums = { path = "../../_types/enums" }
errors = { path = "../../lib/errors" }
hashing = { path = "../../lib/files/hashing" }
mysql_queries = { path = "../../lib/mysql_queries" }
tokens = { path = "../../_types/tokens" }
# External
log = "0.4.14"
# Runtime
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
tokio = { version = "1.6.0", features = ["macros"] }
# MySql Database
# "offline" supports typesafe builds without a live database
# "rustls" doesn't appear to work with IP addresses, per:
# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828
sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] }
[dev-dependencies]
# None yet
+33
View File
@@ -0,0 +1,33 @@
use log::{info, log};
use sqlx::mysql::MySqlPoolOptions;
use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG};
use errors::AnyhowResult;
use crate::seeding::users::seed_user_accounts;
use crate::seeding::zero_shot_tts::seed_zero_shot_tts;
pub mod seeding;
#[tokio::main]
pub async fn main() -> AnyhowResult<()> {
easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG));
info!("Database seed CLI script.");
let db_connection_string =
easyenv::get_env_string_or_default(
"MYSQL_URL",
DEFAULT_MYSQL_CONNECTION_STRING);
let pool = MySqlPoolOptions::new()
.max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 3)?)
.connect(&db_connection_string)
.await?;
seed_user_accounts(&pool).await?;
seed_zero_shot_tts(&pool).await?;
info!("Done!");
Ok(())
}
@@ -0,0 +1,2 @@
pub mod users;
pub mod zero_shot_tts;
@@ -0,0 +1,52 @@
use log::{info, warn};
use sqlx::{MySql, Pool};
use errors::{anyhow, AnyhowResult};
use hashing::bcrypt::bcrypt_password_hash::bcrypt_password_hash;
use hashing::md5::email_to_gravatar_hash::email_to_gravatar_hash;
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs};
pub async fn seed_user_accounts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
info!("Seeding user accounts...");
let users = [
("admin", "admin@storyteller.ai", "password"),
("hanashi", "admin@storyteller.ai", "password"),
("test", "admin@storyteller.ai", "password"),
];
for (username, email, password) in users {
let result = seed_user(username, email, password, &mysql_pool).await;
match result {
Ok(_) => info!("Seeded {}", username),
Err(err) => warn!("Could not seed user {} : {:?}", username, err),
}
}
Ok(())
}
async fn seed_user(
username: &str,
email_address: &str,
password: &str,
mysql_pool: &Pool<MySql>,
) -> AnyhowResult<()> {
info!("Seeding user {} ...", username);
let display_name = username.clone();
let username = username.to_lowercase();
let email_gravatar_hash = email_to_gravatar_hash(&email_address);
let password_hash = bcrypt_password_hash(password)?;
create_account(mysql_pool, CreateAccountArgs {
username: &username,
display_name,
email_address,
email_gravatar_hash: &email_gravatar_hash,
password_hash: &password_hash,
ip_address: "127.0.0.1",
}).await.map_err(|err| anyhow!("err: {:?}", err))?;
Ok(())
}
@@ -0,0 +1,52 @@
use log::{info, warn};
use sqlx::{MySql, Pool};
use errors::{anyhow, AnyhowResult};
use hashing::bcrypt::bcrypt_password_hash::bcrypt_password_hash;
use hashing::md5::email_to_gravatar_hash::email_to_gravatar_hash;
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs};
pub async fn seed_zero_shot_tts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
info!("Seeding zero shot TTS...");
let users = [
("admin", "admin@storyteller.ai", "password"),
("hanashi", "admin@storyteller.ai", "password"),
("test", "admin@storyteller.ai", "password"),
];
for (username, email, password) in users {
let result = seed_user(username, email, password, &mysql_pool).await;
match result {
Ok(_) => info!("Seeded {}", username),
Err(err) => warn!("Could not seed user {} : {:?}", username, err),
}
}
Ok(())
}
async fn seed_user(
username: &str,
email_address: &str,
password: &str,
mysql_pool: &Pool<MySql>,
) -> AnyhowResult<()> {
info!("Seeding user {} ...", username);
let display_name = username.clone();
let username = username.to_lowercase();
let email_gravatar_hash = email_to_gravatar_hash(&email_address);
let password_hash = bcrypt_password_hash(password)?;
create_account(mysql_pool, CreateAccountArgs {
username: &username,
display_name,
email_address,
email_gravatar_hash: &email_gravatar_hash,
password_hash: &password_hash,
ip_address: "127.0.0.1",
}).await.map_err(|err| anyhow!("err: {:?}", err))?;
Ok(())
}
+2
View File
@@ -18,7 +18,9 @@ path = "src/lib.rs"
errors = { path = "../../errors" }
# External
bcrypt = "0.8" # password hashes
data-encoding = "2.3.2"
md-5 = "0.9.1" # DO NOT USE FOR SERIOUS CRYPTO (we use this for gravatar)
ring = "0.17.0-alpha.10"
[dev-dependencies]
@@ -0,0 +1,25 @@
use errors::AnyhowResult;
/// Create a bcrypt password hash.
/// TODO(bt,2023-10-05): Investigate stronger hashing (Argon2??)
/// TODO(bt,2023-10-05): Return something better than an AnyhowResult<()> !
pub fn bcrypt_password_hash(password: &str) -> AnyhowResult<String> {
let password_hash = bcrypt_lib::hash(password, bcrypt_lib::DEFAULT_COST)?;
Ok(password_hash)
}
#[cfg(test)]
mod tests {
use crate::bcrypt::bcrypt_password_hash::bcrypt_password_hash;
#[test]
fn test_bcrypt_password_hashing() {
let password = "password";
for _ in 0..3 {
// NB(1): Each bcrypt hash has an os-random salt
// NB(2): Testing these takes time (as bcrypt is meant to), so we only test 3.
let password_hash = bcrypt_password_hash(&password).unwrap();
assert!(bcrypt_lib::verify(password, &password_hash).unwrap());
}
}
}
@@ -0,0 +1 @@
pub mod bcrypt_password_hash;
+14
View File
@@ -18,4 +18,18 @@
#![allow(dead_code)]
#![allow(non_snake_case)]
// TODO(bt,2023-10-05): Should this library contain any "user libraries"
// (like password hashing and gravatar), or focus on purely hashing functions?
// NB: Because we have our own bcrypt module
extern crate bcrypt as bcrypt_lib;
pub mod bcrypt;
// NB(bt,2023-10-05): md5 is broken and should not be used for anything seriously.
// We're using it in this case to support email gravatar hashing, which honestly,
// should be carefully reconsidered since all of these hashes are broken and exposed
// to the public.
pub mod md5;
pub mod sha256;
@@ -0,0 +1,25 @@
use md5::{Digest, Md5};
/// See https://gravatar.com/
pub fn email_to_gravatar_hash(email_address: &str) -> String {
let email = email_address.trim().to_lowercase();
let mut hasher = Md5::new();
hasher.update(email);
let hash = hasher.finalize();
let gravatar_hash = format!("{:x}", hash);
gravatar_hash
}
#[cfg(test)]
mod tests {
use crate::md5::email_to_gravatar_hash::email_to_gravatar_hash;
#[test]
fn test_email_hashing() {
assert_eq!(email_to_gravatar_hash("email@email.com"), "4f64c9f81bb0d4ee969aaf7b4a5a6f40".to_string());
assert_eq!(email_to_gravatar_hash("foo@bar.com"), "f3ada405ce890b6f8204094deb12d8a8".to_string());
assert_eq!(email_to_gravatar_hash("brandon@storyteller.ai"), "54bd9d9491cd63f733f0970ae4059438".to_string());
}
}
+1
View File
@@ -0,0 +1 @@
pub mod email_to_gravatar_hash;
@@ -23,6 +23,7 @@ pub struct CreateAccountSuccessResult {
pub user_id: u64,
}
#[derive(Debug, Copy, Clone)]
pub enum CreateAccountError {
EmailIsTaken,
UsernameIsTaken,