mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
edit email address
This commit is contained in:
1 parent
b311719e9b
commit
654bd160ef
10 files changed
+290
-1
No files matched your search
@@ -0,0 +1,12 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailRequest {
|
||||
pub email_address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod edit_email;
|
||||
pub mod edit_username;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod email_to_gravatar;
|
||||
pub mod generate_random_synthetic_email;
|
||||
pub mod generate_random_synthetic_email;
|
||||
pub mod validate_email_address;
|
||||
Whitespace-only changes.
@@ -2,5 +2,6 @@ pub mod set_can_access_studio_transactional;
|
||||
pub mod set_user_ban_status;
|
||||
pub mod set_user_feature_flags;
|
||||
pub mod set_user_feature_flags_transactional;
|
||||
pub mod update_email;
|
||||
pub mod update_user_record_with_new_stripe_customer_id;
|
||||
pub mod update_username;
|
||||
@@ -0,0 +1,83 @@
|
||||
use crate::utils::transactor::Transactor;
|
||||
use sqlx;
|
||||
use sqlx::Error::Database;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct UpdateEmailArgs<'e, 't> {
|
||||
pub token: &'e UserToken,
|
||||
pub email_address: &'e str,
|
||||
pub email_gravatar_hash: &'e str,
|
||||
pub ip_address: &'e str,
|
||||
pub transactor: Transactor<'e, 't>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateEmailError {
|
||||
EmailIsTaken,
|
||||
DatabaseError { source: sqlx::Error },
|
||||
}
|
||||
|
||||
impl Error for UpdateEmailError {}
|
||||
|
||||
impl Display for UpdateEmailError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UpdateEmailError::EmailIsTaken => {
|
||||
write!(f, "UpdateEmailError: email is taken")
|
||||
}
|
||||
UpdateEmailError::DatabaseError { source } => {
|
||||
write!(f, "UpdateEmailError: database error: {:?}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_email<'e, 't>(
|
||||
args: UpdateEmailArgs<'e, 't>
|
||||
) -> Result<(), UpdateEmailError>
|
||||
{
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
email_address = ?,
|
||||
email_confirmed = false,
|
||||
email_confirmed_by_google = false,
|
||||
email_is_synthetic = false,
|
||||
email_gravatar_hash = ?,
|
||||
ip_address_last_update = ?,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.email_address,
|
||||
args.email_gravatar_hash,
|
||||
args.ip_address,
|
||||
args.token,
|
||||
);
|
||||
|
||||
let query_result = args.transactor.execute(query).await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(Database(err)) => {
|
||||
let maybe_code = err.code()
|
||||
.map(|c| c.into_owned());
|
||||
|
||||
// MySQL error code 23000 is a duplicate key constraint violation
|
||||
if maybe_code.as_deref() == Some("23000")
|
||||
&& err.message().contains("email_address")
|
||||
{
|
||||
return Err(UpdateEmailError::EmailIsTaken);
|
||||
}
|
||||
|
||||
Err(UpdateEmailError::DatabaseError { source: Database(err) })
|
||||
},
|
||||
Err(err) => {
|
||||
Err(UpdateEmailError::DatabaseError { source: err })
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ use crate::http_server::endpoints::user_ratings::batch_get_user_rating_handler::
|
||||
use crate::http_server::endpoints::user_ratings::get_user_rating_handler::*;
|
||||
use crate::http_server::endpoints::user_ratings::set_user_rating_handler::*;
|
||||
use crate::http_server::endpoints::users::create_account_handler::*;
|
||||
use crate::http_server::endpoints::users::edit_email_handler::*;
|
||||
use crate::http_server::endpoints::users::edit_username_handler::*;
|
||||
use crate::http_server::endpoints::users::get_profile_handler::*;
|
||||
use crate::http_server::endpoints::users::google_sso::google_sso_handler::*;
|
||||
@@ -122,6 +123,8 @@ use crate::http_server::endpoints::weights::list::list_weights_by_user_handler::
|
||||
use crate::http_server::endpoints::weights::search::search_model_weights_impl::*;
|
||||
use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*;
|
||||
use crate::http_server::endpoints::weights::update::update_weight_handler::*;
|
||||
use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse};
|
||||
use artcraft_api_defs::users::edit_username::{EditUsernameRequest, EditUsernameResponse};
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
|
||||
use artcraft_api_defs::common::responses::media_links::*;
|
||||
@@ -314,6 +317,7 @@ use tokens::tokens::zs_voice_datasets::*;
|
||||
crate::http_server::endpoints::user_ratings::get_user_rating_handler::get_user_rating_handler,
|
||||
crate::http_server::endpoints::user_ratings::set_user_rating_handler::set_user_rating_handler,
|
||||
crate::http_server::endpoints::users::create_account_handler::create_account_handler,
|
||||
crate::http_server::endpoints::users::edit_email_handler::edit_email_handler,
|
||||
crate::http_server::endpoints::users::edit_username_handler::edit_username_handler,
|
||||
crate::http_server::endpoints::users::get_profile_handler::get_profile_handler,
|
||||
crate::http_server::endpoints::users::google_sso::google_sso_handler::google_sso_handler,
|
||||
@@ -508,6 +512,9 @@ use tokens::tokens::zs_voice_datasets::*;
|
||||
GptImage1EditImageNumImages,
|
||||
GptImage1EditImageImageQuality,
|
||||
GptImage1EditImageResponse,
|
||||
EditEmailError,
|
||||
EditEmailRequest,
|
||||
EditEmailResponse,
|
||||
EditUsernameError,
|
||||
EditUsernameRequest,
|
||||
EditUsernameResponse,
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
use utoipa::ToSchema;
|
||||
use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse};
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
use hashing::md5::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::users::user::update::update_email::{update_email, UpdateEmailArgs, UpdateEmailError};
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum EditEmailError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for EditEmailError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EditEmailError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EditEmailError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EditEmailError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
EditEmailError::BadInput(reason) => reason.to_string(),
|
||||
EditEmailError::NotAuthorized => "unauthorized".to_string(),
|
||||
EditEmailError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for EditEmailError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic email format validation
|
||||
fn validate_email(email: &str) -> Result<(), String> {
|
||||
let email = email.trim();
|
||||
|
||||
if email.is_empty() {
|
||||
return Err("email address cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if email.len() > 255 {
|
||||
return Err("email address is too long".to_string());
|
||||
}
|
||||
|
||||
// Basic format check: must contain exactly one @ with content on both sides
|
||||
let at_count = email.matches('@').count();
|
||||
if at_count != 1 {
|
||||
return Err("email address must contain exactly one @".to_string());
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = email.split('@').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("invalid email format".to_string());
|
||||
}
|
||||
|
||||
let local = parts[0];
|
||||
let domain = parts[1];
|
||||
|
||||
if local.is_empty() {
|
||||
return Err("email address must have content before @".to_string());
|
||||
}
|
||||
|
||||
if domain.is_empty() {
|
||||
return Err("email address must have a domain after @".to_string());
|
||||
}
|
||||
|
||||
// Domain must contain at least one dot
|
||||
if !domain.contains('.') {
|
||||
return Err("email domain must contain a dot".to_string());
|
||||
}
|
||||
|
||||
// Domain cannot start or end with a dot
|
||||
if domain.starts_with('.') || domain.ends_with('.') {
|
||||
return Err("email domain cannot start or end with a dot".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Edit email address of the current user.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Users",
|
||||
path = "/v1/user/edit_email",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = EditEmailResponse),
|
||||
(status = 400, description = "Bad input", body = EditEmailError),
|
||||
(status = 401, description = "Not authorized", body = EditEmailError),
|
||||
(status = 500, description = "Server error", body = EditEmailError),
|
||||
),
|
||||
params(
|
||||
("request" = EditEmailRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn edit_email_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<EditEmailRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<EditEmailResponse>, EditEmailError>
|
||||
{
|
||||
let email_address = request.email_address.trim().to_lowercase();
|
||||
|
||||
if let Err(reason) = validate_email(&email_address) {
|
||||
return Err(EditEmailError::BadInput(format!("bad email: {}", &reason)));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
EditEmailError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|_err| {
|
||||
EditEmailError::NotAuthorized
|
||||
})?;
|
||||
|
||||
if user_session.role.is_banned {
|
||||
return Err(EditEmailError::NotAuthorized);
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let gravatar_hash = email_to_gravatar_hash(&email_address);
|
||||
|
||||
let result = update_email(UpdateEmailArgs {
|
||||
token: &user_session.user_token_typed,
|
||||
email_address: &email_address,
|
||||
email_gravatar_hash: &gravatar_hash,
|
||||
ip_address: &ip_address,
|
||||
transactor: Transactor::for_connection(&mut mysql_connection),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(()) => {},
|
||||
Err(UpdateEmailError::EmailIsTaken) => {
|
||||
return Err(EditEmailError::BadInput("email address is already in use".to_string()));
|
||||
}
|
||||
Err(UpdateEmailError::DatabaseError { source }) => {
|
||||
warn!("Error updating email: {:?}", source);
|
||||
return Err(EditEmailError::ServerError);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(EditEmailResponse { success: true }))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod create_account_handler;
|
||||
pub mod edit_email_handler;
|
||||
pub mod edit_profile_handler;
|
||||
pub mod edit_username_handler;
|
||||
pub mod get_profile_handler;
|
||||
|
||||
+6
@@ -9,6 +9,7 @@ use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_templates::list
|
||||
use crate::http_server::endpoints::tts::list_user_tts_inference_results::list_user_tts_inference_results_handler;
|
||||
use crate::http_server::endpoints::tts::list_user_tts_models::list_user_tts_models_handler;
|
||||
use crate::http_server::endpoints::users::create_account_handler::create_account_handler;
|
||||
use crate::http_server::endpoints::users::edit_email_handler::edit_email_handler;
|
||||
use crate::http_server::endpoints::users::edit_profile_handler::edit_profile_handler;
|
||||
use crate::http_server::endpoints::users::edit_username_handler::edit_username_handler;
|
||||
use crate::http_server::endpoints::users::get_profile_handler::get_profile_handler;
|
||||
@@ -99,6 +100,11 @@ pub fn add_user_routes<T, B> (app: App<T>) -> App<T>
|
||||
|
||||
// NB(bt): Modern user profile routes
|
||||
.service(web::scope("/v1/user")
|
||||
.service(
|
||||
web::resource("/edit_email")
|
||||
.route(web::post().to(edit_email_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(
|
||||
web::resource("/edit_username")
|
||||
.route(web::post().to(edit_username_handler))
|
||||
|
||||
Reference in new issue
Block a user