mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-11 12:23:20 +00:00
Merge pull request #137 from storytold/brandon-jake-password-reset
password reset
This commit is contained in:
43 files changed
+935
-32
No files matched your search
Generated
+160
-18
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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;
|
||||
+5
-6
@@ -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 ==========
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
}
|
||||
+70
@@ -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}")),
|
||||
}
|
||||
}
|
||||
+29
@@ -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(|_| ())
|
||||
}
|
||||
+55
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user