Merge pull request #137 from storytold/brandon-jake-password-reset

password reset
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-11-12 02:27:56 -05:00
commit a31a72abb7
43 files changed
+935 -32

No files matched your search

Generated
+160 -18
View File
@@ -690,6 +690,12 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "analytics-job"
version = "0.0.1"
@@ -1196,9 +1202,9 @@ checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
[[package]]
name = "byteorder"
version = "1.4.3"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
@@ -1314,6 +1320,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "chumsky"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
dependencies = [
"hashbrown 0.14.2",
"stacker",
]
[[package]]
name = "cidr-utils"
version = "0.5.11"
@@ -1389,7 +1405,7 @@ version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck 0.4.0",
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.26",
@@ -2339,7 +2355,7 @@ dependencies = [
"serde_json",
"sqlx",
"storyteller_root",
"strum",
"strum 0.24.1",
"tokens",
"tokio 1.34.0",
]
@@ -2358,6 +2374,30 @@ dependencies = [
"tokens",
]
[[package]]
name = "email-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
dependencies = [
"base64 0.21.0",
"memchr",
]
[[package]]
name = "email_address"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112"
[[package]]
name = "email_sender"
version = "0.1.0"
dependencies = [
"errors",
"lettre",
]
[[package]]
name = "encoding_rs"
version = "0.8.28"
@@ -2388,7 +2428,7 @@ dependencies = [
"serde_json",
"sqlx",
"sqlx-core",
"strum",
"strum 0.24.1",
"toml",
]
@@ -2477,6 +2517,12 @@ dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fdeflate"
version = "0.3.0"
@@ -2665,7 +2711,7 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [
"fastrand",
"fastrand 1.7.0",
"futures-core",
"futures-io",
"memchr",
@@ -2857,6 +2903,16 @@ dependencies = [
"ahash 0.7.6",
]
[[package]]
name = "hashbrown"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash 0.8.3",
"allocator-api2",
]
[[package]]
name = "hashing"
version = "0.0.1"
@@ -2874,7 +2930,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -2888,9 +2944,9 @@ dependencies = [
[[package]]
name = "heck"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
dependencies = [
"unicode-segmentation",
]
@@ -3169,7 +3225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -3450,6 +3506,31 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "lettre"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a466bc111374ccf4d90877dba636924a2185e67e5be4b35d32043199365097b2"
dependencies = [
"base64 0.21.0",
"chumsky",
"email-encoding",
"email_address",
"fastrand 2.0.1",
"futures-util",
"hostname",
"httpdate",
"idna 0.4.0",
"mime",
"native-tls",
"nom",
"once_cell",
"quoted_printable",
"socket2 0.5.5",
"tokio 1.34.0",
"url",
]
[[package]]
name = "lexical-sort"
version = "0.3.1"
@@ -4194,7 +4275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -4277,6 +4358,14 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "password"
version = "0.0.1"
dependencies = [
"bcrypt",
"errors",
]
[[package]]
name = "password-hash"
version = "0.4.2"
@@ -4531,6 +4620,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
dependencies = [
"cc",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.3"
@@ -4587,6 +4685,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
[[package]]
name = "r2d2"
version = "0.8.9"
@@ -5724,7 +5828,7 @@ checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9"
dependencies = [
"dotenvy",
"either",
"heck 0.4.0",
"heck 0.4.1",
"hex",
"once_cell",
"proc-macro2",
@@ -5764,6 +5868,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
dependencies = [
"cc",
"cfg-if 1.0.0",
"libc",
"psm",
"winapi 0.3.9",
]
[[package]]
name = "standback"
version = "0.2.17"
@@ -5863,6 +5980,7 @@ dependencies = [
"easyenv",
"elasticsearch",
"elasticsearch_schema",
"email_sender",
"enums",
"errors",
"futures 0.1.31",
@@ -5995,22 +6113,41 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
"strum_macros 0.24.3",
]
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.0",
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.107",
]
[[package]]
name = "strum_macros"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.26",
]
[[package]]
name = "subprocess"
version = "0.2.9"
@@ -6290,7 +6427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if 1.0.0",
"fastrand",
"fastrand 1.7.0",
"libc",
"redox_syscall 0.2.13",
"remove_dir_all",
@@ -6483,7 +6620,7 @@ dependencies = [
"serde_json",
"sqlx",
"sqlx-core",
"strum",
"strum 0.24.1",
"toml",
]
@@ -7180,11 +7317,13 @@ dependencies = [
"actix-web 4.0.1",
"anyhow",
"base64 0.21.0",
"bcrypt",
"byteorder",
"chrono",
"container_common",
"cookies",
"crockford",
"dotenv",
"email_sender",
"enums",
"errors",
"hmac 0.10.1",
@@ -7195,6 +7334,7 @@ dependencies = [
"md-5",
"mysql_queries",
"once_cell",
"password",
"r2d2_redis",
"rand 0.8.5",
"redis_caching",
@@ -7207,6 +7347,8 @@ dependencies = [
"sha2 0.9.5",
"sqlx",
"sqlx_mysql_helpers",
"strum 0.25.0",
"strum_macros 0.25.3",
"time 0.2.26",
"tokens",
"toml",
@@ -7477,7 +7619,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum",
"strum 0.24.1",
"toml",
]
+2
View File
@@ -35,6 +35,7 @@ members = [
"crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers
"crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve.
"crates/lib/elasticsearch_schema",
"crates/lib/email_sender",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/filesys",
@@ -46,6 +47,7 @@ members = [
"crates/lib/jobs_common",
"crates/lib/logging",
"crates/lib/mysql_queries",
"crates/lib/password",
"crates/lib/redis_common",
"crates/lib/server_environment",
"crates/lib/sqlx_mysql_helpers",
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
RENAME TABLE user_password_resets TO password_resets;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
RENAME TABLE password_resets TO user_password_resets;
@@ -0,0 +1,11 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- revert
ALTER TABLE user_password_resets
MODIFY COLUMN version INT NOT NULL DEFAULT 0;
-- revert
ALTER TABLE user_password_resets
MODIFY COLUMN current_password_version INT NOT NULL DEFAULT 0;
@@ -0,0 +1,9 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_password_resets
MODIFY COLUMN current_password_version INT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE user_password_resets
MODIFY COLUMN version INT UNSIGNED NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- revert
ALTER TABLE users
MODIFY COLUMN password_version INT NOT NULL DEFAULT 0;
-- revert
ALTER TABLE users
MODIFY COLUMN version INT NOT NULL DEFAULT 0;
@@ -0,0 +1,10 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
MODIFY COLUMN password_version INT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE users
MODIFY COLUMN version INT UNSIGNED NOT NULL DEFAULT 0;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_password_resets
RENAME COLUMN public_reset_token TO secret_key;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_password_resets
RENAME COLUMN secret_key TO public_reset_token;
@@ -0,0 +1,12 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_password_resets
DROP INDEX public_reset_token;
ALTER TABLE user_password_resets
ADD UNIQUE INDEX unique_user_token_and_secret_key (user_token, public_reset_token);
ALTER TABLE user_password_resets
ADD INDEX index_secret_key (public_reset_token);
@@ -0,0 +1,12 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_password_resets
DROP INDEX unique_user_token_and_secret_key;
ALTER TABLE user_password_resets
ADD UNIQUE INDEX public_reset_token (public_reset_token);
ALTER TABLE user_password_resets
DROP INDEX index_secret_key;
@@ -16,7 +16,7 @@
-- Also when creating the password hash, be sure to set `expires_at` to be "NOW() + 3 hours", or
-- some other small, reasonable time frame.
--
CREATE TABLE password_resets (
CREATE TABLE user_password_resets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
@@ -29,12 +29,12 @@ CREATE TABLE password_resets (
-- Secret "key" for the password reset
-- This will be shared via URL or user text input.
secret_key VARCHAR(32) NOT NULL,
public_reset_token VARCHAR(32) NOT NULL,
-- Copied from the user record at the time of password reset issuance.
-- If the user's password version is greater than this value, then this
-- reset is no longer valid.
current_password_version INT NOT NULL DEFAULT 0,
current_password_version INT UNSIGNED NOT NULL DEFAULT 0,
-- Whether the password reset has been consumed.
is_redeemed BOOLEAN NOT NULL DEFAULT false,
@@ -45,7 +45,7 @@ CREATE TABLE password_resets (
ip_address_redemption VARCHAR(40) DEFAULT NULL,
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
@@ -58,8 +58,7 @@ CREATE TABLE password_resets (
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (user_token, secret_key),
UNIQUE KEY (public_reset_token),
KEY fk_user_token (user_token),
KEY index_secret_key (secret_key),
@@ -41,7 +41,7 @@ CREATE TABLE users (
password_hash BINARY(60) NOT NULL,
-- Incremented with every update to the password.
password_version INT NOT NULL DEFAULT 0,
password_version INT UNSIGNED NOT NULL DEFAULT 0,
-- ========== ABUSE TRACKING ==========
@@ -145,7 +145,7 @@ CREATE TABLE users (
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
+3 -1
View File
@@ -30,6 +30,7 @@ pub(crate) enum TokenPrefix {
MediaFile,
MediaUpload,
NewsStory, // NB: aichatbot / sqlite
PasswordReset,
TtsRenderTask, // NB: aichatbot / sqlite
User,
UserSession,
@@ -92,6 +93,7 @@ impl PrefixGenerator for TokenPrefix {
Self::MediaFile => "m_",
Self::MediaUpload => "mu_",
Self::NewsStory => "news_story_",
Self::PasswordReset => "pw_reset_",
Self::TtsRenderTask => "tts_task_",
Self::User => "user_", // NB: Previously "U:"
Self::UserSession => "session_",
@@ -288,7 +290,7 @@ mod tests {
#[test]
fn test_all_prefixes_end_with_separator_length_one() {
for prefix in get_all_prefixes().iter().map(|s| *s) {
if prefix == "news_story_" || prefix == "tts_task_" || prefix == "INT_API:" {
if prefix == "news_story_" || prefix == "tts_task_" || prefix == "INT_API:" || prefix == "pw_reset_" {
// TODO/FIXME: I'm too tired at 5AM to replacen from the left. Make this test valid.
// These tokens are from the AIChatBot sidecar, so asserting their validity is less important.
continue;
+1
View File
@@ -9,6 +9,7 @@ pub mod generic_inference_jobs;
pub mod media_files;
pub mod media_uploads;
pub mod model_categories;
pub mod password_reset;
pub mod sqlite;
pub mod tts_inference_jobs;
pub mod tts_model_upload_jobs;
@@ -0,0 +1,14 @@
use std::fmt::Debug;
use serde::Deserialize;
use serde::Serialize;
use crate::prefixes::TokenPrefix;
/// The primary key for `password_reset`s
#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct PasswordResetToken(pub String);
impl_string_token!(PasswordResetToken);
impl_crockford_generator!(PasswordResetToken, 32usize, TokenPrefix::PasswordReset, CrockfordMixed);
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "email_sender"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
errors = { path = "../errors" }
lettre = "0.11.0"
+13
View File
@@ -0,0 +1,13 @@
//! email_sender
//!
//! Provide SMTP and other email sending capabilities to the rest of our services.
//!
/// Public re-exports
pub mod letter_exports {
pub use lettre::Message;
pub use lettre::message::header::ContentType;
pub use lettre::message::Mailbox;
}
pub mod smtp_email_sender;
@@ -0,0 +1,33 @@
use lettre::{Message, SmtpTransport, Transport};
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::response::Response;
use errors::AnyhowResult;
#[derive(Clone)]
pub struct SmtpEmailSender {
mailer: SmtpTransport,
}
impl SmtpEmailSender {
// NB: To set this up for Gmail:
//
// - https://support.google.com/a/answer/2956491?hl=en&fl=1&sjid=8333642123536524043-NA
//
// "Less secure apps" setting, which must be enabled at the organization level and within the
// individual user account:
// - https://support.google.com/mail/thread/5621336/bad-credentials-using-gmail-smtp?hl=en
//
pub fn new(relay: &str, username: String, password: String) -> AnyhowResult<Self> {
Ok(Self {
mailer: SmtpTransport::relay(relay)?
.credentials(Credentials::new(username, password))
.build()
})
}
pub fn send_message(&self, message: &Message) -> AnyhowResult<Response> {
Ok(self.mailer.send(message)?)
}
}
@@ -1,5 +1,6 @@
pub mod user;
pub mod user_badges;
pub mod user_password_resets;
pub mod user_profiles;
pub mod user_ratings;
pub mod user_roles;
@@ -14,6 +14,7 @@ SELECT
username,
email_address,
password_hash,
password_version,
is_banned
FROM users
WHERE email_address = ?
@@ -15,6 +15,7 @@ SELECT
username,
email_address,
password_hash,
password_version,
is_banned
FROM users
WHERE username = ?
@@ -7,5 +7,6 @@ pub struct UserRecordForLogin {
pub username: String,
pub email_address: String,
pub password_hash: Vec<u8>,
pub password_version: u32,
pub is_banned: i8,
}
@@ -0,0 +1,70 @@
use anyhow::anyhow;
use sqlx::{MySql, Transaction};
use errors::AnyhowResult;
use tokens::tokens::password_reset::PasswordResetToken;
use tokens::tokens::users::UserToken;
pub struct ChangePasswordFromPasswordResetArgs<'a, 'b> {
pub password_reset_token: &'a PasswordResetToken,
pub user_token: &'a UserToken,
pub new_password_hash: &'a str,
pub ip_address: &'a str,
pub mysql_transaction: Transaction<'b, MySql>,
}
pub async fn change_password_from_password_reset<'a, 'b>(
mut args: ChangePasswordFromPasswordResetArgs<'a, 'b>
) -> AnyhowResult<()> {
let query = sqlx::query!(
r#"
UPDATE users
SET
password_hash = ?,
ip_address_last_update = ?,
password_version = password_version + 1,
version = version + 1
WHERE
token = ?
LIMIT 1
"#,
args.new_password_hash,
args.ip_address,
args.user_token,
);
let query_result = query.execute(&mut args.mysql_transaction).await;
if let Err(err) = query_result {
let _r = args.mysql_transaction.rollback().await.ok();
return Err(anyhow!("Error updating user password from password reset request: {err}"));
}
let query = sqlx::query!(
r#"
UPDATE user_password_resets
SET
is_redeemed = true,
ip_address_redemption = ?,
version = version + 1
WHERE
token = ?
LIMIT 1
"#,
args.ip_address,
args.password_reset_token,
);
let query_result = query.execute(&mut args.mysql_transaction).await;
if let Err(err) = query_result {
let _r = args.mysql_transaction.rollback().await.ok();
return Err(anyhow!("Error updating user password from password reset request: {err}"));
}
match args.mysql_transaction.commit().await {
Ok(_) => Ok(()),
Err(err) => Err(anyhow!("Error committing password reset changes: {err}")),
}
}
@@ -0,0 +1,29 @@
use anyhow::Context;
use sqlx::MySqlPool;
use errors::AnyhowResult;
use tokens::tokens::password_reset::PasswordResetToken;
use crate::queries::users::user::lookup_user_for_login_result::UserRecordForLogin;
pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, ip_address: String, secret_key: String) -> AnyhowResult<()> {
let token = PasswordResetToken::generate();
sqlx::query!(
r#"
INSERT INTO user_password_resets
(token, user_token, public_reset_token, current_password_version, ip_address_creation, expires_at)
VALUES (?, ?, ?, ?, ?, NOW() + INTERVAL 3 hour);
"#,
token,
user.token,
secret_key,
user.password_version,
ip_address
)
.execute(pool)
.await
.context("inserting password reset")
.map(|_| ())
}
@@ -0,0 +1,55 @@
use anyhow::anyhow;
use sqlx::{Error, MySql, MySqlPool, Transaction};
use errors::AnyhowResult;
use tokens::tokens::password_reset::PasswordResetToken;
use tokens::tokens::users::UserToken;
pub struct PasswordResetStateAndTransaction <'a> {
pub transaction: Transaction<'a, MySql>,
pub reset_state: PasswordResetLookupInfo,
}
pub struct PasswordResetLookupInfo {
pub password_reset_token: PasswordResetToken,
pub user_token: UserToken,
}
/// Begin a lookup transaction.
/// `select for update` ensures the record won't change, blocking race conditions.
pub async fn lookup_password_reset_request<'a>(password_reset_token: &str, pool: &'a MySqlPool) -> AnyhowResult<Option<PasswordResetStateAndTransaction<'a>>> {
let mut transaction = pool.begin().await?;
let result = sqlx::query_as!(
PasswordResetLookupInfo,
r#"
SELECT
pw.token as `password_reset_token: tokens::tokens::password_reset::PasswordResetToken`,
pw.user_token as `user_token: tokens::tokens::users::UserToken`
FROM user_password_resets AS pw
JOIN users AS u
ON pw.user_token = u.token
WHERE public_reset_token = ?
AND pw.expires_at > NOW()
AND pw.current_password_version = u.password_version
AND NOT pw.is_redeemed
AND NOT u.is_banned
LIMIT 1
FOR UPDATE
"#,
password_reset_token,
)
.fetch_one(&mut transaction)
.await;
match result {
Ok(record) => Ok(Some(PasswordResetStateAndTransaction {
transaction,
reset_state: record,
})),
Err(err) => match err {
Error::RowNotFound => Ok(None),
_ => Err(anyhow!(err)),
},
}
}
@@ -0,0 +1,3 @@
pub mod change_password_from_password_reset;
pub mod create_password_reset_request;
pub mod lookup_password_reset_request;
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "password"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[lib]
# NB: Public "password" crate is squatted for over eight years
name = "password"
path = "src/lib.rs"
[dependencies]
# Internal
errors = { path = "../errors" }
# External
bcrypt = "0.8" # password hashes
#hmac = "0.10.0" # jwt signing
#jwt = "0.13.0"
#sha2 = "0.9.5" # hmac signing
[dev-dependencies]
# None yet
@@ -0,0 +1,28 @@
use errors::AnyhowResult;
pub fn bcrypt_confirm_password(password: String, bcrypt_hash: &str) -> AnyhowResult<bool> {
let verified = bcrypt::verify(&password, &bcrypt_hash)?;
Ok(verified)
}
#[cfg(test)]
mod tests {
use crate::bcrypt_confirm_password::bcrypt_confirm_password;
#[test]
fn test_confirmation() {
// NB: This is to make sure we don't migrate the algorithm to a state where it won't work anymore.
// NB: Be mindful that these are expensive to compute and don't add too many test cases.
let hash_to_passwords = [
("$2b$12$ieU6.dygzbiZjtED5Qz8Vu8fSjDjE9R4AykzpGI5hL666AjV753Iu", "password"),
("$2b$12$nGHAaxDCXj0oC/C5vbnhAOQhZHprKmJ8eTnX.uVKXvQNFildSAIAa", "hunter2"),
("$2b$12$USIc/tDDOobgW96sBdw13u2Ts9bmddMHDrrixBdwASxur2h4oy8iC", "hunter2"), // hunter2 again
("$2b$12$9418BXjnbbJwfu1YFe4r3OOsePo6kflnqypfOFnA1QsID06mqn3X.", "&*KA@jh2l9*2!!2"),
];
for (hash, password) in hash_to_passwords {
let valid = bcrypt_confirm_password(password.to_string(), hash).unwrap();
assert!(valid);
}
}
}
@@ -0,0 +1,19 @@
use errors::AnyhowResult;
pub fn bcrypt_hash_password(password: String) -> AnyhowResult<String> {
let hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST)?;
Ok(hash)
}
#[cfg(test)]
mod tests {
use crate::bcrypt_hash_password::bcrypt_hash_password;
#[test]
fn test_password_hash_round_trip() {
let password = "password";
let hash = bcrypt_hash_password(password.to_string()).unwrap();
let valid = bcrypt::verify(password, &hash).unwrap();
assert!(valid);
}
}
+24
View File
@@ -0,0 +1,24 @@
//! password
//!
//! Password-related functions
//!
// Never allow these
#![forbid(private_in_public)]
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
// Okay to toggle
#![forbid(unreachable_patterns)]
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
// Always allow
#![allow(dead_code)]
#![allow(non_snake_case)]
// TODO(bt,2023-11-11): Start using a more modern / safer password hashing algorithm such as Argon2 or scrypt
// https://medium.com/@mpreziuso/password-hashing-pbkdf2-scrypt-bcrypt-and-argon2-e25aaf41598e
// https://stytch.com/blog/argon2-vs-bcrypt-vs-scrypt/
pub mod bcrypt_hash_password;
pub mod bcrypt_confirm_password;
@@ -183,7 +183,7 @@ pub async fn process_rvc_v2_model<'a, 'b>(
//#[cfg(test)]
//mod tests {
// use crockford::crockford_entropy_lower;
// use crockford_deprecated::crockford_entropy_lower;
//
// #[test]
// fn temp() {
+6 -1
View File
@@ -17,10 +17,13 @@ path = "src/lib.rs"
# Internal
container_common = { path = "../../../lib/deprecated/container_common" }
cookies = { path = "../../../lib/cookies" }
crockford = { path = "../../../lib/crockford" }
email_sender = { path = "../../../lib/email_sender" }
enums = { path = "../../../_types/enums" }
errors = { path = "../../../lib/errors" }
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
mysql_queries = { path = "../../../lib/mysql_queries" }
password = { path = "../../../lib/password" }
redis_caching = { path = "../../../lib/caching/redis_caching" }
redis_common = { path = "../../../lib/redis_common" }
reusable_types = { path = "../../../_types/reusable_types" }
@@ -45,7 +48,6 @@ actix-web = "4.0.0-beta.10"
jwt = "0.13.0"
# Crypto
bcrypt = "0.8" # password hashes
hmac = "0.10.0" # cookies
md-5 = "0.9.1" # gravatar
sha2 = "0.9.5" # file hashes, inference text hashes, etc.
@@ -65,3 +67,6 @@ sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "o
# TODO: Shouldn't really be a dependency, but need to add caching for the DDoS attack.
# Redis Database
r2d2_redis = "0.14.0"
byteorder = "1.5.0"
strum = "0.25.0"
strum_macros = "0.25.3"
@@ -7,6 +7,8 @@ use actix_web::error::Error;
use crate::endpoints::create_account_handler::create_account_handler;
use crate::endpoints::login_handler::login_handler;
use crate::endpoints::logout_handler::logout_handler;
use crate::endpoints::password_reset_redeem_handler::password_reset_redeem_handler;
use crate::endpoints::password_reset_request_handler::password_reset_request_handler;
use crate::endpoints::session_info_handler::session_info_handler;
// NB: This does not include the user edit endpoints since FakeYou's API mounts them alongside other things.
@@ -25,6 +27,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes<T, B> (app: App<
{
app
.service(
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
web::resource("/create_account")
.route(web::post().to(create_account_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -35,6 +38,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes<T, B> (app: App<
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
web::resource("/login")
.route(web::post().to(login_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -45,6 +49,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes<T, B> (app: App<
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
web::resource("/logout")
.route(web::post().to(logout_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -55,6 +60,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes<T, B> (app: App<
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
web::resource("/session")
.route(web::get().to(session_info_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -64,4 +70,14 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes<T, B> (app: App<
.route(web::get().to(session_info_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
web::resource("/v1/password_reset/request")
.route(web::post().to(password_reset_request_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
web::resource("/v1/password_reset/redeem")
.route(web::post().to(password_reset_redeem_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
}
@@ -18,6 +18,7 @@ use http_server_common::response::serialize_as_json_error::serialize_as_json_err
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError};
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use password::bcrypt_hash_password::bcrypt_hash_password;
use user_input_common::check_for_slurs::contains_slurs;
use crate::cookies::session::session_cookie_manager::SessionCookieManager;
@@ -132,7 +133,7 @@ pub async fn create_account_handler(
});
}
let password_hash = match bcrypt::hash(&request.password, bcrypt::DEFAULT_COST) {
let password_hash = match bcrypt_hash_password(request.password.clone()) {
Ok(hash) => hash,
Err(err) => {
warn!("Bcrypt error: {:?}", err);
@@ -18,6 +18,7 @@ use mysql_queries::helpers::boolean_converters::i8_to_bool;
use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email;
use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username;
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use password::bcrypt_confirm_password::bcrypt_confirm_password;
use crate::cookies::session::session_cookie_manager::SessionCookieManager;
@@ -123,7 +124,7 @@ pub async fn login_handler(
}
};
match bcrypt::verify(&request.password, &actual_hash) {
match bcrypt_confirm_password(request.password.clone(), &actual_hash) {
Err(e) => {
warn!("Login hash comparison error: {:?}", e);
return Err(LoginErrorResponse::server_error());
@@ -135,7 +136,7 @@ pub async fn login_handler(
}
// Good to go...!
},
};
}
let ip_address = get_request_ip(&http_request);
@@ -3,4 +3,6 @@ pub mod edit_profile_handler;
pub mod get_profile_handler;
pub mod login_handler;
pub mod logout_handler;
pub mod password_reset_redeem_handler;
pub mod password_reset_request_handler;
pub mod session_info_handler;
@@ -0,0 +1,150 @@
use std::fmt::Display;
use actix_web::{HttpRequest, HttpResponse, ResponseError, web};
use actix_web::http::StatusCode;
use log::{error, warn};
use serde::Deserialize;
use sqlx::MySqlPool;
use strum_macros::Display;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::users::user_password_resets::change_password_from_password_reset::{change_password_from_password_reset, ChangePasswordFromPasswordResetArgs};
use mysql_queries::queries::users::user_password_resets::lookup_password_reset_request::lookup_password_reset_request;
use password::bcrypt_hash_password::bcrypt_hash_password;
#[derive(Deserialize)]
pub struct PasswordResetRedemptionRequest {
username_or_email: String,
reset_token: String,
new_password: String,
new_password_validation: String,
}
#[derive(Serialize)]
pub struct PasswordResetRedemptionResponse {
success: bool,
}
#[derive(Serialize, Debug, Display)]
pub enum PasswordResetRedemptionError {
/// Account does not exist or reset token is wrong.
InvalidRedemption,
PasswordsDoNotMatch,
Internal,
}
#[derive(Serialize, Debug)]
pub struct PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError,
}
impl Display for PasswordResetRedemptionErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<PasswordResetRedemptionError> for PasswordResetRedemptionErrorResponse {
fn from(value: PasswordResetRedemptionError) -> Self {
Self { kind: value }
}
}
impl From<errors::AnyhowError> for PasswordResetRedemptionErrorResponse {
fn from(value: errors::AnyhowError) -> Self {
log::error!("Internal error: {value}");
Self { kind: PasswordResetRedemptionError::Internal }
}
}
impl ResponseError for PasswordResetRedemptionErrorResponse {
//TODO: Yknow, clean this up and stuff
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
pub async fn password_reset_redeem_handler(
http_request: HttpRequest,
request: web::Json<PasswordResetRedemptionRequest>,
mysql_pool: web::Data<MySqlPool>,
) -> Result<HttpResponse, PasswordResetRedemptionErrorResponse> {
let new_password = request.new_password.trim();
if new_password != request.new_password_validation.trim() {
return Err(PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError::PasswordsDoNotMatch,
});
}
//TODO: Handle banned users, they shouldn't be able to do this
let result = lookup_password_reset_request(&request.reset_token, &mysql_pool).await
.map_err(|err| {
log::error!("Password reset error {:?}", err);
err
});
let transaction_and_state = match result {
Ok(Some(reset_state)) => reset_state,
Ok(None) => {
warn!("No such reset request.");
return Err(PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError::InvalidRedemption,
});
}
Err(err) => {
error!("lookup error: {err}");
return Err(PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError::InvalidRedemption,
});
}
};
println!("Reset state: {}", transaction_and_state.reset_state.user_token);
let ip_address = get_request_ip(&http_request);
let password_hash = match bcrypt_hash_password(new_password.to_string()) {
Ok(hash) => hash,
Err(err) => {
error!("password hash error: {err}");
return Err(PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError::Internal,
});
}
};
let result = change_password_from_password_reset(ChangePasswordFromPasswordResetArgs {
password_reset_token: &transaction_and_state.reset_state.password_reset_token,
user_token: &transaction_and_state.reset_state.user_token,
new_password_hash: &password_hash,
ip_address: &ip_address,
mysql_transaction: transaction_and_state.transaction,
}).await;
if let Err(err) = result {
error!("password reset error: {err}");
return Err(PasswordResetRedemptionErrorResponse {
kind: PasswordResetRedemptionError::Internal,
});
}
let response = PasswordResetRedemptionResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
PasswordResetRedemptionError::Internal
})?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -0,0 +1,149 @@
use std::fmt::Display;
use actix_web::{HttpRequest, HttpResponse, ResponseError, web};
use actix_web::http::StatusCode;
use log::{error, warn};
use serde::Deserialize;
use sqlx::MySqlPool;
use strum_macros::Display;
use crockford::crockford_entropy_lower;
use email_sender::letter_exports;
use email_sender::letter_exports::Message;
use email_sender::smtp_email_sender::SmtpEmailSender;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email;
use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username;
use mysql_queries::queries::users::user_password_resets::create_password_reset_request::create_password_reset;
#[derive(Deserialize)]
pub struct PasswordResetRequestedRequest {
username_or_email: String,
}
#[derive(Serialize)]
pub struct PasswordResetRequestedResponse {
success: bool,
}
#[derive(Serialize, Debug, Display)]
pub enum PasswordResetRequestedRequestError {
NoSuchUser,
Internal,
}
#[derive(Serialize, Debug)]
pub struct PasswordResetRequestedErrorResponse {
kind: PasswordResetRequestedRequestError,
}
impl Display for PasswordResetRequestedErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<PasswordResetRequestedRequestError> for PasswordResetRequestedErrorResponse {
fn from(value: PasswordResetRequestedRequestError) -> Self {
Self { kind: value }
}
}
impl From<errors::AnyhowError> for PasswordResetRequestedErrorResponse {
fn from(value: errors::AnyhowError) -> Self {
log::error!("Internal error: {value}");
Self { kind: PasswordResetRequestedRequestError::Internal }
}
}
impl ResponseError for PasswordResetRequestedErrorResponse {
//TODO: Yknow, clean this up and stuff
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
///
/// Non-authenticated!
///
/// - create password reset request
/// - sends email?
/// - inserts record and stuff
pub async fn password_reset_request_handler(
http_request: HttpRequest,
request: web::Json<PasswordResetRequestedRequest>,
mysql_pool: web::Data<MySqlPool>,
sender: web::Data<SmtpEmailSender>,
) -> Result<HttpResponse, PasswordResetRequestedErrorResponse> {
let username_or_email = request.username_or_email.trim();
let user = if username_or_email.contains("@") {
lookup_user_for_login_by_email(&username_or_email, &mysql_pool).await
} else {
lookup_user_for_login_by_username(&username_or_email, &mysql_pool).await
}.map_err(|e| {
warn!("Password reset user lookup error: {:?}", e);
//TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻
PasswordResetRequestedRequestError::NoSuchUser
})?;
let secret_key = crockford_entropy_lower(32);
//TODO: Handle banned users, they shouldn't be able to do this
let ip_address = get_request_ip(&http_request);
create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await
.map_err(|err| {
log::error!("Error creating password reset: {err}");
PasswordResetRequestedRequestError::Internal
})?;
let from_address = "Support <support@storyteller.ai>"
.parse()
.map_err(|err| {
log::error!("Error parsing from address: {err}");
PasswordResetRequestedRequestError::Internal
})?;
let to_address = user.email_address
.parse()
.map_err(|err| {
log::error!("Error parsing to address: {err}");
PasswordResetRequestedRequestError::Internal
})?;
let email = Message::builder()
.from(from_address)
.to(to_address)
.subject("FakeYou Password Reset")
.header(letter_exports::ContentType::TEXT_PLAIN)
.body(format!("Here's the secret key: {secret_key}"))
.map_err(|err| {
log::error!("Error constructing email: {err}");
PasswordResetRequestedRequestError::Internal
})?;
sender.send_message(&email).map_err(|err| {
log::error!("Error sending email: {err}");
PasswordResetRequestedRequestError::Internal
})?;
let response = PasswordResetRequestedResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
PasswordResetRequestedRequestError::Internal
})?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -26,6 +26,7 @@ cookies = { path = "../../../lib/cookies" }
datetimes = { path = "../../../lib/datetimes" }
easyenv = { path = "../../../lib/easyenv" }
elasticsearch_schema = { path = "../../../lib/elasticsearch_schema" }
email_sender= { path = "../../../lib/email_sender" }
enums = { path = "../../../_types/enums" }
errors = { path = "../../../lib/errors" }
google_drive_common = { path = "../../../lib/api_clients/google_drive_common" }
@@ -57,6 +57,7 @@ use config::common_env::CommonEnv;
use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING;
use config::shared_constants::DEFAULT_RUST_LOG;
use container_common::files::read_toml_file_to_struct::read_toml_file_to_struct;
use email_sender::smtp_email_sender::SmtpEmailSender;
use errors::AnyhowResult;
use http_server_common::cors::{build_cors_config, build_production_cors_config};
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
@@ -434,6 +435,12 @@ async fn main() -> AnyhowResult<()> {
.trim()
.to_string();
// TODO(bt,2023-11-11): Password and account details should be a secret, but gotta go fast.
let email_sender = SmtpEmailSender::new(
"smtp.gmail.com",
"noreply@storyteller.ai".to_string(),
"FakeYouHanashi1".to_string())?;
let server_state = ServerState {
env_config: EnvConfig {
num_workers,
@@ -475,6 +482,7 @@ async fn main() -> AnyhowResult<()> {
audio_uploads_bucket_root,
sort_key_crypto,
static_api_token_set,
email_sender,
caches: InMemoryCaches {
durable: DurableInMemoryCaches {
model_token_info: model_token_info_cache,
@@ -651,6 +659,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
.app_data(web::Data::new(server_state_arc.stripe.clone().config.clone()))
.app_data(web::Data::new(server_state_arc.stripe.clone().client.clone()))
.app_data(web::Data::new(server_state_arc.third_party_url_redirector.clone()))
.app_data(web::Data::new(server_state_arc.email_sender.clone()))
.app_data(web::Data::new(server_environment))
.app_data(web::Data::from(product_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
.app_data(web::Data::from(stripe_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
@@ -6,6 +6,7 @@ use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSe
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
use billing_component::stripe::stripe_config::StripeConfig;
use cloud_storage::bucket_client::BucketClient;
use email_sender::smtp_email_sender::SmtpEmailSender;
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
use mysql_queries::mediators::badge_granter::BadgeGranter;
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
@@ -78,6 +79,8 @@ pub struct ServerState {
pub sort_key_crypto: SortKeyCrypto,
pub email_sender: SmtpEmailSender,
pub ip_ban_list: IpBanList,
pub cidr_ban_set: BannedCidrSet,