From c7d0839bfb7681462f38e96d58396025c4bb0630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:51:50 +0200 Subject: [PATCH] fix(labrinth): retire Sendy for new email newsletter subscriptions (#4073) * tweak(frontend): do not sign up for the newsletter by default * fix(labrinth): retire Sendy for new email newsletter subscriptions --- apps/frontend/src/pages/auth/sign-up.vue | 2 +- ...d46bc09344c1846b3098accce5801e571e5e.json} | 5 +- ...29b0535241bbb6d74143925e16cf8cd720c4.json} | 10 ++- ...7e34bf813ddbfb88bf31b9863078bc48c8623.json | 14 +++ ...20_user-newsletter-subscription-column.sql | 1 + apps/labrinth/src/auth/validate.rs | 33 +++++-- .../labrinth/src/database/models/user_item.rs | 10 ++- apps/labrinth/src/routes/internal/flows.rs | 90 ++++++++----------- 8 files changed, 95 insertions(+), 70 deletions(-) rename apps/labrinth/.sqlx/{query-32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55.json => query-010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e.json} (71%) rename apps/labrinth/.sqlx/{query-b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0.json => query-5fcdeeeb820ada62e10feb0beefa29b0535241bbb6d74143925e16cf8cd720c4.json} (88%) create mode 100644 apps/labrinth/.sqlx/query-c960b09ddc19530383f143c349c7e34bf813ddbfb88bf31b9863078bc48c8623.json create mode 100644 apps/labrinth/migrations/20250727184120_user-newsletter-subscription-column.sql diff --git a/apps/frontend/src/pages/auth/sign-up.vue b/apps/frontend/src/pages/auth/sign-up.vue index 7f88fa512..a8cb5664c 100644 --- a/apps/frontend/src/pages/auth/sign-up.vue +++ b/apps/frontend/src/pages/auth/sign-up.vue @@ -218,7 +218,7 @@ const username = ref(""); const password = ref(""); const confirmPassword = ref(""); const token = ref(""); -const subscribe = ref(true); +const subscribe = ref(false); async function createAccount() { startLoading(); diff --git a/apps/labrinth/.sqlx/query-32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55.json b/apps/labrinth/.sqlx/query-010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e.json similarity index 71% rename from apps/labrinth/.sqlx/query-32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55.json rename to apps/labrinth/.sqlx/query-010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e.json index 8d844ddf9..082a6e4e2 100644 --- a/apps/labrinth/.sqlx/query-32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55.json +++ b/apps/labrinth/.sqlx/query-010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21\n )\n ", + "query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, $22\n )\n ", "describe": { "columns": [], "parameters": { @@ -25,10 +25,11 @@ "Text", "Text", "Text", + "Bool", "Bool" ] }, "nullable": [] }, - "hash": "32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55" + "hash": "010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e" } diff --git a/apps/labrinth/.sqlx/query-b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0.json b/apps/labrinth/.sqlx/query-5fcdeeeb820ada62e10feb0beefa29b0535241bbb6d74143925e16cf8cd720c4.json similarity index 88% rename from apps/labrinth/.sqlx/query-b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0.json rename to apps/labrinth/.sqlx/query-5fcdeeeb820ada62e10feb0beefa29b0535241bbb6d74143925e16cf8cd720c4.json index 988ec11ad..0c33202b9 100644 --- a/apps/labrinth/.sqlx/query-b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0.json +++ b/apps/labrinth/.sqlx/query-5fcdeeeb820ada62e10feb0beefa29b0535241bbb6d74143925e16cf8cd720c4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ", + "query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ", "describe": { "columns": [ { @@ -122,6 +122,11 @@ "ordinal": 23, "name": "allow_friend_requests", "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "is_subscribed_to_newsletter", + "type_info": "Bool" } ], "parameters": { @@ -154,8 +159,9 @@ true, true, true, + false, false ] }, - "hash": "b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0" + "hash": "5fcdeeeb820ada62e10feb0beefa29b0535241bbb6d74143925e16cf8cd720c4" } diff --git a/apps/labrinth/.sqlx/query-c960b09ddc19530383f143c349c7e34bf813ddbfb88bf31b9863078bc48c8623.json b/apps/labrinth/.sqlx/query-c960b09ddc19530383f143c349c7e34bf813ddbfb88bf31b9863078bc48c8623.json new file mode 100644 index 000000000..975dc151a --- /dev/null +++ b/apps/labrinth/.sqlx/query-c960b09ddc19530383f143c349c7e34bf813ddbfb88bf31b9863078bc48c8623.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE users\n SET is_subscribed_to_newsletter = TRUE\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "c960b09ddc19530383f143c349c7e34bf813ddbfb88bf31b9863078bc48c8623" +} diff --git a/apps/labrinth/migrations/20250727184120_user-newsletter-subscription-column.sql b/apps/labrinth/migrations/20250727184120_user-newsletter-subscription-column.sql new file mode 100644 index 000000000..5a475b68b --- /dev/null +++ b/apps/labrinth/migrations/20250727184120_user-newsletter-subscription-column.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN is_subscribed_to_newsletter BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index 806eaa126..325adc3d5 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -1,6 +1,6 @@ use super::AuthProvider; use crate::auth::AuthenticationError; -use crate::database::models::user_item; +use crate::database::models::{DBUser, user_item}; use crate::database::redis::RedisPool; use crate::models::pats::Scopes; use crate::models::users::User; @@ -44,17 +44,16 @@ where Ok(Some((scopes, User::from_full(db_user)))) } -pub async fn get_user_from_headers<'a, E>( +pub async fn get_full_user_from_headers<'a, E>( req: &HttpRequest, executor: E, redis: &RedisPool, session_queue: &AuthQueue, required_scopes: Scopes, -) -> Result<(Scopes, User), AuthenticationError> +) -> Result<(Scopes, DBUser), AuthenticationError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - // Fetch DB user record and minos user from headers let (scopes, db_user) = get_user_record_from_bearer_token( req, None, @@ -65,13 +64,33 @@ where .await? .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let user = User::from_full(db_user); - if !scopes.contains(required_scopes) { return Err(AuthenticationError::InvalidCredentials); } - Ok((scopes, user)) + Ok((scopes, db_user)) +} + +pub async fn get_user_from_headers<'a, E>( + req: &HttpRequest, + executor: E, + redis: &RedisPool, + session_queue: &AuthQueue, + required_scopes: Scopes, +) -> Result<(Scopes, User), AuthenticationError> +where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, +{ + let (scopes, db_user) = get_full_user_from_headers( + req, + executor, + redis, + session_queue, + required_scopes, + ) + .await?; + + Ok((scopes, User::from_full(db_user))) } pub async fn get_user_record_from_bearer_token<'a, 'b, E>( diff --git a/apps/labrinth/src/database/models/user_item.rs b/apps/labrinth/src/database/models/user_item.rs index 6a2e4aba6..4d447702d 100644 --- a/apps/labrinth/src/database/models/user_item.rs +++ b/apps/labrinth/src/database/models/user_item.rs @@ -49,6 +49,8 @@ pub struct DBUser { pub badges: Badges, pub allow_friend_requests: bool, + + pub is_subscribed_to_newsletter: bool, } impl DBUser { @@ -63,13 +65,13 @@ impl DBUser { avatar_url, raw_avatar_url, bio, created, github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id, email_verified, password, paypal_id, paypal_country, paypal_email, - venmo_handle, stripe_customer_id, allow_friend_requests + venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, - $14, $15, $16, $17, $18, $19, $20, $21 + $14, $15, $16, $17, $18, $19, $20, $21, $22 ) ", self.id as DBUserId, @@ -93,6 +95,7 @@ impl DBUser { self.venmo_handle, self.stripe_customer_id, self.allow_friend_requests, + self.is_subscribed_to_newsletter, ) .execute(&mut **transaction) .await?; @@ -178,7 +181,7 @@ impl DBUser { created, role, badges, github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id, email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email, - venmo_handle, stripe_customer_id, allow_friend_requests + venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter FROM users WHERE id = ANY($1) OR LOWER(username) = ANY($2) ", @@ -212,6 +215,7 @@ impl DBUser { stripe_customer_id: u.stripe_customer_id, totp_secret: u.totp_secret, allow_friend_requests: u.allow_friend_requests, + is_subscribed_to_newsletter: u.is_subscribed_to_newsletter, }; acc.insert(u.id, (Some(u.username), user)); diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 281f85be0..cba2de5d9 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1,5 +1,7 @@ use crate::auth::email::send_email; -use crate::auth::validate::get_user_record_from_bearer_token; +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; @@ -232,6 +234,7 @@ impl TempUser { role: Role::Developer.to_string(), badges: Badges::default(), allow_friend_requests: true, + is_subscribed_to_newsletter: false, } .insert(transaction) .await?; @@ -1291,37 +1294,6 @@ pub async fn delete_auth_provider( Ok(HttpResponse::NoContent().finish()) } -pub async fn sign_up_sendy(email: &str) -> Result<(), AuthenticationError> { - let url = dotenvy::var("SENDY_URL")?; - let id = dotenvy::var("SENDY_LIST_ID")?; - let api_key = dotenvy::var("SENDY_API_KEY")?; - let site_url = dotenvy::var("SITE_URL")?; - - if url.is_empty() || url == "none" { - tracing::info!("Sendy URL not set, skipping signup"); - return Ok(()); - } - - let mut form = HashMap::new(); - - form.insert("api_key", &*api_key); - form.insert("email", email); - form.insert("list", &*id); - form.insert("referrer", &*site_url); - - let client = reqwest::Client::new(); - client - .post(format!("{url}/subscribe")) - .form(&form) - .send() - .await? - .error_for_status()? - .text() - .await?; - - Ok(()) -} - pub async fn check_sendy_subscription( email: &str, ) -> Result { @@ -1456,6 +1428,9 @@ pub async fn create_account_with_password( 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?; @@ -1476,10 +1451,6 @@ pub async fn create_account_with_password( &format!("Welcome to Modrinth, {}!", new_account.username), )?; - if new_account.sign_up_newsletter.unwrap_or(false) { - sign_up_sendy(&new_account.email).await?; - } - transaction.commit().await?; Ok(HttpResponse::Ok().json(res)) @@ -2420,15 +2391,24 @@ pub async fn subscribe_newsletter( .await? .1; - if let Some(email) = user.email { - sign_up_sendy(&email).await?; + sqlx::query!( + " + UPDATE users + SET is_subscribed_to_newsletter = TRUE + WHERE id = $1 + ", + user.id.0 as i64, + ) + .execute(&**pool) + .await?; - Ok(HttpResponse::NoContent().finish()) - } else { - Err(ApiError::InvalidInput( - "User does not have an email.".to_string(), - )) - } + crate::database::models::DBUser::clear_caches( + &[(user.id.into(), None)], + &redis, + ) + .await?; + + Ok(HttpResponse::NoContent().finish()) } #[get("email/subscribe")] @@ -2438,7 +2418,7 @@ pub async fn get_newsletter_subscription_status( redis: Data, session_queue: Data, ) -> Result { - let user = get_user_from_headers( + let user = get_full_user_from_headers( &req, &**pool, &redis, @@ -2448,16 +2428,16 @@ pub async fn get_newsletter_subscription_status( .await? .1; - if let Some(email) = user.email { - let is_subscribed = check_sendy_subscription(&email).await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ - "subscribed": is_subscribed - }))) - } else { - Ok(HttpResponse::Ok().json(serde_json::json!({ - "subscribed": false - }))) - } + let is_subscribed = user.is_subscribed_to_newsletter + || if let Some(email) = user.email { + check_sendy_subscription(&email).await? + } else { + false + }; + + Ok(HttpResponse::Ok().json(serde_json::json!({ + "subscribed": is_subscribed + }))) } fn send_email_verify(