mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 21:17:06 +00:00
implement create user validation endpoint and add more specific error responses
This commit is contained in:
@@ -43,7 +43,13 @@ pub enum AuthenticationError {
|
|||||||
#[error(
|
#[error(
|
||||||
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
||||||
)]
|
)]
|
||||||
DuplicateUser,
|
DuplicateEmail,
|
||||||
|
#[error("Username is already taken on Modrinth.")]
|
||||||
|
UsernameTaken,
|
||||||
|
#[error(
|
||||||
|
"This authentication provider is already linked to another Modrinth account."
|
||||||
|
)]
|
||||||
|
ProviderAlreadyLinked,
|
||||||
#[error("Invalid state sent, you probably need to get a new websocket")]
|
#[error("Invalid state sent, you probably need to get a new websocket")]
|
||||||
SocketError,
|
SocketError,
|
||||||
#[error("Invalid callback URL specified")]
|
#[error("Invalid callback URL specified")]
|
||||||
@@ -73,7 +79,11 @@ impl actix_web::ResponseError for AuthenticationError {
|
|||||||
AuthenticationError::FileHosting(..) => {
|
AuthenticationError::FileHosting(..) => {
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
}
|
}
|
||||||
AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST,
|
AuthenticationError::DuplicateEmail => StatusCode::BAD_REQUEST,
|
||||||
|
AuthenticationError::UsernameTaken => StatusCode::BAD_REQUEST,
|
||||||
|
AuthenticationError::ProviderAlreadyLinked => {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
AuthenticationError::SocketError => StatusCode::BAD_REQUEST,
|
AuthenticationError::SocketError => StatusCode::BAD_REQUEST,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +112,11 @@ impl AuthenticationError {
|
|||||||
AuthenticationError::InvalidClientId => "invalid_client_id",
|
AuthenticationError::InvalidClientId => "invalid_client_id",
|
||||||
AuthenticationError::Url => "url_error",
|
AuthenticationError::Url => "url_error",
|
||||||
AuthenticationError::FileHosting(..) => "file_hosting",
|
AuthenticationError::FileHosting(..) => "file_hosting",
|
||||||
AuthenticationError::DuplicateUser => "duplicate_user",
|
AuthenticationError::DuplicateEmail => "duplicate_email",
|
||||||
|
AuthenticationError::UsernameTaken => "username_taken",
|
||||||
|
AuthenticationError::ProviderAlreadyLinked => {
|
||||||
|
"provider_already_linked"
|
||||||
|
}
|
||||||
AuthenticationError::SocketError => "socket",
|
AuthenticationError::SocketError => "socket",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::database::models::{DBUser, DBUserId};
|
|||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::env::ENV;
|
use crate::env::ENV;
|
||||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||||
|
use crate::models::error::ApiError as ApiErrorResponse;
|
||||||
use crate::models::notifications::NotificationBody;
|
use crate::models::notifications::NotificationBody;
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::users::{Badges, Role};
|
use crate::models::users::{Badges, Role};
|
||||||
@@ -23,6 +24,7 @@ use crate::util::ext::get_image_ext;
|
|||||||
use crate::util::img::upload_image_optimized;
|
use crate::util::img::upload_image_optimized;
|
||||||
use crate::util::validate::validation_errors_to_string;
|
use crate::util::validate::validation_errors_to_string;
|
||||||
use actix_http::header::LOCATION;
|
use actix_http::header::LOCATION;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::web::{Data, Query, ServiceConfig, scope};
|
use actix_web::web::{Data, Query, ServiceConfig, scope};
|
||||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||||
use argon2::password_hash::SaltString;
|
use argon2::password_hash::SaltString;
|
||||||
@@ -40,6 +42,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use thiserror::Error;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
@@ -52,6 +55,7 @@ pub fn config(cfg: &mut ServiceConfig) {
|
|||||||
.service(auth_callback)
|
.service(auth_callback)
|
||||||
.service(delete_auth_provider)
|
.service(delete_auth_provider)
|
||||||
.service(create_oauth_account)
|
.service(create_oauth_account)
|
||||||
|
.service(validate_create_account_with_password)
|
||||||
.service(create_account_with_password)
|
.service(create_account_with_password)
|
||||||
.service(login_password)
|
.service(login_password)
|
||||||
.service(login_2fa)
|
.service(login_2fa)
|
||||||
@@ -96,7 +100,7 @@ impl TempUser {
|
|||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err(AuthenticationError::DuplicateUser);
|
return Err(AuthenticationError::DuplicateEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
@@ -107,7 +111,7 @@ impl TempUser {
|
|||||||
.wrap_err("failed to fetch existing user by id")?;
|
.wrap_err("failed to fetch existing user by id")?;
|
||||||
|
|
||||||
if existing_id.is_some() {
|
if existing_id.is_some() {
|
||||||
return Err(AuthenticationError::DuplicateUser);
|
return Err(AuthenticationError::UsernameTaken);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (avatar_url, raw_avatar_url) = if let Some(avatar_url) =
|
let (avatar_url, raw_avatar_url) = if let Some(avatar_url) =
|
||||||
@@ -1266,7 +1270,7 @@ pub async fn auth_callback(
|
|||||||
|
|
||||||
if let Some(id) = user_id {
|
if let Some(id) = user_id {
|
||||||
if user_id_opt.is_some() {
|
if user_id_opt.is_some() {
|
||||||
return Err(AuthenticationError::DuplicateUser);
|
return Err(AuthenticationError::ProviderAlreadyLinked);
|
||||||
}
|
}
|
||||||
|
|
||||||
provider
|
provider
|
||||||
@@ -1339,6 +1343,7 @@ pub async fn auth_callback(
|
|||||||
// for this, we redirect them to a frontend page which lets them set a username.
|
// for this, we redirect them to a frontend page which lets them set a username.
|
||||||
// then frontend will call `/create/oauth` with the same state parameter and
|
// then frontend will call `/create/oauth` with the same state parameter and
|
||||||
// chosen settings (username, subscribe to newsletter), and handle navigation.
|
// chosen settings (username, subscribe to newsletter), and handle navigation.
|
||||||
|
let suggested_username = oauth_user.username.clone();
|
||||||
|
|
||||||
flow_guard
|
flow_guard
|
||||||
.replace_with(DBFlow::OAuthPending {
|
.replace_with(DBFlow::OAuthPending {
|
||||||
@@ -1356,7 +1361,8 @@ pub async fn auth_callback(
|
|||||||
.append_pair(
|
.append_pair(
|
||||||
"requires_dob",
|
"requires_dob",
|
||||||
&requires_dob(provider).to_string(),
|
&requires_dob(provider).to_string(),
|
||||||
);
|
)
|
||||||
|
.append_pair("username", &suggested_username);
|
||||||
|
|
||||||
let redirect_url = redirect_url.to_string();
|
let redirect_url = redirect_url.to_string();
|
||||||
Ok(HttpResponse::TemporaryRedirect()
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
@@ -1394,6 +1400,10 @@ async fn create_oauth_account(
|
|||||||
redis: Data<RedisPool>,
|
redis: Data<RedisPool>,
|
||||||
web::Json(new_account): web::Json<NewOAuthAccount>,
|
web::Json(new_account): web::Json<NewOAuthAccount>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
new_account.validate().map_err(|err| {
|
||||||
|
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||||
|
})?;
|
||||||
|
|
||||||
if !check_hcaptcha(&req, &new_account.challenge).await? {
|
if !check_hcaptcha(&req, &new_account.challenge).await? {
|
||||||
return Err(ApiError::Turnstile);
|
return Err(ApiError::Turnstile);
|
||||||
}
|
}
|
||||||
@@ -1525,19 +1535,243 @@ pub async fn check_sendy_subscription(
|
|||||||
Ok(response.trim() == "Subscribed")
|
Ok(response.trim() == "Subscribed")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize)]
|
||||||
pub struct NewAccount {
|
pub struct NewAccount {
|
||||||
// keep in sync with NewOAuthAccount
|
// keep in sync with NewOAuthAccount
|
||||||
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
#[validate(length(min = 8, max = 256))]
|
|
||||||
pub password: String,
|
pub password: String,
|
||||||
#[validate(email)]
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub challenge: String,
|
pub challenge: Option<String>,
|
||||||
pub sign_up_newsletter: Option<bool>,
|
pub sign_up_newsletter: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Validate)]
|
||||||
|
struct AccountRegisterFlow {
|
||||||
|
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
|
||||||
|
username: String,
|
||||||
|
#[validate(length(min = 8, max = 256))]
|
||||||
|
password: String,
|
||||||
|
#[validate(email)]
|
||||||
|
email: String,
|
||||||
|
sign_up_newsletter: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ReadyAccountRegisterFlow {
|
||||||
|
inner: AccountRegisterFlow,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum AccountRegisterValidateError {
|
||||||
|
#[error("Username is already taken on Modrinth.")]
|
||||||
|
UsernameTaken,
|
||||||
|
#[error(
|
||||||
|
"Email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
||||||
|
)]
|
||||||
|
DuplicateEmail,
|
||||||
|
#[error("{}", match .0 {
|
||||||
|
Some(feedback) => format!("Password too weak: {feedback}"),
|
||||||
|
None => "Specified password is too weak! Please improve its strength.".to_string(),
|
||||||
|
})]
|
||||||
|
WeakPassword(Option<String>),
|
||||||
|
#[error("{0}")]
|
||||||
|
InvalidInput(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountRegisterValidateError {
|
||||||
|
fn error_code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AccountRegisterValidateError::UsernameTaken => "username_taken",
|
||||||
|
AccountRegisterValidateError::DuplicateEmail => "duplicate_email",
|
||||||
|
AccountRegisterValidateError::WeakPassword(_) => "weak_password",
|
||||||
|
AccountRegisterValidateError::InvalidInput(_) => "invalid_input",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::ResponseError for AccountRegisterValidateError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::build(self.status_code()).json(ApiErrorResponse {
|
||||||
|
error: self.error_code(),
|
||||||
|
description: self.to_string(),
|
||||||
|
details: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AccountRegisterValidateError> for ApiError {
|
||||||
|
fn from(value: AccountRegisterValidateError) -> Self {
|
||||||
|
match &value {
|
||||||
|
AccountRegisterValidateError::UsernameTaken => {
|
||||||
|
ApiError::Authentication(AuthenticationError::UsernameTaken)
|
||||||
|
}
|
||||||
|
AccountRegisterValidateError::DuplicateEmail => {
|
||||||
|
ApiError::Authentication(AuthenticationError::DuplicateEmail)
|
||||||
|
}
|
||||||
|
_ => ApiError::InvalidInput(value.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NewAccount> for AccountRegisterFlow {
|
||||||
|
fn from(account: NewAccount) -> Self {
|
||||||
|
Self {
|
||||||
|
username: account.username,
|
||||||
|
password: account.password,
|
||||||
|
email: account.email,
|
||||||
|
sign_up_newsletter: account.sign_up_newsletter.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountRegisterFlow {
|
||||||
|
async fn validate(
|
||||||
|
self,
|
||||||
|
pool: &PgPool,
|
||||||
|
redis: &RedisPool,
|
||||||
|
) -> Result<ReadyAccountRegisterFlow, AccountRegisterValidateError> {
|
||||||
|
validator::Validate::validate(&self).map_err(|err| {
|
||||||
|
AccountRegisterValidateError::InvalidInput(
|
||||||
|
validation_errors_to_string(err, None),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if crate::database::models::DBUser::get(&self.username, pool, redis)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
AccountRegisterValidateError::InvalidInput(err.to_string())
|
||||||
|
})?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(AccountRegisterValidateError::UsernameTaken);
|
||||||
|
}
|
||||||
|
|
||||||
|
let score =
|
||||||
|
zxcvbn::zxcvbn(&self.password, &[&self.username, &self.email]);
|
||||||
|
|
||||||
|
if score.score() < Score::Three {
|
||||||
|
let feedback = score
|
||||||
|
.feedback()
|
||||||
|
.and_then(|x| x.warning())
|
||||||
|
.map(|w| w.to_string());
|
||||||
|
return Err(AccountRegisterValidateError::WeakPassword(feedback));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||||
|
&self.email,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
AccountRegisterValidateError::InvalidInput(err.to_string())
|
||||||
|
})?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(AccountRegisterValidateError::DuplicateEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ReadyAccountRegisterFlow { inner: self })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadyAccountRegisterFlow {
|
||||||
|
async fn execute(
|
||||||
|
self,
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: &PgPool,
|
||||||
|
redis: &RedisPool,
|
||||||
|
email_queue: &EmailQueue,
|
||||||
|
) -> Result<crate::models::sessions::Session, ApiError> {
|
||||||
|
let register_flow = self.inner;
|
||||||
|
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
let user_id =
|
||||||
|
crate::database::models::generate_user_id(&mut transaction).await?;
|
||||||
|
|
||||||
|
let hasher = Argon2::default();
|
||||||
|
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
|
||||||
|
let password_hash = hasher
|
||||||
|
.hash_password(register_flow.password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
crate::database::models::DBUser {
|
||||||
|
id: user_id,
|
||||||
|
github_id: None,
|
||||||
|
discord_id: None,
|
||||||
|
gitlab_id: None,
|
||||||
|
google_id: None,
|
||||||
|
steam_id: None,
|
||||||
|
microsoft_id: None,
|
||||||
|
password: Some(password_hash),
|
||||||
|
paypal_id: None,
|
||||||
|
paypal_country: None,
|
||||||
|
paypal_email: None,
|
||||||
|
venmo_handle: None,
|
||||||
|
stripe_customer_id: None,
|
||||||
|
totp_secret: None,
|
||||||
|
username: register_flow.username.clone(),
|
||||||
|
email: Some(register_flow.email.clone()),
|
||||||
|
email_verified: false,
|
||||||
|
avatar_url: None,
|
||||||
|
raw_avatar_url: None,
|
||||||
|
bio: None,
|
||||||
|
created: Utc::now(),
|
||||||
|
role: Role::Developer.to_string(),
|
||||||
|
badges: Badges::default(),
|
||||||
|
allow_friend_requests: true,
|
||||||
|
is_subscribed_to_newsletter: register_flow.sign_up_newsletter,
|
||||||
|
}
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let session =
|
||||||
|
issue_session(req, user_id, &mut transaction, redis, None).await?;
|
||||||
|
let res = crate::models::sessions::Session::from(session, true, None);
|
||||||
|
|
||||||
|
let mailbox: Mailbox = register_flow.email.parse().map_err(|_| {
|
||||||
|
ApiError::InvalidInput("Invalid email address!".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let flow = DBFlow::ConfirmEmail {
|
||||||
|
user_id,
|
||||||
|
confirm_email: register_flow.email.clone(),
|
||||||
|
}
|
||||||
|
.insert(Duration::hours(24), redis)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
email_queue
|
||||||
|
.send_one(
|
||||||
|
&mut transaction,
|
||||||
|
NotificationBody::VerifyEmail { flow },
|
||||||
|
user_id,
|
||||||
|
mailbox,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.as_user_error()?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("create/validate")]
|
||||||
|
pub async fn validate_create_account_with_password(
|
||||||
|
pool: Data<PgPool>,
|
||||||
|
redis: Data<RedisPool>,
|
||||||
|
new_account: web::Json<NewAccount>,
|
||||||
|
) -> Result<(), AccountRegisterValidateError> {
|
||||||
|
AccountRegisterFlow::from(new_account.into_inner())
|
||||||
|
.validate(&pool, &redis)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[post("create")]
|
#[post("create")]
|
||||||
pub async fn create_account_with_password(
|
pub async fn create_account_with_password(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@@ -1546,123 +1780,19 @@ pub async fn create_account_with_password(
|
|||||||
new_account: web::Json<NewAccount>,
|
new_account: web::Json<NewAccount>,
|
||||||
email: web::Data<EmailQueue>,
|
email: web::Data<EmailQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
new_account.0.validate().map_err(|err| {
|
let new_account = new_account.into_inner();
|
||||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !check_hcaptcha(&req, &new_account.challenge).await? {
|
if !check_hcaptcha(&req, new_account.challenge.as_deref().unwrap_or(""))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
return Err(ApiError::Turnstile);
|
return Err(ApiError::Turnstile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if crate::database::models::DBUser::get(
|
let ready_flow = AccountRegisterFlow::from(new_account)
|
||||||
&new_account.username,
|
.validate(&pool, &redis)
|
||||||
&**pool,
|
.await?;
|
||||||
&redis,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Err(ApiError::InvalidInput("Username is taken!".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut transaction = pool.begin().await?;
|
let res = ready_flow.execute(req, &pool, &redis, &email).await?;
|
||||||
let user_id =
|
|
||||||
crate::database::models::generate_user_id(&mut transaction).await?;
|
|
||||||
|
|
||||||
let new_account = new_account.0;
|
|
||||||
|
|
||||||
let score = zxcvbn::zxcvbn(
|
|
||||||
&new_account.password,
|
|
||||||
&[&new_account.username, &new_account.email],
|
|
||||||
);
|
|
||||||
|
|
||||||
if score.score() < Score::Three {
|
|
||||||
return Err(ApiError::InvalidInput(
|
|
||||||
if let Some(feedback) = score.feedback().and_then(|x| x.warning()) {
|
|
||||||
format!("Password too weak: {feedback}")
|
|
||||||
} else {
|
|
||||||
"Specified password is too weak! Please improve its strength."
|
|
||||||
.to_string()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasher = Argon2::default();
|
|
||||||
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
|
|
||||||
let password_hash = hasher
|
|
||||||
.hash_password(new_account.password.as_bytes(), &salt)?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
|
||||||
&new_account.email,
|
|
||||||
&**pool,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
return Err(ApiError::InvalidInput(
|
|
||||||
"Email is already registered on Modrinth! Try 'Forgot password' to access your account.".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::database::models::DBUser {
|
|
||||||
id: user_id,
|
|
||||||
github_id: None,
|
|
||||||
discord_id: None,
|
|
||||||
gitlab_id: None,
|
|
||||||
google_id: None,
|
|
||||||
steam_id: None,
|
|
||||||
microsoft_id: None,
|
|
||||||
password: Some(password_hash),
|
|
||||||
paypal_id: None,
|
|
||||||
paypal_country: None,
|
|
||||||
paypal_email: None,
|
|
||||||
venmo_handle: None,
|
|
||||||
stripe_customer_id: None,
|
|
||||||
totp_secret: None,
|
|
||||||
username: new_account.username.clone(),
|
|
||||||
email: Some(new_account.email.clone()),
|
|
||||||
email_verified: false,
|
|
||||||
avatar_url: None,
|
|
||||||
raw_avatar_url: None,
|
|
||||||
bio: None,
|
|
||||||
created: Utc::now(),
|
|
||||||
role: Role::Developer.to_string(),
|
|
||||||
badges: Badges::default(),
|
|
||||||
allow_friend_requests: true,
|
|
||||||
is_subscribed_to_newsletter: new_account
|
|
||||||
.sign_up_newsletter
|
|
||||||
.unwrap_or(false),
|
|
||||||
}
|
|
||||||
.insert(&mut transaction)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let session =
|
|
||||||
issue_session(req, user_id, &mut transaction, &redis, None).await?;
|
|
||||||
let res = crate::models::sessions::Session::from(session, true, None);
|
|
||||||
|
|
||||||
let mailbox: Mailbox = new_account.email.parse().map_err(|_| {
|
|
||||||
ApiError::InvalidInput("Invalid email address!".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let flow = DBFlow::ConfirmEmail {
|
|
||||||
user_id,
|
|
||||||
confirm_email: new_account.email.clone(),
|
|
||||||
}
|
|
||||||
.insert(Duration::hours(24), &redis)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
email
|
|
||||||
.send_one(
|
|
||||||
&mut transaction,
|
|
||||||
NotificationBody::VerifyEmail { flow },
|
|
||||||
user_id,
|
|
||||||
mailbox,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.as_user_error()?;
|
|
||||||
|
|
||||||
transaction.commit().await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(res))
|
Ok(HttpResponse::Ok().json(res))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user