mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-09 19:41:32 +00:00
Beginning password reset implementation
- Email send is not implemented yet - All of this is still untested, clearly
This commit is contained in:
1 parent
1cc3c5eee4
commit
16d6542911
15 files changed
+206
-6
No files matched your search
Generated
+3
-3
@@ -1178,7 +1178,7 @@ dependencies = [
|
||||
name = "buckets"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"crockford_deprecated",
|
||||
"crockford",
|
||||
"errors",
|
||||
]
|
||||
|
||||
@@ -1654,7 +1654,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crockford_deprecated"
|
||||
name = "crockford"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
@@ -2187,7 +2187,7 @@ dependencies = [
|
||||
"concurrency",
|
||||
"config",
|
||||
"container_common",
|
||||
"crockford_deprecated",
|
||||
"crockford",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"enums",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ members = [
|
||||
"crates/lib/caching/redis_caching",
|
||||
"crates/lib/concurrency",
|
||||
"crates/lib/cookies",
|
||||
"crates/lib/crockford_deprecated",
|
||||
"crates/lib/crockford",
|
||||
"crates/lib/datetimes",
|
||||
"crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose
|
||||
"crates/lib/deprecated/container_common", # TODO(bt, 2023-02-13): Pull apart and rehome pieces
|
||||
|
||||
@@ -30,6 +30,8 @@ pub(crate) enum TokenPrefix {
|
||||
MediaFile,
|
||||
MediaUpload,
|
||||
NewsStory, // NB: aichatbot / sqlite
|
||||
PasswordReset,
|
||||
TtsModel,
|
||||
TtsRenderTask, // NB: aichatbot / sqlite
|
||||
User,
|
||||
UserSession,
|
||||
@@ -92,6 +94,8 @@ impl PrefixGenerator for TokenPrefix {
|
||||
Self::MediaFile => "m_",
|
||||
Self::MediaUpload => "mu_",
|
||||
Self::NewsStory => "news_story_",
|
||||
Self::PasswordReset => "pw_reset_",
|
||||
Self::TtsModel => "TM:", // NB: Old-style prefix, do not use for future tokens.
|
||||
Self::TtsRenderTask => "tts_task_",
|
||||
Self::User => "user_", // NB: Previously "U:"
|
||||
Self::UserSession => "session_",
|
||||
|
||||
@@ -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::EntityType;
|
||||
|
||||
/// 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, EntityType::PasswordReset, CrockfordUpper);
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "email"
|
||||
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" }
|
||||
mail-smtp = "0.3.0"
|
||||
@@ -0,0 +1,27 @@
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> {
|
||||
todo!();
|
||||
// use futures::Future;
|
||||
// use mail_headers::{
|
||||
// headers::*,
|
||||
// header_components::Domain
|
||||
// };
|
||||
// use mail_core::{Mail, default_impl::simple_context};
|
||||
// use mail_smtp::{self as smtp, ConnectionConfig};
|
||||
|
||||
// let ctx = simple_context::new(Domain::from_unchecked("example.com".to_owned()), "asdkds".parse().unwrap())
|
||||
// .unwrap();
|
||||
|
||||
// let mut mail = Mail::plain_text("Some body", &ctx);
|
||||
// mail.insert_headers(headers! {
|
||||
// _From: ["bla@example.com"],
|
||||
// _To: ["blub@example.com"],
|
||||
// Subject: "Some Mail"
|
||||
// }.unwrap());
|
||||
|
||||
// let con_config = ConnectionConfig::builder_local_unencrypted().build();
|
||||
|
||||
// let fut = smtp::send(mail.into(), con_config, ctx);
|
||||
// let results = fut.wait();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use anyhow::Context;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::password_reset::PasswordResetToken;
|
||||
|
||||
use super::lookup_user_for_login_result::UserRecordForLogin;
|
||||
|
||||
// todo: ip_address_redemption nullable
|
||||
|
||||
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 password_resets
|
||||
(token, user_token, secret_key, 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(|_| ())
|
||||
}
|
||||
@@ -14,7 +14,8 @@ SELECT
|
||||
username,
|
||||
email_address,
|
||||
password_hash,
|
||||
is_banned
|
||||
is_banned,
|
||||
password_version
|
||||
FROM users
|
||||
WHERE email_address = ?
|
||||
LIMIT 1
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ SELECT
|
||||
username,
|
||||
email_address,
|
||||
password_hash,
|
||||
is_banned
|
||||
is_banned,
|
||||
password_version
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
LIMIT 1
|
||||
|
||||
@@ -8,4 +8,5 @@ pub struct UserRecordForLogin {
|
||||
pub email_address: String,
|
||||
pub password_hash: Vec<u8>,
|
||||
pub is_banned: i8,
|
||||
pub password_version: i32,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod create_account;
|
||||
pub mod create_password_reset_request;
|
||||
pub mod get_user_token_by_username;
|
||||
pub mod list_users_query_builder;
|
||||
pub mod lookup_user_for_login_by_email;
|
||||
|
||||
@@ -17,6 +17,7 @@ path = "src/lib.rs"
|
||||
# Internal
|
||||
container_common = { path = "../../../lib/deprecated/container_common" }
|
||||
cookies = { path = "../../../lib/cookies" }
|
||||
email = { path = "../../../lib/email" }
|
||||
enums = { path = "../../../_types/enums" }
|
||||
errors = { path = "../../../lib/errors" }
|
||||
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
@@ -65,3 +66,5 @@ 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"
|
||||
crockford = "1.1.2"
|
||||
byteorder = "1.5.0"
|
||||
@@ -3,4 +3,5 @@ pub mod edit_profile_handler;
|
||||
pub mod get_profile_handler;
|
||||
pub mod login_handler;
|
||||
pub mod logout_handler;
|
||||
pub mod password_reset_handler;
|
||||
pub mod session_info_handler;
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
~ B R A I N S T O R M ~
|
||||
|
||||
- redeem reset request
|
||||
- needs a way to identify the user because only the tuple is unique (user + key)
|
||||
username or email address
|
||||
- provide new password
|
||||
*/
|
||||
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use byteorder::ByteOrder;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::warn;
|
||||
use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset;
|
||||
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 rand::RngCore;
|
||||
use serde::Deserialize;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
/// This can be reused in login requests in the future!
|
||||
#[derive(Deserialize)]
|
||||
pub enum UserLogin {
|
||||
Email(String),
|
||||
Username(String),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PasswordResetRequest {
|
||||
login: UserLogin,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PasswordResetResponse {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub enum PasswordResetRequestError {
|
||||
NoSuchUser,
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PasswordResetErrorResponse {
|
||||
kind: PasswordResetRequestError,
|
||||
}
|
||||
impl From<PasswordResetRequestError> for PasswordResetErrorResponse {
|
||||
fn from(value: PasswordResetRequestError) -> Self {
|
||||
Self { kind: value }
|
||||
}
|
||||
}
|
||||
impl From<errors::AnyhowError> for PasswordResetErrorResponse {
|
||||
fn from(value: errors::AnyhowError) -> Self {
|
||||
log::error!("Internal error: {value}");
|
||||
Self { kind: PasswordResetRequestError::Internal }
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// 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<PasswordResetRequest>,
|
||||
// session_cookie_manager: web::Data<SessionCookieManager>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
) -> Result<HttpResponse, PasswordResetErrorResponse> {
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let user = match &request.login {
|
||||
UserLogin::Email(email) => lookup_user_for_login_by_email(&email, &mysql_pool).await,
|
||||
UserLogin::Username(username) => lookup_user_for_login_by_username(&username, &mysql_pool).await,
|
||||
}.map_err(|e| {
|
||||
warn!("Password reset user lookup: {:?}", e);
|
||||
PasswordResetRequestError::NoSuchUser
|
||||
//TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻
|
||||
})?;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut secret_key_bytes = [0u8;40];
|
||||
// Has to fit within (4 / 3) * string length (32 chars),
|
||||
// but also be a multiple of 8 so we can read them as `u64`s
|
||||
// (for Crockford)
|
||||
rng.fill_bytes(&mut secret_key_bytes);
|
||||
|
||||
let mut secret_key = String::new();
|
||||
secret_key_bytes.chunks_exact(8)
|
||||
.map(|chunk| byteorder::LittleEndian::read_u64(chunk))
|
||||
.for_each(|chunk| crockford::encode_into::<String>(chunk, &mut secret_key));
|
||||
|
||||
|
||||
//TODO: Handle banned users, they shouldn't be able to do this
|
||||
|
||||
create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await?;
|
||||
email::send_password_reset(&user.email_address, secret_key).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
Reference in new issue
Block a user