max / makenotwork
10 files changed,
+136 insertions,
-99 deletions
| @@ -7,13 +7,16 @@ use axum::{ | |||
| 7 | 7 | }; | |
| 8 | 8 | use serde::Deserialize; | |
| 9 | 9 | ||
| 10 | + | use sqlx::PgPool; | |
| 11 | + | ||
| 10 | 12 | use crate::{ | |
| 11 | 13 | auth::AuthUser, | |
| 14 | + | config::Config, | |
| 12 | 15 | constants, | |
| 13 | 16 | db, | |
| 17 | + | email::EmailClient, | |
| 14 | 18 | error::{AppError, Result}, | |
| 15 | 19 | templates::FormStatusTemplate, | |
| 16 | - | AppState, | |
| 17 | 20 | }; | |
| 18 | 21 | ||
| 19 | 22 | /// Form input for broadcasting to followers. | |
| @@ -26,7 +29,9 @@ pub struct BroadcastForm { | |||
| 26 | 29 | /// Send a plain-text broadcast email to all followers. | |
| 27 | 30 | #[tracing::instrument(skip_all, name = "users::broadcast_send")] | |
| 28 | 31 | pub(in crate::routes::api) async fn broadcast_send( | |
| 29 | - | State(state): State<AppState>, | |
| 32 | + | State(db): State<PgPool>, | |
| 33 | + | State(config): State<Config>, | |
| 34 | + | State(email): State<EmailClient>, | |
| 30 | 35 | AuthUser(user): AuthUser, | |
| 31 | 36 | Form(form): Form<BroadcastForm>, | |
| 32 | 37 | ) -> Result<Response> { | |
| @@ -59,7 +64,7 @@ pub(in crate::routes::api) async fn broadcast_send( | |||
| 59 | 64 | } | |
| 60 | 65 | ||
| 61 | 66 | // Rate limit: one broadcast per 24 hours | |
| 62 | - | if !db::users::try_set_broadcast_at(&state.db, user.id).await? { | |
| 67 | + | if !db::users::try_set_broadcast_at(&db, user.id).await? { | |
| 63 | 68 | return Ok(Html(FormStatusTemplate { | |
| 64 | 69 | success: false, | |
| 65 | 70 | message: "You can only send one broadcast per 24 hours".to_string(), | |
| @@ -68,12 +73,12 @@ pub(in crate::routes::api) async fn broadcast_send( | |||
| 68 | 73 | ||
| 69 | 74 | // Get follower emails, enforcing the broadcast recipient cap at the type | |
| 70 | 75 | // level — `BoundedRecipients::new` is the only way to obtain a sendable list. | |
| 71 | - | let followers = db::follows::get_follower_emails(&state.db, user.id).await?; | |
| 76 | + | let followers = db::follows::get_follower_emails(&db, user.id).await?; | |
| 72 | 77 | let recipients = match crate::email::BoundedRecipients::new(followers) { | |
| 73 | 78 | Ok(r) => r, | |
| 74 | 79 | Err(count) => { | |
| 75 | 80 | // Roll back the 24h rate-limit slot so the creator can try again after lifting the cap. | |
| 76 | - | let _ = db::users::clear_broadcast_at(&state.db, user.id).await; | |
| 81 | + | let _ = db::users::clear_broadcast_at(&db, user.id).await; | |
| 77 | 82 | return Ok(Html(FormStatusTemplate { | |
| 78 | 83 | success: false, | |
| 79 | 84 | message: format!( | |
| @@ -92,7 +97,7 @@ pub(in crate::routes::api) async fn broadcast_send( | |||
| 92 | 97 | } | |
| 93 | 98 | ||
| 94 | 99 | // Get creator name | |
| 95 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 100 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 96 | 101 | .await? | |
| 97 | 102 | .ok_or(AppError::NotFound)?; | |
| 98 | 103 | let creator_name = db_user.display_name.as_deref() | |
| @@ -103,9 +108,9 @@ pub(in crate::routes::api) async fn broadcast_send( | |||
| 103 | 108 | let body = body.to_string(); | |
| 104 | 109 | let creator_name = creator_name.to_string(); | |
| 105 | 110 | let creator_id = user.id; | |
| 106 | - | let email_client = state.email.clone(); | |
| 107 | - | let host_url = state.config.host_url.clone(); | |
| 108 | - | let signing_secret = state.config.signing_secret.clone(); | |
| 111 | + | let email_client = email.clone(); | |
| 112 | + | let host_url = config.host_url.clone(); | |
| 113 | + | let signing_secret = config.signing_secret.clone(); | |
| 109 | 114 | ||
| 110 | 115 | let followers = recipients.into_inner(); | |
| 111 | 116 | tokio::spawn(async move { |
| @@ -6,20 +6,21 @@ use axum::{ | |||
| 6 | 6 | response::IntoResponse, | |
| 7 | 7 | }; | |
| 8 | 8 | ||
| 9 | + | use sqlx::PgPool; | |
| 10 | + | ||
| 9 | 11 | use crate::{ | |
| 10 | 12 | auth::AuthUser, | |
| 11 | 13 | db::{self, UserId}, | |
| 12 | 14 | error::Result, | |
| 13 | - | AppState, | |
| 14 | 15 | }; | |
| 15 | 16 | ||
| 16 | 17 | /// Revoke contact sharing with a specific creator. | |
| 17 | 18 | #[tracing::instrument(skip_all, name = "users::revoke_contact")] | |
| 18 | 19 | pub(in crate::routes::api) async fn revoke_contact( | |
| 19 | - | State(state): State<AppState>, | |
| 20 | + | State(db): State<PgPool>, | |
| 20 | 21 | AuthUser(user): AuthUser, | |
| 21 | 22 | Path(seller_id): Path<UserId>, | |
| 22 | 23 | ) -> Result<impl IntoResponse> { | |
| 23 | - | db::transactions::revoke_contact_sharing(&state.db, user.id, seller_id).await?; | |
| 24 | + | db::transactions::revoke_contact_sharing(&db, user.id, seller_id).await?; | |
| 24 | 25 | Ok(StatusCode::NO_CONTENT) | |
| 25 | 26 | } |
| @@ -5,19 +5,20 @@ use axum::{ | |||
| 5 | 5 | response::IntoResponse, | |
| 6 | 6 | }; | |
| 7 | 7 | ||
| 8 | + | use sqlx::PgPool; | |
| 9 | + | ||
| 8 | 10 | use crate::{ | |
| 9 | 11 | auth::AuthUser, | |
| 10 | 12 | constants, | |
| 11 | 13 | db, | |
| 12 | 14 | error::{AppError, Result}, | |
| 13 | 15 | templates::AlertTemplate, | |
| 14 | - | AppState, | |
| 15 | 16 | }; | |
| 16 | 17 | ||
| 17 | 18 | /// Generate a new invite code for the authenticated creator. | |
| 18 | 19 | #[tracing::instrument(skip_all, name = "api::create_invite")] | |
| 19 | 20 | pub(in crate::routes::api) async fn create_invite( | |
| 20 | - | State(state): State<AppState>, | |
| 21 | + | State(db): State<PgPool>, | |
| 21 | 22 | AuthUser(user): AuthUser, | |
| 22 | 23 | ) -> Result<impl IntoResponse> { | |
| 23 | 24 | user.check_not_sandbox()?; | |
| @@ -26,7 +27,7 @@ pub(in crate::routes::api) async fn create_invite( | |||
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | // Only creators can generate invites | |
| 29 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 30 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 30 | 31 | .await? | |
| 31 | 32 | .ok_or(AppError::NotFound)?; | |
| 32 | 33 | ||
| @@ -35,7 +36,7 @@ pub(in crate::routes::api) async fn create_invite( | |||
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | 38 | // Check limit | |
| 38 | - | let active_count = db::invites::count_active_invites(&state.db, user.id).await?; | |
| 39 | + | let active_count = db::invites::count_active_invites(&db, user.id).await?; | |
| 39 | 40 | if active_count >= constants::INVITE_LIMIT_PER_CREATOR { | |
| 40 | 41 | return Ok(AlertTemplate::new( | |
| 41 | 42 | "error", | |
| @@ -45,7 +46,7 @@ pub(in crate::routes::api) async fn create_invite( | |||
| 45 | 46 | ||
| 46 | 47 | // Generate and store | |
| 47 | 48 | let code = db::invites::generate_invite_code(); | |
| 48 | - | db::invites::create_invite_code(&state.db, user.id, &code).await?; | |
| 49 | + | db::invites::create_invite_code(&db, user.id, &code).await?; | |
| 49 | 50 | ||
| 50 | 51 | let formatted = db::invites::format_invite_code(&code); | |
| 51 | 52 | let link = format!("makenot.work/join?invite={}", formatted); |
| @@ -8,13 +8,17 @@ use axum::{ | |||
| 8 | 8 | }; | |
| 9 | 9 | use serde::Serialize; | |
| 10 | 10 | ||
| 11 | + | use sqlx::PgPool; | |
| 12 | + | ||
| 11 | 13 | use crate::{ | |
| 12 | 14 | auth::AuthUser, | |
| 15 | + | background::BackgroundTx, | |
| 16 | + | config::Config, | |
| 13 | 17 | db::{self, ItemId}, | |
| 18 | + | email::EmailClient, | |
| 14 | 19 | error::{AppError, Result}, | |
| 15 | - | helpers::{self, htmx_toast_response, is_htmx_request, spawn_email}, | |
| 20 | + | helpers::{self, htmx_toast_response, is_htmx_request}, | |
| 16 | 21 | templates::{LibraryStatusTemplate, SaveStatusTemplate}, | |
| 17 | - | AppState, | |
| 18 | 22 | }; | |
| 19 | 23 | ||
| 20 | 24 | use super::SuccessMessageResponse; | |
| @@ -30,7 +34,10 @@ struct LibraryActionResponse { | |||
| 30 | 34 | /// Claim a free item and add it to the user's library. | |
| 31 | 35 | #[tracing::instrument(skip_all, name = "users::add_to_library")] | |
| 32 | 36 | pub(in crate::routes::api) async fn add_to_library( | |
| 33 | - | State(state): State<AppState>, | |
| 37 | + | State(db): State<PgPool>, | |
| 38 | + | State(config): State<Config>, | |
| 39 | + | State(email): State<EmailClient>, | |
| 40 | + | State(bg): State<BackgroundTx>, | |
| 34 | 41 | headers: HeaderMap, | |
| 35 | 42 | AuthUser(user): AuthUser, | |
| 36 | 43 | Path(item_id): Path<ItemId>, | |
| @@ -39,7 +46,7 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 39 | 46 | let is_htmx = is_htmx_request(&headers); | |
| 40 | 47 | ||
| 41 | 48 | // Get the item | |
| 42 | - | let item = db::items::get_item_by_id(&state.db, item_id) | |
| 49 | + | let item = db::items::get_item_by_id(&db, item_id) | |
| 43 | 50 | .await? | |
| 44 | 51 | .ok_or(AppError::NotFound)?; | |
| 45 | 52 | ||
| @@ -60,17 +67,17 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 60 | 67 | } | |
| 61 | 68 | ||
| 62 | 69 | // Get the project to find the seller | |
| 63 | - | let project = db::projects::get_project_by_id(&state.db, item.project_id) | |
| 70 | + | let project = db::projects::get_project_by_id(&db, item.project_id) | |
| 64 | 71 | .await? | |
| 65 | 72 | .ok_or(AppError::NotFound)?; | |
| 66 | 73 | ||
| 67 | 74 | // Get the seller's username for transaction record | |
| 68 | - | let seller = db::users::get_user_by_id(&state.db, project.user_id) | |
| 75 | + | let seller = db::users::get_user_by_id(&db, project.user_id) | |
| 69 | 76 | .await? | |
| 70 | 77 | .ok_or(AppError::NotFound)?; | |
| 71 | 78 | ||
| 72 | 79 | // Claim the free item + increment sales count atomically | |
| 73 | - | let mut tx = state.db.begin().await?; | |
| 80 | + | let mut tx = db.begin().await?; | |
| 74 | 81 | let claimed = db::transactions::claim_free_item( | |
| 75 | 82 | &mut *tx, | |
| 76 | 83 | &db::transactions::ClaimParams { | |
| @@ -93,7 +100,7 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 93 | 100 | // Grant access to bundle child items | |
| 94 | 101 | if claimed && item.item_type == db::ItemType::Bundle { | |
| 95 | 102 | crate::routes::stripe::grant_bundle_items( | |
| 96 | - | &state.db, item_id, user.id, project.user_id, None, | |
| 103 | + | &db, item_id, user.id, project.user_id, None, | |
| 97 | 104 | ) | |
| 98 | 105 | .await; | |
| 99 | 106 | } | |
| @@ -102,7 +109,7 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 102 | 109 | if claimed && item.enable_license_keys { | |
| 103 | 110 | let key_code = helpers::generate_key_code(); | |
| 104 | 111 | match db::license_keys::create_license_key( | |
| 105 | - | &state.db, | |
| 112 | + | &db, | |
| 106 | 113 | item_id, | |
| 107 | 114 | user.id, | |
| 108 | 115 | None, // free claim — no transaction linked | |
| @@ -122,7 +129,7 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 122 | 129 | ||
| 123 | 130 | // Notify seller of free claim (fire-and-forget) | |
| 124 | 131 | if claimed && seller.notify_sale { | |
| 125 | - | let buyer_user = db::users::get_user_by_id(&state.db, user.id).await.ok().flatten(); | |
| 132 | + | let buyer_user = db::users::get_user_by_id(&db, user.id).await.ok().flatten(); | |
| 126 | 133 | let buyer_username = buyer_user.as_ref() | |
| 127 | 134 | .map(|b| b.username.to_string()) | |
| 128 | 135 | .unwrap_or_else(|| "Someone".to_string()); | |
| @@ -130,17 +137,20 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 130 | 137 | let seller_email = seller.email.clone(); | |
| 131 | 138 | let seller_name = seller.display_name.clone(); | |
| 132 | 139 | let unsub_url = crate::email::generate_unsubscribe_url( | |
| 133 | - | &state.config.host_url, seller.id, crate::email::UnsubscribeAction::Sale, &seller.id.to_string(), &state.config.signing_secret, | |
| 140 | + | &config.host_url, seller.id, crate::email::UnsubscribeAction::Sale, &seller.id.to_string(), &config.signing_secret, | |
| 134 | 141 | ); | |
| 135 | - | spawn_email!(state, "sale notification", |email| { | |
| 136 | - | email.send_sale_notification( | |
| 142 | + | let email = email.clone(); | |
| 143 | + | bg.spawn("sale notification", async move { | |
| 144 | + | if let Err(e) = email.send_sale_notification( | |
| 137 | 145 | &seller_email, | |
| 138 | 146 | seller_name.as_deref(), | |
| 139 | 147 | &buyer_username, | |
| 140 | 148 | &item_title, | |
| 141 | 149 | "Free", | |
| 142 | 150 | Some(&unsub_url), | |
| 143 | - | ) | |
| 151 | + | ).await { | |
| 152 | + | tracing::error!(error = ?e, "failed to send sale notification"); | |
| 153 | + | } | |
| 144 | 154 | }); | |
| 145 | 155 | } | |
| 146 | 156 | ||
| @@ -159,7 +169,7 @@ pub(in crate::routes::api) async fn add_to_library( | |||
| 159 | 169 | /// Remove a free item from the user's library. | |
| 160 | 170 | #[tracing::instrument(skip_all, name = "users::remove_from_library")] | |
| 161 | 171 | pub(in crate::routes::api) async fn remove_from_library( | |
| 162 | - | State(state): State<AppState>, | |
| 172 | + | State(db): State<PgPool>, | |
| 163 | 173 | headers: HeaderMap, | |
| 164 | 174 | AuthUser(user): AuthUser, | |
| 165 | 175 | Path(item_id): Path<ItemId>, | |
| @@ -167,11 +177,11 @@ pub(in crate::routes::api) async fn remove_from_library( | |||
| 167 | 177 | let is_htmx = is_htmx_request(&headers); | |
| 168 | 178 | ||
| 169 | 179 | // Remove the free item from library | |
| 170 | - | let removed = db::transactions::remove_free_item_from_library(&state.db, user.id, item_id).await?; | |
| 180 | + | let removed = db::transactions::remove_free_item_from_library(&db, user.id, item_id).await?; | |
| 171 | 181 | ||
| 172 | 182 | // Decrement denormalized sales_count | |
| 173 | 183 | if removed { | |
| 174 | - | db::items::decrement_sales_count(&state.db, item_id).await?; | |
| 184 | + | db::items::decrement_sales_count(&db, item_id).await?; | |
| 175 | 185 | } | |
| 176 | 186 | ||
| 177 | 187 | if is_htmx { |
| @@ -7,12 +7,13 @@ use axum::{ | |||
| 7 | 7 | }; | |
| 8 | 8 | use serde::Deserialize; | |
| 9 | 9 | ||
| 10 | + | use sqlx::PgPool; | |
| 11 | + | ||
| 10 | 12 | use crate::{ | |
| 11 | 13 | auth::AuthUser, | |
| 12 | 14 | db, | |
| 13 | 15 | error::Result, | |
| 14 | 16 | templates::SaveStatusTemplate, | |
| 15 | - | AppState, | |
| 16 | 17 | }; | |
| 17 | 18 | ||
| 18 | 19 | /// Form input for notification preferences (checkbox values: "on" or absent). | |
| @@ -31,7 +32,7 @@ pub struct UpdatePreferencesForm { | |||
| 31 | 32 | /// Update the authenticated user's notification preferences. | |
| 32 | 33 | #[tracing::instrument(skip_all, name = "users::update_preferences")] | |
| 33 | 34 | pub(in crate::routes::api) async fn update_preferences( | |
| 34 | - | State(state): State<AppState>, | |
| 35 | + | State(db): State<PgPool>, | |
| 35 | 36 | AuthUser(user): AuthUser, | |
| 36 | 37 | Form(form): Form<UpdatePreferencesForm>, | |
| 37 | 38 | ) -> Result<Response> { | |
| @@ -44,8 +45,8 @@ pub(in crate::routes::api) async fn update_preferences( | |||
| 44 | 45 | let tips_enabled = form.tips_enabled.as_deref() == Some("on"); | |
| 45 | 46 | let notify_tip = form.notify_tip.as_deref() == Some("on"); | |
| 46 | 47 | ||
| 47 | - | db::users::update_notification_preferences(&state.db, user.id, notify_sale, notify_follower, notify_release, login_notification_enabled, notify_issues, notify_status).await?; | |
| 48 | - | db::users::update_tip_preferences(&state.db, user.id, tips_enabled, notify_tip).await?; | |
| 48 | + | db::users::update_notification_preferences(&db, user.id, notify_sale, notify_follower, notify_release, login_notification_enabled, notify_issues, notify_status).await?; | |
| 49 | + | db::users::update_tip_preferences(&db, user.id, tips_enabled, notify_tip).await?; | |
| 49 | 50 | ||
| 50 | 51 | Ok(Html(SaveStatusTemplate { | |
| 51 | 52 | success: true, |
| @@ -7,17 +7,20 @@ use axum::{ | |||
| 7 | 7 | Json, | |
| 8 | 8 | }; | |
| 9 | 9 | use serde::{Deserialize, Serialize}; | |
| 10 | + | use sqlx::PgPool; | |
| 10 | 11 | use tower_sessions::Session; | |
| 11 | 12 | ||
| 12 | 13 | use crate::{ | |
| 13 | 14 | auth::AuthUser, | |
| 15 | + | background::BackgroundTx, | |
| 16 | + | config::Config, | |
| 14 | 17 | db::{self, UserId, Username}, | |
| 15 | - | email, | |
| 18 | + | email::{self, EmailClient}, | |
| 16 | 19 | error::{AppError, Result, ResultExt}, | |
| 17 | 20 | helpers::is_htmx_request, | |
| 18 | 21 | templates::{AlertTemplate, FormStatusTemplate, SaveStatusTemplate}, | |
| 19 | 22 | validation, | |
| 20 | - | AppState, | |
| 23 | + | AppCaches, AppState, Billing, Integrations, | |
| 21 | 24 | }; | |
| 22 | 25 | ||
| 23 | 26 | use crate::extractors::ValidatedForm; | |
| @@ -42,7 +45,7 @@ pub struct UpdateProfileRequest { | |||
| 42 | 45 | /// Update the authenticated user's display name and/or bio. | |
| 43 | 46 | #[tracing::instrument(skip_all, name = "users::update_profile")] | |
| 44 | 47 | pub(in crate::routes::api) async fn update_profile( | |
| 45 | - | State(state): State<AppState>, | |
| 48 | + | State(db): State<PgPool>, | |
| 46 | 49 | headers: HeaderMap, | |
| 47 | 50 | AuthUser(user): AuthUser, | |
| 48 | 51 | ValidatedForm(req): ValidatedForm<UpdateProfileRequest>, | |
| @@ -73,7 +76,7 @@ pub(in crate::routes::api) async fn update_profile( | |||
| 73 | 76 | } | |
| 74 | 77 | ||
| 75 | 78 | let updated = db::users::update_user_profile( | |
| 76 | - | &state.db, | |
| 79 | + | &db, | |
| 77 | 80 | user.id, | |
| 78 | 81 | req.display_name.as_deref(), | |
| 79 | 82 | req.bio.as_deref(), | |
| @@ -105,7 +108,7 @@ pub struct UpdateThemeRequest { | |||
| 105 | 108 | /// Set the authenticated user's public-profile theme. | |
| 106 | 109 | #[tracing::instrument(skip_all, name = "users::update_profile_theme")] | |
| 107 | 110 | pub(in crate::routes::api) async fn update_profile_theme( | |
| 108 | - | State(state): State<AppState>, | |
| 111 | + | State(db): State<PgPool>, | |
| 109 | 112 | headers: HeaderMap, | |
| 110 | 113 | AuthUser(user): AuthUser, | |
| 111 | 114 | ValidatedForm(req): ValidatedForm<UpdateThemeRequest>, | |
| @@ -113,7 +116,7 @@ pub(in crate::routes::api) async fn update_profile_theme( | |||
| 113 | 116 | user.check_not_suspended()?; | |
| 114 | 117 | let theme_id = crate::theming::normalize_theme_id(req.theme_id.as_deref()) | |
| 115 | 118 | .map_err(|id| AppError::validation(format!("Unknown theme: {id}")))?; | |
| 116 | - | db::users::update_user_theme(&state.db, user.id, theme_id.as_deref()).await?; | |
| 119 | + | db::users::update_user_theme(&db, user.id, theme_id.as_deref()).await?; | |
| 117 | 120 | ||
| 118 | 121 | if is_htmx_request(&headers) { | |
| 119 | 122 | return Ok(Html(SaveStatusTemplate { | |
| @@ -134,7 +137,8 @@ pub struct UpdatePasswordRequest { | |||
| 134 | 137 | /// Change the authenticated user's password after verifying the current one. | |
| 135 | 138 | #[tracing::instrument(skip_all, name = "users::update_password")] | |
| 136 | 139 | pub(in crate::routes::api) async fn update_password( | |
| 137 | - | State(state): State<AppState>, | |
| 140 | + | State(db): State<PgPool>, | |
| 141 | + | State(caches): State<AppCaches>, | |
| 138 | 142 | headers: HeaderMap, | |
| 139 | 143 | session: Session, | |
| 140 | 144 | AuthUser(user): AuthUser, | |
| @@ -144,7 +148,7 @@ pub(in crate::routes::api) async fn update_password( | |||
| 144 | 148 | let is_htmx = is_htmx_request(&headers); | |
| 145 | 149 | ||
| 146 | 150 | // Get current user with password hash | |
| 147 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 151 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 148 | 152 | .await? | |
| 149 | 153 | .ok_or(AppError::NotFound)?; | |
| 150 | 154 | ||
| @@ -200,7 +204,7 @@ pub(in crate::routes::api) async fn update_password( | |||
| 200 | 204 | // by swapping in `delete_all_sessions_for_user`: that would also log the user | |
| 201 | 205 | // out of their current web session, and the JWT bump already happened here. | |
| 202 | 206 | let new_hash = crate::auth::hash_password_async(req.new_password.clone()).await?; | |
| 203 | - | db::users::update_user_password(&state.db, user.id, &new_hash).await?; | |
| 207 | + | db::users::update_user_password(&db, user.id, &new_hash).await?; | |
| 204 | 208 | ||
| 205 | 209 | // Invalidate all other sessions so stolen/leaked sessions can't survive a password change | |
| 206 | 210 | let current_tracking_id = session | |
| @@ -209,7 +213,7 @@ pub(in crate::routes::api) async fn update_password( | |||
| 209 | 213 | .ok() | |
| 210 | 214 | .flatten(); | |
| 211 | 215 | let revoked_ids = if let Some(current_id) = current_tracking_id { | |
| 212 | - | db::sessions::delete_other_sessions(&state.db, current_id, user.id).await? | |
| 216 | + | db::sessions::delete_other_sessions(&db, current_id, user.id).await? | |
| 213 | 217 | } else { | |
| 214 | 218 | // Legacy current session: no SESSION_TRACKING_KEY, so it has no | |
| 215 | 219 | // `user_sessions` row and the auth extractor never validates it against | |
| @@ -219,10 +223,10 @@ pub(in crate::routes::api) async fn update_password( | |||
| 219 | 223 | // the *tracked* branch, where it WOULD kill the current row) still | |
| 220 | 224 | // holds. Other untracked legacy sessions can't be targeted by id and | |
| 221 | 225 | // are left to expire. | |
| 222 | - | db::sessions::delete_all_sessions_for_user(&state.db, user.id).await? | |
| 226 | + | db::sessions::delete_all_sessions_for_user(&db, user.id).await? | |
| 223 | 227 | }; | |
| 224 | 228 | for id in &revoked_ids { | |
| 225 | - | state.caches.session_cache.remove(id); | |
| 229 | + | caches.session_cache.remove(id); | |
| 226 | 230 | } | |
| 227 | 231 | if !revoked_ids.is_empty() { | |
| 228 | 232 | tracing::info!(user_id = %user.id, revoked = revoked_ids.len(), event = "password_change_revoke_sessions", "Revoked other sessions on password change"); | |
| @@ -275,11 +279,11 @@ pub(in crate::routes::api) async fn delete_account( | |||
| 275 | 279 | /// Self-deactivate account (enter limbo state). | |
| 276 | 280 | #[tracing::instrument(skip_all, name = "users::deactivate_account")] | |
| 277 | 281 | pub(in crate::routes::api) async fn deactivate_account( | |
| 278 | - | State(state): State<AppState>, | |
| 282 | + | State(db): State<PgPool>, | |
| 279 | 283 | AuthUser(user): AuthUser, | |
| 280 | 284 | ) -> Result<impl IntoResponse> { | |
| 281 | 285 | user.check_not_sandbox()?; | |
| 282 | - | db::users::deactivate_user(&state.db, user.id).await?; | |
| 286 | + | db::users::deactivate_user(&db, user.id).await?; | |
| 283 | 287 | tracing::info!(user_id = %user.id, "user self-deactivated account"); | |
| 284 | 288 | Ok(StatusCode::NO_CONTENT) | |
| 285 | 289 | } | |
| @@ -287,10 +291,10 @@ pub(in crate::routes::api) async fn deactivate_account( | |||
| 287 | 291 | /// Reactivate a self-deactivated account. | |
| 288 | 292 | #[tracing::instrument(skip_all, name = "users::reactivate_account")] | |
| 289 | 293 | pub(in crate::routes::api) async fn reactivate_account( | |
| 290 | - | State(state): State<AppState>, | |
| 294 | + | State(db): State<PgPool>, | |
| 291 | 295 | AuthUser(user): AuthUser, | |
| 292 | 296 | ) -> Result<impl IntoResponse> { | |
| 293 | - | db::users::reactivate_user(&state.db, user.id).await?; | |
| 297 | + | db::users::reactivate_user(&db, user.id).await?; | |
| 294 | 298 | tracing::info!(user_id = %user.id, "user reactivated account"); | |
| 295 | 299 | Ok(StatusCode::NO_CONTENT) | |
| 296 | 300 | } | |
| @@ -300,12 +304,15 @@ pub(in crate::routes::api) async fn reactivate_account( | |||
| 300 | 304 | /// and blocks new purchases. Content remains hosted indefinitely. | |
| 301 | 305 | #[tracing::instrument(skip_all, name = "users::pause_creator")] | |
| 302 | 306 | pub(in crate::routes::api) async fn pause_creator( | |
| 303 | - | State(state): State<AppState>, | |
| 307 | + | State(db): State<PgPool>, | |
| 308 | + | State(payments): State<Billing>, | |
| 309 | + | State(bg): State<BackgroundTx>, | |
| 310 | + | State(integrations): State<Integrations>, | |
| 304 | 311 | AuthUser(user): AuthUser, | |
| 305 | 312 | ) -> Result<impl IntoResponse> { | |
| 306 | 313 | user.check_not_sandbox()?; | |
| 307 | 314 | ||
| 308 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 315 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 309 | 316 | .await? | |
| 310 | 317 | .ok_or(AppError::NotFound)?; | |
| 311 | 318 | ||
| @@ -322,9 +329,9 @@ pub(in crate::routes::api) async fn pause_creator( | |||
| 322 | 329 | return Err(AppError::BadRequest("Only creators can pause their account".to_string())); | |
| 323 | 330 | } | |
| 324 | 331 | ||
| 325 | - | if let Some(ref stripe) = state.stripe { | |
| 332 | + | if let Some(ref stripe) = payments.stripe { | |
| 326 | 333 | // Cancel the creator's own tier subscription on Stripe (platform-level) | |
| 327 | - | if let Some(ct_sub) = db::creator_tiers::get_creator_sub_by_user(&state.db, user.id).await? | |
| 334 | + | if let Some(ct_sub) = db::creator_tiers::get_creator_sub_by_user(&db, user.id).await? | |
| 328 | 335 | && ct_sub.status == db::SubscriptionStatus::Active | |
| 329 | 336 | && let Err(e) = stripe.cancel_platform_subscription(&ct_sub.stripe_subscription_id).await | |
| 330 | 337 | { | |
| @@ -335,21 +342,21 @@ pub(in crate::routes::api) async fn pause_creator( | |||
| 335 | 342 | // account). Fanned out on the background queue so a creator with many | |
| 336 | 343 | // fans doesn't tie the request up for N serial Stripe round-trips. | |
| 337 | 344 | if let Some(ref stripe_account_id) = db_user.stripe_account_id { | |
| 338 | - | let fan_subs = db::subscriptions::get_active_subscriptions_by_creator(&state.db, user.id).await?; | |
| 345 | + | let fan_subs = db::subscriptions::get_active_subscriptions_by_creator(&db, user.id).await?; | |
| 339 | 346 | let ids = fan_subs.into_iter().map(|s| s.stripe_subscription_id).collect(); | |
| 340 | 347 | crate::payments::fan_ops::spawn_fan_sub_fanout( | |
| 341 | - | &state.bg, | |
| 348 | + | &bg, | |
| 342 | 349 | std::sync::Arc::clone(stripe), | |
| 343 | 350 | stripe_account_id.clone(), | |
| 344 | 351 | ids, | |
| 345 | 352 | crate::payments::fan_ops::FanSubOp::CancelAtPeriodEnd(true), | |
| 346 | - | state.wam.clone(), | |
| 353 | + | integrations.wam.clone(), | |
| 347 | 354 | ); | |
| 348 | 355 | } | |
| 349 | 356 | } | |
| 350 | 357 | ||
| 351 | 358 | // Set the pause timestamp | |
| 352 | - | db::users::pause_creator(&state.db, user.id).await?; | |
| 359 | + | db::users::pause_creator(&db, user.id).await?; | |
| 353 | 360 | tracing::info!(user_id = %user.id, "creator paused account"); | |
| 354 | 361 | ||
| 355 | 362 | Ok(StatusCode::NO_CONTENT) | |
| @@ -358,25 +365,27 @@ pub(in crate::routes::api) async fn pause_creator( | |||
| 358 | 365 | /// Disconnect the authenticated user's Stripe account. | |
| 359 | 366 | #[tracing::instrument(skip_all, name = "users::disconnect_stripe")] | |
| 360 | 367 | pub(in crate::routes::api) async fn disconnect_stripe( | |
| 361 | - | State(state): State<AppState>, | |
| 368 | + | State(db): State<PgPool>, | |
| 362 | 369 | AuthUser(user): AuthUser, | |
| 363 | 370 | ) -> Result<impl IntoResponse> { | |
| 364 | 371 | user.check_not_suspended()?; | |
| 365 | - | db::users::disconnect_user_stripe(&state.db, user.id).await?; | |
| 372 | + | db::users::disconnect_user_stripe(&db, user.id).await?; | |
| 366 | 373 | Ok(StatusCode::NO_CONTENT) | |
| 367 | 374 | } | |
| 368 | 375 | ||
| 369 | 376 | /// Resend the email verification link to the authenticated user. | |
| 370 | 377 | #[tracing::instrument(skip_all, name = "users::resend_verification")] | |
| 371 | 378 | pub(in crate::routes::api) async fn resend_verification( | |
| 372 | - | State(state): State<AppState>, | |
| 379 | + | State(db): State<PgPool>, | |
| 380 | + | State(config): State<Config>, | |
| 381 | + | State(email): State<EmailClient>, | |
| 373 | 382 | headers: HeaderMap, | |
| 374 | 383 | AuthUser(user): AuthUser, | |
| 375 | 384 | ) -> Result<Response> { | |
| 376 | 385 | let is_htmx = is_htmx_request(&headers); | |
| 377 | 386 | ||
| 378 | 387 | // Get full user data | |
| 379 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 388 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 380 | 389 | .await? | |
| 381 | 390 | .ok_or(AppError::NotFound)?; | |
| 382 | 391 | ||
| @@ -393,14 +402,14 @@ pub(in crate::routes::api) async fn resend_verification( | |||
| 393 | 402 | ||
| 394 | 403 | // Generate verification URL | |
| 395 | 404 | let verify_url = email::generate_verification_url( | |
| 396 | - | &state.config.host_url, | |
| 405 | + | &config.host_url, | |
| 397 | 406 | user.id, | |
| 398 | 407 | &db_user.email, | |
| 399 | - | &state.config.signing_secret, | |
| 408 | + | &config.signing_secret, | |
| 400 | 409 | ); | |
| 401 | 410 | ||
| 402 | 411 | // Send verification email | |
| 403 | - | if let Err(e) = state.email | |
| 412 | + | if let Err(e) = email | |
| 404 | 413 | .send_verification(&db_user.email, db_user.display_name.as_deref(), &verify_url) | |
| 405 | 414 | .await | |
| 406 | 415 | { | |
| @@ -432,7 +441,9 @@ pub struct RequestDeletionForm { | |||
| 432 | 441 | /// Send an account deletion confirmation email after verifying the username. | |
| 433 | 442 | #[tracing::instrument(skip_all, name = "users::request_account_deletion")] | |
| 434 | 443 | pub(in crate::routes::api) async fn request_account_deletion( | |
| 435 | - | State(state): State<AppState>, | |
| 444 | + | State(db): State<PgPool>, | |
| 445 | + | State(config): State<Config>, | |
| 446 | + | State(email): State<EmailClient>, | |
| 436 | 447 | headers: HeaderMap, | |
| 437 | 448 | AuthUser(user): AuthUser, | |
| 438 | 449 | ValidatedForm(form): ValidatedForm<RequestDeletionForm>, | |
| @@ -441,7 +452,7 @@ pub(in crate::routes::api) async fn request_account_deletion( | |||
| 441 | 452 | let is_htmx = is_htmx_request(&headers); | |
| 442 | 453 | ||
| 443 | 454 | // Get user from DB | |
| 444 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 455 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 445 | 456 | .await? | |
| 446 | 457 | .ok_or(AppError::NotFound)?; | |
| 447 | 458 | ||
| @@ -458,14 +469,14 @@ pub(in crate::routes::api) async fn request_account_deletion( | |||
| 458 | 469 | ||
| 459 | 470 | // Generate deletion URL | |
| 460 | 471 | let delete_url = email::generate_deletion_url( | |
| 461 | - | &state.config.host_url, | |
| 472 | + | &config.host_url, | |
| 462 | 473 | user.id, | |
| 463 | 474 | &db_user.email, | |
| 464 | - | &state.config.signing_secret, | |
| 475 | + | &config.signing_secret, | |
| 465 | 476 | ); | |
| 466 | 477 | ||
| 467 | 478 | // Send deletion confirmation email | |
| 468 | - | if let Err(e) = state.email | |
| 479 | + | if let Err(e) = email | |
| 469 | 480 | .send_deletion_confirmation(&db_user.email, db_user.display_name.as_deref(), &delete_url) | |
| 470 | 481 | .await | |
| 471 | 482 | { | |
| @@ -503,7 +514,7 @@ pub struct AppealForm { | |||
| 503 | 514 | /// Submit an appeal for a suspended account. | |
| 504 | 515 | #[tracing::instrument(skip_all, name = "users::submit_appeal")] | |
| 505 | 516 | pub(in crate::routes::api) async fn submit_appeal( | |
| 506 | - | State(state): State<AppState>, | |
| 517 | + | State(db): State<PgPool>, | |
| 507 | 518 | headers: HeaderMap, | |
| 508 | 519 | AuthUser(user): AuthUser, | |
| 509 | 520 | ValidatedForm(form): ValidatedForm<AppealForm>, | |
| @@ -519,7 +530,7 @@ pub(in crate::routes::api) async fn submit_appeal( | |||
| 519 | 530 | } | |
| 520 | 531 | ||
| 521 | 532 | // Reject re-submission if a recent denial exists (within 30 days) | |
| 522 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 533 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 523 | 534 | .await? | |
| 524 | 535 | .ok_or(AppError::NotFound)?; | |
| 525 | 536 | if db_user.appeal_decision.as_deref() == Some("denied") | |
| @@ -550,7 +561,7 @@ pub(in crate::routes::api) async fn submit_appeal( | |||
| 550 | 561 | return Err(AppError::validation("Appeal must be between 1 and 2000 characters".to_string())); | |
| 551 | 562 | } | |
| 552 | 563 | ||
| 553 | - | db::users::submit_appeal(&state.db, user.id, appeal_text).await?; | |
| 564 | + | db::users::submit_appeal(&db, user.id, appeal_text).await?; | |
| 554 | 565 | ||
| 555 | 566 | tracing::info!(user_id = %user.id, "suspension appeal submitted"); | |
| 556 | 567 |
| @@ -6,18 +6,21 @@ use axum::{ | |||
| 6 | 6 | }; | |
| 7 | 7 | use tower_sessions::Session; | |
| 8 | 8 | ||
| 9 | + | use sqlx::PgPool; | |
| 10 | + | ||
| 9 | 11 | use crate::{ | |
| 10 | 12 | auth::{AuthUser, SESSION_TRACKING_KEY}, | |
| 11 | 13 | db::{self, UserSessionId}, | |
| 12 | 14 | error::{AppError, Result}, | |
| 13 | 15 | templates::UserSessionsPartialTemplate, | |
| 14 | - | AppState, | |
| 16 | + | AppCaches, | |
| 15 | 17 | }; | |
| 16 | 18 | ||
| 17 | 19 | /// Revoke a single session (sign out another device). | |
| 18 | 20 | #[tracing::instrument(skip_all, name = "api::revoke_session")] | |
| 19 | 21 | pub(in crate::routes::api) async fn revoke_session( | |
| 20 | - | State(state): State<AppState>, | |
| 22 | + | State(db): State<PgPool>, | |
| 23 | + | State(caches): State<AppCaches>, | |
| 21 | 24 | session: Session, | |
| 22 | 25 | AuthUser(user): AuthUser, | |
| 23 | 26 | Path(session_id): Path<UserSessionId>, | |
| @@ -31,11 +34,11 @@ pub(in crate::routes::api) async fn revoke_session( | |||
| 31 | 34 | )); | |
| 32 | 35 | } | |
| 33 | 36 | ||
| 34 | - | db::sessions::delete_user_session(&state.db, session_id, user.id).await?; | |
| 35 | - | state.caches.session_cache.remove(&session_id); | |
| 37 | + | db::sessions::delete_user_session(&db, session_id, user.id).await?; | |
| 38 | + | caches.session_cache.remove(&session_id); | |
| 36 | 39 | ||
| 37 | 40 | // Re-render the sessions list | |
| 38 | - | let sessions = db::sessions::get_user_sessions(&state.db, user.id).await?; | |
| 41 | + | let sessions = db::sessions::get_user_sessions(&db, user.id).await?; | |
| 39 | 42 | let current_tracking_id = session | |
| 40 | 43 | .get::<UserSessionId>(SESSION_TRACKING_KEY) | |
| 41 | 44 | .await | |
| @@ -51,7 +54,8 @@ pub(in crate::routes::api) async fn revoke_session( | |||
| 51 | 54 | /// Revoke all sessions except the current one. | |
| 52 | 55 | #[tracing::instrument(skip_all, name = "api::revoke_other_sessions")] | |
| 53 | 56 | pub(in crate::routes::api) async fn revoke_other_sessions( | |
| 54 | - | State(state): State<AppState>, | |
| 57 | + | State(db): State<PgPool>, | |
| 58 | + | State(caches): State<AppCaches>, | |
| 55 | 59 | session: Session, | |
| 56 | 60 | AuthUser(user): AuthUser, | |
| 57 | 61 | ) -> Result<impl IntoResponse> { | |
| @@ -62,15 +66,15 @@ pub(in crate::routes::api) async fn revoke_other_sessions( | |||
| 62 | 66 | .flatten(); | |
| 63 | 67 | ||
| 64 | 68 | if let Some(current_id) = current_tracking_id { | |
| 65 | - | let revoked_ids = db::sessions::delete_other_sessions(&state.db, current_id, user.id).await?; | |
| 69 | + | let revoked_ids = db::sessions::delete_other_sessions(&db, current_id, user.id).await?; | |
| 66 | 70 | for id in &revoked_ids { | |
| 67 | - | state.caches.session_cache.remove(id); | |
| 71 | + | caches.session_cache.remove(id); | |
| 68 | 72 | } | |
| 69 | 73 | tracing::info!(user_id = %user.id, revoked = revoked_ids.len(), event = "revoke_other_sessions", "Revoked other sessions"); | |
| 70 | 74 | } | |
| 71 | 75 | ||
| 72 | 76 | // Re-render the sessions list | |
| 73 | - | let sessions = db::sessions::get_user_sessions(&state.db, user.id).await?; | |
| 77 | + | let sessions = db::sessions::get_user_sessions(&db, user.id).await?; | |
| 74 | 78 | ||
| 75 | 79 | Ok(UserSessionsPartialTemplate { | |
| 76 | 80 | sessions, |
| @@ -7,12 +7,13 @@ use axum::{ | |||
| 7 | 7 | }; | |
| 8 | 8 | use serde::Deserialize; | |
| 9 | 9 | ||
| 10 | + | use sqlx::PgPool; | |
| 11 | + | ||
| 10 | 12 | use crate::{ | |
| 11 | 13 | auth::AuthUser, | |
| 12 | 14 | db, | |
| 13 | 15 | error::Result, | |
| 14 | 16 | templates::SaveStatusTemplate, | |
| 15 | - | AppState, | |
| 16 | 17 | }; | |
| 17 | 18 | ||
| 18 | 19 | #[derive(Debug, Deserialize)] | |
| @@ -23,13 +24,13 @@ pub struct StripeTaxForm { | |||
| 23 | 24 | /// Toggle Stripe Tax for the authenticated creator. | |
| 24 | 25 | #[tracing::instrument(skip_all, name = "users::toggle_stripe_tax")] | |
| 25 | 26 | pub(in crate::routes::api) async fn toggle_stripe_tax( | |
| 26 | - | State(state): State<AppState>, | |
| 27 | + | State(db): State<PgPool>, | |
| 27 | 28 | AuthUser(user): AuthUser, | |
| 28 | 29 | Form(form): Form<StripeTaxForm>, | |
| 29 | 30 | ) -> Result<Response> { | |
| 30 | 31 | let enabled = form.stripe_tax_enabled.as_deref() == Some("on"); | |
| 31 | 32 | ||
| 32 | - | db::users::update_stripe_tax_enabled(&state.db, user.id, enabled).await?; | |
| 33 | + | db::users::update_stripe_tax_enabled(&db, user.id, enabled).await?; | |
| 33 | 34 | ||
| 34 | 35 | Ok(Html(SaveStatusTemplate { | |
| 35 | 36 | success: true, |
| @@ -9,9 +9,10 @@ use serde::Deserialize; | |||
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | 11 | auth::AuthUser, | |
| 12 | + | email::EmailClient, | |
| 12 | 13 | error::Result, | |
| 13 | 14 | templates::FormStatusTemplate, | |
| 14 | - | AppState, | |
| 15 | + | Integrations, | |
| 15 | 16 | }; | |
| 16 | 17 | ||
| 17 | 18 | /// Form input for submitting a support ticket. | |
| @@ -25,7 +26,8 @@ pub struct SupportTicketForm { | |||
| 25 | 26 | /// Submit a support ticket. Creates a WAM ticket and sends a confirmation email. | |
| 26 | 27 | #[tracing::instrument(skip_all, name = "users::submit_support_ticket")] | |
| 27 | 28 | pub(in crate::routes::api) async fn submit_support_ticket( | |
| 28 | - | State(state): State<AppState>, | |
| 29 | + | State(email): State<EmailClient>, | |
| 30 | + | State(integrations): State<Integrations>, | |
| 29 | 31 | AuthUser(user): AuthUser, | |
| 30 | 32 | Form(form): Form<SupportTicketForm>, | |
| 31 | 33 | ) -> Result<Response> { | |
| @@ -63,7 +65,7 @@ pub(in crate::routes::api) async fn submit_support_ticket( | |||
| 63 | 65 | ); | |
| 64 | 66 | ||
| 65 | 67 | // Create WAM ticket for triage | |
| 66 | - | if let Some(ref wam) = state.wam { | |
| 68 | + | if let Some(ref wam) = integrations.wam { | |
| 67 | 69 | let title = format!("[support] {}", subject); | |
| 68 | 70 | wam.create_ticket( | |
| 69 | 71 | &title, | |
| @@ -94,7 +96,7 @@ Makenotwork Support"#, | |||
| 94 | 96 | category, | |
| 95 | 97 | message, | |
| 96 | 98 | ); | |
| 97 | - | if let Err(e) = state.email.send_alert( | |
| 99 | + | if let Err(e) = email.send_alert( | |
| 98 | 100 | &user.email, | |
| 99 | 101 | &format!("We received your request: {}", subject), | |
| 100 | 102 | &confirmation, | |
| @@ -107,7 +109,7 @@ Makenotwork Support"#, | |||
| 107 | 109 | "New support ticket from {} ({}):\n\nSubject: {}\nCategory: {}\nUser ID: {}\n\n{}", | |
| 108 | 110 | user.username, user.email, subject, category, user.id, message, | |
| 109 | 111 | ); | |
| 110 | - | if let Err(e) = state.email.send_alert( | |
| 112 | + | if let Err(e) = email.send_alert( | |
| 111 | 113 | "info@makenot.work", | |
| 112 | 114 | &format!("[support] {}", subject), | |
| 113 | 115 | &support_body, |
| @@ -8,6 +8,8 @@ use axum::{ | |||
| 8 | 8 | }; | |
| 9 | 9 | use serde::Deserialize; | |
| 10 | 10 | ||
| 11 | + | use sqlx::PgPool; | |
| 12 | + | ||
| 11 | 13 | use crate::{ | |
| 12 | 14 | auth::AuthUser, | |
| 13 | 15 | db, | |
| @@ -15,7 +17,6 @@ use crate::{ | |||
| 15 | 17 | helpers::is_htmx_request, | |
| 16 | 18 | templates::AlertTemplate, | |
| 17 | 19 | validation, | |
| 18 | - | AppState, | |
| 19 | 20 | }; | |
| 20 | 21 | ||
| 21 | 22 | /// Form input for applying to the creator waitlist. | |
| @@ -39,7 +40,7 @@ pub struct WaitlistApplyForm { | |||
| 39 | 40 | /// Submit an application to the creator waitlist. | |
| 40 | 41 | #[tracing::instrument(skip_all, name = "users::waitlist_apply")] | |
| 41 | 42 | pub(in crate::routes::api) async fn waitlist_apply( | |
| 42 | - | State(state): State<AppState>, | |
| 43 | + | State(db): State<PgPool>, | |
| 43 | 44 | headers: HeaderMap, | |
| 44 | 45 | AuthUser(user): AuthUser, | |
| 45 | 46 | Form(form): Form<WaitlistApplyForm>, | |
| @@ -55,7 +56,7 @@ pub(in crate::routes::api) async fn waitlist_apply( | |||
| 55 | 56 | } | |
| 56 | 57 | ||
| 57 | 58 | // Check verified email | |
| 58 | - | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 59 | + | let db_user = db::users::get_user_by_id(&db, user.id) | |
| 59 | 60 | .await? | |
| 60 | 61 | .ok_or(AppError::NotFound)?; | |
| 61 | 62 | ||
| @@ -67,7 +68,7 @@ pub(in crate::routes::api) async fn waitlist_apply( | |||
| 67 | 68 | } | |
| 68 | 69 | ||
| 69 | 70 | // Check if already applied | |
| 70 | - | if db::waitlist::get_waitlist_entry_by_user(&state.db, user.id).await?.is_some() { | |
| 71 | + | if db::waitlist::get_waitlist_entry_by_user(&db, user.id).await?.is_some() { | |
| 71 | 72 | if is_htmx { | |
| 72 | 73 | return Ok(AlertTemplate::new("info", "You've already applied.").into_response()); | |
| 73 | 74 | } | |
| @@ -93,7 +94,7 @@ pub(in crate::routes::api) async fn waitlist_apply( | |||
| 93 | 94 | } | |
| 94 | 95 | ||
| 95 | 96 | // Create entry | |
| 96 | - | db::waitlist::create_waitlist_entry(&state.db, user.id, &full_pitch).await?; | |
| 97 | + | db::waitlist::create_waitlist_entry(&db, user.id, &full_pitch).await?; | |
| 97 | 98 | ||
| 98 | 99 | tracing::info!(user_id = %user.id, "waitlist application submitted"); | |
| 99 | 100 |