You've already forked AstralRinth
forked from didirus/AstralRinth
[DO NOT MERGE] Email notification system (#4338)
* Migration * Fixup db models * Redis * Stuff * Switch PKs to BIGSERIALs, insert to notifications_deliveries when inserting notifications * Queue, templates * Query cache * Fixes, fixtures * Perf, cache template data & HTML bodies * Notification type configuration, ResetPassword notification type * Reset password * Query cache * Clippy + fmt * Traces, fix typo, fix user email in ResetPassword * send_email * Models, db * Remove dead code, adjust notification settings in migration * Clippy fmt * Delete dead code, fixes * Fmt * Update apps/labrinth/src/queue/email.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Remove old fixtures * Unify email retry delay * Fix type * External notifications * Remove `notifications_types_preference_restrictions`, as user notification preferences is out of scope for this PR * Query cache, fmt, clippy * Fix join in get_many_user_exposed_on_site * Remove migration comment * Query cache * Update html body urls * Remove comment * Add paymentfailed.service variable to PaymentFailed notification variant * Fix compile error * Fix deleting notifications * Update apps/labrinth/src/database/models/user_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update apps/labrinth/src/database/models/user_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update Cargo.toml Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update apps/labrinth/migrations/20250902133943_notification-extension.sql Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Address review comments * Fix compliation * Update apps/labrinth/src/database/models/users_notifications_preferences_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Use strfmt to format emails * Configurable Reply-To * Configurable Reply-To * Refactor for email background task * Send some emails inline * Fix account creation email check * Revert "Use strfmt to format emails" This reverts commit e0d6614afe51fa6349918377e953ba294c34ae0b. * Reintroduce fill_template * Set password reset email inline * Process more emails per index * clippy fmt * Query cache --------- Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Josiah Glosson <soujournme@gmail.com>
This commit is contained in:
committed by
GitHub
parent
1491642209
commit
902d749293
@@ -1,5 +1,6 @@
|
||||
use crate::auth::{get_user_from_headers, send_email};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::users_redeemals::{self, UserRedeemal};
|
||||
@@ -13,6 +14,7 @@ use crate::models::billing::{
|
||||
Product, ProductMetadata, ProductPrice, SubscriptionMetadata,
|
||||
SubscriptionStatus, UserSubscription,
|
||||
};
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::Badges;
|
||||
use crate::queue::session::AuthQueue;
|
||||
@@ -2458,7 +2460,7 @@ pub async fn stripe_webhook(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(email) = metadata.user_item.email {
|
||||
if metadata.user_item.email.is_some() {
|
||||
let money = rusty_money::Money::from_minor(
|
||||
metadata.charge_item.amount as i64,
|
||||
rusty_money::iso::find(
|
||||
@@ -2467,22 +2469,29 @@ pub async fn stripe_webhook(
|
||||
.unwrap_or(rusty_money::iso::USD),
|
||||
);
|
||||
|
||||
let _ = send_email(
|
||||
email,
|
||||
"Payment Failed for Modrinth",
|
||||
&format!(
|
||||
"Our attempt to collect payment for {money} from the payment card on file was unsuccessful."
|
||||
),
|
||||
"Please visit the following link below to update your payment method or contact your card provider. If the button does not work, you can copy the link and paste it into your browser.",
|
||||
Some((
|
||||
"Update billing settings",
|
||||
&format!(
|
||||
"{}/{}",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_BILLING_PATH")?
|
||||
),
|
||||
)),
|
||||
);
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::PaymentFailed {
|
||||
amount: money.to_string(),
|
||||
service: if metadata
|
||||
.product_item
|
||||
.metadata
|
||||
.is_midas()
|
||||
{
|
||||
"Modrinth+"
|
||||
} else if metadata
|
||||
.product_item
|
||||
.metadata
|
||||
.is_pyro()
|
||||
{
|
||||
"Modrinth Servers"
|
||||
} else {
|
||||
"a Modrinth product"
|
||||
}
|
||||
.to_owned(),
|
||||
},
|
||||
}
|
||||
.insert(metadata.user_item.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
52
apps/labrinth/src/routes/internal/external_notifications.rs
Normal file
52
apps/labrinth/src/routes/internal/external_notifications.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::database::models::ids::DBUserId;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::guards::external_notification_key_guard;
|
||||
use actix_web::web;
|
||||
use actix_web::{HttpResponse, post};
|
||||
use ariadne::ids::UserId;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(create);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateNotification {
|
||||
pub body: NotificationBody,
|
||||
pub user_ids: Vec<UserId>,
|
||||
}
|
||||
|
||||
#[post("external_notifications", guard = "external_notification_key_guard")]
|
||||
pub async fn create(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
create_notification: web::Json<CreateNotification>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let CreateNotification { body, user_ids } =
|
||||
create_notification.into_inner();
|
||||
let user_ids = user_ids
|
||||
.into_iter()
|
||||
.map(|x| DBUserId(x.0 as i64))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
if !DBUser::exists_many(&user_ids, &mut *txn).await? {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"One of the specified users do not exist.".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
NotificationBuilder { body }
|
||||
.insert_many(user_ids, &mut txn, &redis)
|
||||
.await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::auth::email::send_email;
|
||||
use crate::auth::validate::{
|
||||
get_full_user_from_headers, get_user_record_from_bearer_token,
|
||||
};
|
||||
use crate::auth::{AuthProvider, AuthenticationError, get_user_from_headers};
|
||||
use crate::database::models::DBUser;
|
||||
use crate::database::models::flow_item::DBFlow;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::{Badges, Role};
|
||||
use crate::queue::email::EmailQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::session::issue_session;
|
||||
@@ -25,6 +27,7 @@ use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use ariadne::ids::random_base62_rng;
|
||||
use base64::Engine;
|
||||
use chrono::{Duration, Utc};
|
||||
use lettre::message::Mailbox;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
@@ -1159,14 +1162,10 @@ pub async fn auth_callback(
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
} else if let Some(email) = user.and_then(|x| x.email) {
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method added",
|
||||
&format!("When logging into Modrinth, you can now log in using the {} authentication provider.", provider.as_str()),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
} else if let Some(user) = user {
|
||||
NotificationBuilder { body: NotificationBody::AuthProviderAdded { provider: provider.as_str().to_string() } }
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
@@ -1268,19 +1267,14 @@ pub async fn delete_auth_provider(
|
||||
.update_user_id(user.id.into(), None, &mut transaction)
|
||||
.await?;
|
||||
|
||||
if delete_provider.provider != AuthProvider::PayPal
|
||||
&& let Some(email) = user.email
|
||||
{
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method removed",
|
||||
&format!(
|
||||
"When logging into Modrinth, you can no longer log in using the {} authentication provider.",
|
||||
delete_provider.provider.as_str()
|
||||
),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
if delete_provider.provider != AuthProvider::PayPal {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::AuthProviderRemoved {
|
||||
provider: delete_provider.provider.as_str().to_string(),
|
||||
},
|
||||
}
|
||||
.insert(user.id.into(), &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
@@ -1342,6 +1336,7 @@ pub async fn create_account_with_password(
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
new_account: web::Json<NewAccount>,
|
||||
email: web::Data<EmailQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
new_account.0.validate().map_err(|err| {
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
@@ -1437,6 +1432,10 @@ pub async fn create_account_with_password(
|
||||
let session = issue_session(req, user_id, &mut transaction, &redis).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(),
|
||||
@@ -1444,11 +1443,15 @@ pub async fn create_account_with_password(
|
||||
.insert(Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(
|
||||
new_account.email.clone(),
|
||||
flow,
|
||||
&format!("Welcome to Modrinth, {}!", new_account.username),
|
||||
)?;
|
||||
email
|
||||
.send_one(
|
||||
&mut transaction,
|
||||
NotificationBody::VerifyEmail { flow },
|
||||
user_id,
|
||||
mailbox,
|
||||
)
|
||||
.await?
|
||||
.as_user_error()?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
@@ -1796,15 +1799,11 @@ pub async fn finish_2fa_flow(
|
||||
codes.push(to_base62(val));
|
||||
}
|
||||
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Two-factor authentication enabled",
|
||||
"When logging into Modrinth, you can now enter a code generated by your authenticator app in addition to entering your usual email address and password.",
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::TwoFactorEnabled,
|
||||
}
|
||||
.insert(user.id.into(), &mut transaction, &redis)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
@@ -1895,15 +1894,11 @@ pub async fn remove_2fa(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Two-factor authentication removed",
|
||||
"When logging into Modrinth, you no longer need two-factor authentication to gain access.",
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::TwoFactorRemoved,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
||||
@@ -1925,15 +1920,18 @@ pub async fn reset_password_begin(
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
reset_password: web::Json<ResetPassword>,
|
||||
email: web::Data<EmailQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if !check_hcaptcha(&req, &reset_password.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
let user =
|
||||
match crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||
&reset_password.username_or_email,
|
||||
&**pool,
|
||||
&mut *txn,
|
||||
)
|
||||
.await?[..]
|
||||
{
|
||||
@@ -1941,7 +1939,7 @@ pub async fn reset_password_begin(
|
||||
// Try finding by username or ID
|
||||
crate::database::models::DBUser::get(
|
||||
&reset_password.username_or_email,
|
||||
&**pool,
|
||||
&mut *txn,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
@@ -1950,7 +1948,7 @@ pub async fn reset_password_begin(
|
||||
// If there is only one user with the given email, ignoring case,
|
||||
// we can assume it's the user we want to reset the password for
|
||||
crate::database::models::DBUser::get_id(
|
||||
user_id, &**pool, &redis,
|
||||
user_id, &mut *txn, &redis,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -1962,12 +1960,12 @@ pub async fn reset_password_begin(
|
||||
if let Some(user_id) =
|
||||
crate::database::models::DBUser::get_by_email(
|
||||
&reset_password.username_or_email,
|
||||
&**pool,
|
||||
&mut *txn,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
crate::database::models::DBUser::get_id(
|
||||
user_id, &**pool, &redis,
|
||||
user_id, &mut *txn, &redis,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -1978,7 +1976,7 @@ pub async fn reset_password_begin(
|
||||
|
||||
if let Some(DBUser {
|
||||
id: user_id,
|
||||
email: Some(email),
|
||||
email: user_email,
|
||||
..
|
||||
}) = user
|
||||
{
|
||||
@@ -1986,23 +1984,21 @@ pub async fn reset_password_begin(
|
||||
.insert(Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email(
|
||||
email,
|
||||
"Reset your password",
|
||||
"Please visit the following link below to reset your password. If the button does not work, you can copy the link and paste it into your browser.",
|
||||
"If you did not request for your password to be reset, you can safely ignore this email.",
|
||||
Some((
|
||||
"Reset password",
|
||||
&format!(
|
||||
"{}/{}?flow={}",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_RESET_PASSWORD_PATH")?,
|
||||
flow
|
||||
),
|
||||
)),
|
||||
)?;
|
||||
if let Ok(mailbox) = user_email.unwrap_or_default().parse() {
|
||||
email
|
||||
.send_one(
|
||||
&mut txn,
|
||||
NotificationBody::ResetPassword { flow },
|
||||
user_id,
|
||||
mailbox,
|
||||
)
|
||||
.await?
|
||||
.as_user_error()?;
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
@@ -2138,20 +2134,18 @@ pub async fn change_password(
|
||||
DBFlow::remove(flow, &redis).await?;
|
||||
}
|
||||
|
||||
if let Some(email) = user.email {
|
||||
let changed = if update_password.is_some() {
|
||||
"changed"
|
||||
} else {
|
||||
"removed"
|
||||
};
|
||||
|
||||
send_email(
|
||||
email,
|
||||
&format!("Password {changed}"),
|
||||
&format!("Your password has been {changed} on your account."),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
if update_password.is_some() {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::PasswordChanged,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
} else {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::PasswordRemoved,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
@@ -2172,14 +2166,19 @@ pub async fn set_email(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
email: web::Json<SetEmail>,
|
||||
email_address: web::Json<SetEmail>,
|
||||
email: web::Data<EmailQueue>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
stripe_client: Data<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
email.0.validate().map_err(|err| {
|
||||
email_address.0.validate().map_err(|err| {
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
|
||||
let mailbox: Mailbox = email_address.email.parse().map_err(|_| {
|
||||
ApiError::InvalidInput("Invalid email address!".to_string())
|
||||
})?;
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -2191,7 +2190,7 @@ pub async fn set_email(
|
||||
.1;
|
||||
|
||||
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||
&email.email,
|
||||
&email_address.email,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
@@ -2210,23 +2209,21 @@ pub async fn set_email(
|
||||
SET email = $1, email_verified = FALSE
|
||||
WHERE (id = $2)
|
||||
",
|
||||
email.email,
|
||||
email_address.email,
|
||||
user.id.0 as i64,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(user_email) = user.email {
|
||||
send_email(
|
||||
user_email,
|
||||
"Email changed",
|
||||
&format!(
|
||||
"Your email has been updated to {} on your account.",
|
||||
email.email
|
||||
),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
if let Some(user_email) = user.email.clone() {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::EmailChanged {
|
||||
new_email: email_address.email.clone(),
|
||||
to_email: user_email,
|
||||
},
|
||||
}
|
||||
.insert(user.id.into(), &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(customer_id) = user
|
||||
@@ -2238,7 +2235,7 @@ pub async fn set_email(
|
||||
&stripe_client,
|
||||
&customer_id,
|
||||
stripe::UpdateCustomer {
|
||||
email: Some(&email.email),
|
||||
email: Some(&email_address.email),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -2247,18 +2244,23 @@ pub async fn set_email(
|
||||
|
||||
let flow = DBFlow::ConfirmEmail {
|
||||
user_id: user.id.into(),
|
||||
confirm_email: email.email.clone(),
|
||||
confirm_email: email_address.email.clone(),
|
||||
}
|
||||
.insert(Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(
|
||||
email.email.clone(),
|
||||
flow,
|
||||
"We need to verify your email address.",
|
||||
)?;
|
||||
email
|
||||
.send_one(
|
||||
&mut transaction,
|
||||
NotificationBody::VerifyEmail { flow },
|
||||
user.id.into(),
|
||||
mailbox,
|
||||
)
|
||||
.await?
|
||||
.as_user_error()?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
&[(user.id.into(), None)],
|
||||
&redis,
|
||||
@@ -2274,6 +2276,7 @@ pub async fn resend_verify_email(
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
email: web::Data<EmailQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
@@ -2285,7 +2288,7 @@ pub async fn resend_verify_email(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if let Some(email) = user.email {
|
||||
if let Some(email_address) = user.email {
|
||||
if user.email_verified.unwrap_or(false) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"User email is already verified!".to_string(),
|
||||
@@ -2294,16 +2297,28 @@ pub async fn resend_verify_email(
|
||||
|
||||
let flow = DBFlow::ConfirmEmail {
|
||||
user_id: user.id.into(),
|
||||
confirm_email: email.clone(),
|
||||
confirm_email: email_address.clone(),
|
||||
}
|
||||
.insert(Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(
|
||||
email,
|
||||
flow,
|
||||
"We need to verify your email address.",
|
||||
)?;
|
||||
let mailbox: Mailbox = email_address.parse().map_err(|_| {
|
||||
ApiError::InvalidInput("Invalid email address!".to_string())
|
||||
})?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
email
|
||||
.send_one(
|
||||
&mut transaction,
|
||||
NotificationBody::VerifyEmail { flow },
|
||||
user.id.into(),
|
||||
mailbox,
|
||||
)
|
||||
.await?
|
||||
.as_user_error()?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
} else {
|
||||
@@ -2438,25 +2453,3 @@ pub async fn get_newsletter_subscription_status(
|
||||
"subscribed": is_subscribed
|
||||
})))
|
||||
}
|
||||
|
||||
fn send_email_verify(
|
||||
email: String,
|
||||
flow: String,
|
||||
opener: &str,
|
||||
) -> Result<(), crate::auth::email::MailError> {
|
||||
send_email(
|
||||
email,
|
||||
"Verify your email",
|
||||
opener,
|
||||
"Please visit the following link below to verify your email. If the button does not work, you can copy the link and paste it into your browser. This link expires in 24 hours.",
|
||||
Some((
|
||||
"Verify email",
|
||||
&format!(
|
||||
"{}/{}?flow={}",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
dotenvy::var("SITE_VERIFY_EMAIL_PATH")?,
|
||||
flow
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,14 +69,22 @@ pub async fn export(
|
||||
.map(|x| crate::models::organizations::Organization::from(x, vec![]))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let notifs = crate::database::models::notification_item::DBNotification::get_many_user(
|
||||
user_id, &**pool, &redis,
|
||||
let notifs = crate::database::models::notification_item::DBNotification::get_all_user(
|
||||
user_id, &**pool,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(crate::models::notifications::Notification::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let notifs_deliveries = crate::database::models::notifications_deliveries_item::DBNotificationDelivery::get_all_user(
|
||||
user_id, &**pool,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(crate::models::notifications::NotificationDelivery::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_clients =
|
||||
crate::database::models::oauth_client_item::DBOAuthClient::get_all_user_clients(
|
||||
user_id, &**pool,
|
||||
@@ -195,6 +203,7 @@ pub async fn export(
|
||||
"projects": projects,
|
||||
"orgs": orgs,
|
||||
"notifs": notifs,
|
||||
"notifs_deliveries": notifs_deliveries,
|
||||
"oauth_clients": oauth_clients,
|
||||
"oauth_authorizations": oauth_authorizations,
|
||||
"pats": pats,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) mod admin;
|
||||
pub mod billing;
|
||||
pub mod external_notifications;
|
||||
pub mod flows;
|
||||
pub mod gdpr;
|
||||
pub mod medal;
|
||||
@@ -26,6 +27,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
.configure(billing::config)
|
||||
.configure(gdpr::config)
|
||||
.configure(statuses::config)
|
||||
.configure(medal::config),
|
||||
.configure(medal::config)
|
||||
.configure(external_notifications::config),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub enum ApiError {
|
||||
#[error("Password Hashing Error: {0}")]
|
||||
PasswordHashing(#[from] argon2::password_hash::Error),
|
||||
#[error("{0}")]
|
||||
Mail(#[from] crate::auth::email::MailError),
|
||||
Mail(#[from] crate::queue::email::MailError),
|
||||
#[error("Error while rerouting request: {0}")]
|
||||
Reroute(#[from] reqwest::Error),
|
||||
#[error("Unable to read Zip Archive: {0}")]
|
||||
|
||||
@@ -779,7 +779,7 @@ pub async fn user_notifications(
|
||||
}
|
||||
|
||||
let mut notifications: Vec<Notification> =
|
||||
crate::database::models::notification_item::DBNotification::get_many_user(
|
||||
crate::database::models::notification_item::DBNotification::get_many_user_exposed_on_site(
|
||||
id, &**pool, &redis,
|
||||
)
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user