max / makenotwork
41 files changed,
+410 insertions,
-421 deletions
| @@ -3445,7 +3445,7 @@ dependencies = [ | |||
| 3445 | 3445 | ||
| 3446 | 3446 | [[package]] | |
| 3447 | 3447 | name = "makenotwork" | |
| 3448 | - | version = "0.5.10" | |
| 3448 | + | version = "0.5.11" | |
| 3449 | 3449 | dependencies = [ | |
| 3450 | 3450 | "anyhow", | |
| 3451 | 3451 | "argon2", |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "makenotwork" | |
| 3 | - | version = "0.5.11" | |
| 3 | + | version = "0.5.12" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "LICENSE" | |
| 6 | 6 |
| @@ -528,19 +528,18 @@ fn strip_ansi_escapes(s: &str) -> String { | |||
| 528 | 528 | let mut chars = s.chars(); | |
| 529 | 529 | while let Some(c) = chars.next() { | |
| 530 | 530 | if c == '\x1b' { | |
| 531 | - | // Consume the '[' and then any parameter/intermediate bytes up to | |
| 532 | - | // the final byte (an ASCII letter). | |
| 533 | - | if let Some(next) = chars.next() { | |
| 534 | - | if next == '[' { | |
| 535 | - | // CSI sequence: skip until we hit a letter (0x40..=0x7E). | |
| 536 | - | for tail in chars.by_ref() { | |
| 537 | - | if tail.is_ascii_alphabetic() { | |
| 538 | - | break; | |
| 539 | - | } | |
| 531 | + | // Consume the next char; if it's '[' we have a CSI sequence | |
| 532 | + | // and we skip parameter/intermediate bytes up to the final byte. | |
| 533 | + | // Otherwise (OSC / other sequences) just drop the two-char escape. | |
| 534 | + | if let Some(next) = chars.next() | |
| 535 | + | && next == '[' | |
| 536 | + | { | |
| 537 | + | // CSI sequence: skip until we hit a letter (0x40..=0x7E). | |
| 538 | + | for tail in chars.by_ref() { | |
| 539 | + | if tail.is_ascii_alphabetic() { | |
| 540 | + | break; | |
| 540 | 541 | } | |
| 541 | 542 | } | |
| 542 | - | // OSC / other sequences starting with ESC but not '[' — | |
| 543 | - | // just drop the two-char escape and continue. | |
| 544 | 543 | } | |
| 545 | 544 | } else { | |
| 546 | 545 | result.push(c); |
| @@ -149,7 +149,7 @@ pub async fn discover_items( | |||
| 149 | 149 | ) -> Result<Vec<DbDiscoverItemRow>> { | |
| 150 | 150 | let search_term = normalize_search(filters.search); | |
| 151 | 151 | let has_search = search_term.is_some(); | |
| 152 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 152 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 153 | 153 | ||
| 154 | 154 | // Build the base query with optional similarity score. | |
| 155 | 155 | // For short queries (1-2 chars) use a constant match_score since trigram | |
| @@ -274,7 +274,7 @@ pub async fn count_discover_items( | |||
| 274 | 274 | ) -> Result<i64> { | |
| 275 | 275 | let search_term = normalize_search(filters.search); | |
| 276 | 276 | let has_search = search_term.is_some(); | |
| 277 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 277 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 278 | 278 | ||
| 279 | 279 | let mut query = String::from( | |
| 280 | 280 | r#" | |
| @@ -316,7 +316,7 @@ pub async fn discover_projects( | |||
| 316 | 316 | ) -> Result<Vec<DbDiscoverProjectRow>> { | |
| 317 | 317 | let search_term = normalize_search(search); | |
| 318 | 318 | let has_search = search_term.is_some(); | |
| 319 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 319 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 320 | 320 | ||
| 321 | 321 | let mut query = if has_search && !short_query { | |
| 322 | 322 | String::from( | |
| @@ -437,7 +437,7 @@ pub async fn count_discover_projects( | |||
| 437 | 437 | ) -> Result<i64> { | |
| 438 | 438 | let search_term = normalize_search(search); | |
| 439 | 439 | let has_search = search_term.is_some(); | |
| 440 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 440 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 441 | 441 | ||
| 442 | 442 | let mut query = String::from( | |
| 443 | 443 | r#" | |
| @@ -486,7 +486,7 @@ pub async fn get_item_type_counts( | |||
| 486 | 486 | ) -> Result<Vec<DbItemTypeCount>> { | |
| 487 | 487 | let search_term = normalize_search(search); | |
| 488 | 488 | let has_search = search_term.is_some(); | |
| 489 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 489 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 490 | 490 | ||
| 491 | 491 | let mut query = String::from( | |
| 492 | 492 | r#" | |
| @@ -552,7 +552,7 @@ pub async fn get_price_range_counts( | |||
| 552 | 552 | ) -> Result<DbPriceRangeCounts> { | |
| 553 | 553 | let search_term = normalize_search(search); | |
| 554 | 554 | let has_search = search_term.is_some(); | |
| 555 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 555 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 556 | 556 | ||
| 557 | 557 | let mut query = String::from( | |
| 558 | 558 | r#" | |
| @@ -627,7 +627,7 @@ pub async fn get_ai_tier_counts( | |||
| 627 | 627 | ) -> Result<Vec<DbItemTypeCount>> { | |
| 628 | 628 | let search_term = normalize_search(search); | |
| 629 | 629 | let has_search = search_term.is_some(); | |
| 630 | - | let short_query = search_term.as_deref().map_or(false, is_short_query); | |
| 630 | + | let short_query = search_term.as_deref().is_some_and(is_short_query); | |
| 631 | 631 | ||
| 632 | 632 | let mut query = String::from( | |
| 633 | 633 | r#" |
| @@ -11,6 +11,7 @@ use crate::error::Result; | |||
| 11 | 11 | /// | |
| 12 | 12 | /// Auto-generates a URL-safe slug from the title. If the slug collides with | |
| 13 | 13 | /// an existing item in the same project, appends a counter suffix. | |
| 14 | + | #[allow(clippy::too_many_arguments)] | |
| 14 | 15 | #[tracing::instrument(skip_all)] | |
| 15 | 16 | pub async fn create_item( | |
| 16 | 17 | pool: &PgPool, |
| @@ -31,7 +31,7 @@ pub async fn enqueue_deletions( | |||
| 31 | 31 | ) | |
| 32 | 32 | .bind(&s3_keys) | |
| 33 | 33 | .bind(&buckets) | |
| 34 | - | .bind(&vec![source; keys.len()]) | |
| 34 | + | .bind(vec![source; keys.len()]) | |
| 35 | 35 | .execute(pool) | |
| 36 | 36 | .await?; | |
| 37 | 37 | Ok(()) |
| @@ -1016,6 +1016,7 @@ pub async fn cleanup_stale_pending( | |||
| 1016 | 1016 | /// Create a completed free guest transaction. | |
| 1017 | 1017 | /// | |
| 1018 | 1018 | /// Returns the number of rows inserted (0 if already claimed via ON CONFLICT). | |
| 1019 | + | #[allow(clippy::too_many_arguments)] | |
| 1019 | 1020 | #[tracing::instrument(skip_all)] | |
| 1020 | 1021 | pub async fn create_free_guest_transaction( | |
| 1021 | 1022 | pool: &PgPool, |
| @@ -683,6 +683,7 @@ pub async fn get_all_user_emails(pool: &PgPool) -> Result<Vec<(String, Option<St | |||
| 683 | 683 | } | |
| 684 | 684 | ||
| 685 | 685 | /// Update a user's email notification preferences. | |
| 686 | + | #[allow(clippy::too_many_arguments)] | |
| 686 | 687 | #[tracing::instrument(skip_all)] | |
| 687 | 688 | pub async fn update_notification_preferences( | |
| 688 | 689 | pool: &PgPool, |
| @@ -28,12 +28,11 @@ pub fn resolve_ref(repo: &Repository, refname: &str) -> Result<Oid, GitError> { | |||
| 28 | 28 | ||
| 29 | 29 | // Reject arbitrary revparse expressions (e.g. HEAD~99999, @{upstream}). | |
| 30 | 30 | // Only allow simple ref-like names: alphanumeric, dots, hyphens, underscores, slashes. | |
| 31 | - | if refname.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '/')) { | |
| 32 | - | if let Ok(obj) = repo.revparse_single(refname) | |
| 33 | - | && let Ok(commit) = obj.peel(ObjectType::Commit) | |
| 34 | - | { | |
| 35 | - | return Ok(commit.id()); | |
| 36 | - | } | |
| 31 | + | if refname.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '/')) | |
| 32 | + | && let Ok(obj) = repo.revparse_single(refname) | |
| 33 | + | && let Ok(commit) = obj.peel(ObjectType::Commit) | |
| 34 | + | { | |
| 35 | + | return Ok(commit.id()); | |
| 37 | 36 | } | |
| 38 | 37 | ||
| 39 | 38 | Err(GitError::RefNotFound) |
| @@ -185,7 +185,7 @@ async fn main() { | |||
| 185 | 185 | ], | |
| 186 | 186 | link_prefix: "/docs".to_string(), | |
| 187 | 187 | unpublished_pattern: Some("unpublished/".to_string()), | |
| 188 | - | examples_path: Some(std::path::Path::new(&docs_path).join("../examples").into()), | |
| 188 | + | examples_path: Some(std::path::Path::new(&docs_path).join("../examples")), | |
| 189 | 189 | }, | |
| 190 | 190 | )); | |
| 191 | 191 |
| @@ -286,18 +286,18 @@ pub fn snapshot(handle: &PrometheusHandle) -> MetricsSnapshot { | |||
| 286 | 286 | } | |
| 287 | 287 | routes.push((method, path, status, count)); | |
| 288 | 288 | } | |
| 289 | - | } else if let Some(rest) = line.strip_prefix("http_errors_total{") { | |
| 290 | - | if let Some((labels, value)) = rest.rsplit_once("} ") { | |
| 291 | - | let count: u64 = value.parse().unwrap_or(0); | |
| 292 | - | let kind = extract_label(labels, "kind"); | |
| 293 | - | errors.push((kind, count)); | |
| 294 | - | } | |
| 289 | + | } else if let Some(rest) = line.strip_prefix("http_errors_total{") | |
| 290 | + | && let Some((labels, value)) = rest.rsplit_once("} ") | |
| 291 | + | { | |
| 292 | + | let count: u64 = value.parse().unwrap_or(0); | |
| 293 | + | let kind = extract_label(labels, "kind"); | |
| 294 | + | errors.push((kind, count)); | |
| 295 | 295 | } | |
| 296 | 296 | } | |
| 297 | 297 | ||
| 298 | - | routes.sort_by(|a, b| b.3.cmp(&a.3)); | |
| 298 | + | routes.sort_by_key(|r| std::cmp::Reverse(r.3)); | |
| 299 | 299 | routes.truncate(20); | |
| 300 | - | errors.sort_by(|a, b| b.1.cmp(&a.1)); | |
| 300 | + | errors.sort_by_key(|e| std::cmp::Reverse(e.1)); | |
| 301 | 301 | ||
| 302 | 302 | let total_errors = errors.iter().map(|(_, c)| c).sum(); | |
| 303 | 303 |
| @@ -207,20 +207,20 @@ pub fn spawn_monitor( | |||
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | 209 | // Create WAM ticket on degradation/error transitions | |
| 210 | - | if snap.status != MonitorStatus::Operational { | |
| 211 | - | if let Some(ref wam) = state.wam { | |
| 212 | - | let priority = match snap.status { | |
| 213 | - | MonitorStatus::Error => "critical", | |
| 214 | - | MonitorStatus::Degraded => "high", | |
| 215 | - | MonitorStatus::Operational => unreachable!(), | |
| 216 | - | }; | |
| 217 | - | let title = format!("Health status: {}", snap.status.as_str()); | |
| 218 | - | let body = format!( | |
| 219 | - | "db: {}\ns3: {}\nsessions: {}\ncheck_ms: {}", | |
| 220 | - | snap.db_ok, snap.s3_ok, snap.sessions_ok, snap.check_duration_ms, | |
| 221 | - | ); | |
| 222 | - | wam.create_ticket(&title, Some(&body), priority, "health-status-change", None).await; | |
| 223 | - | } | |
| 210 | + | if snap.status != MonitorStatus::Operational | |
| 211 | + | && let Some(ref wam) = state.wam | |
| 212 | + | { | |
| 213 | + | let priority = match snap.status { | |
| 214 | + | MonitorStatus::Error => "critical", | |
| 215 | + | MonitorStatus::Degraded => "high", | |
| 216 | + | MonitorStatus::Operational => unreachable!(), | |
| 217 | + | }; | |
| 218 | + | let title = format!("Health status: {}", snap.status.as_str()); | |
| 219 | + | let body = format!( | |
| 220 | + | "db: {}\ns3: {}\nsessions: {}\ncheck_ms: {}", | |
| 221 | + | snap.db_ok, snap.s3_ok, snap.sessions_ok, snap.check_duration_ms, | |
| 222 | + | ); | |
| 223 | + | wam.create_ticket(&title, Some(&body), priority, "health-status-change", None).await; | |
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | 226 | previous_status = Some(snap.status); | |
| @@ -235,12 +235,12 @@ pub fn spawn_monitor( | |||
| 235 | 235 | tracing::warn!(pool_size, active, idle = pool_idle, "DB pool pressure >80%"); | |
| 236 | 236 | let cooldown_ok = last_pool_alert_at | |
| 237 | 237 | .is_none_or(|t| t.elapsed().as_secs() >= constants::ALERT_COOLDOWN_SECS); | |
| 238 | - | if cooldown_ok { | |
| 239 | - | if let Some(ref wam) = state.wam { | |
| 240 | - | let title = format!("DB pool pressure: {active}/{pool_size} active"); | |
| 241 | - | wam.create_ticket(&title, None, "high", "db-pool-pressure", None).await; | |
| 242 | - | last_pool_alert_at = Some(Instant::now()); | |
| 243 | - | } | |
| 238 | + | if cooldown_ok | |
| 239 | + | && let Some(ref wam) = state.wam | |
| 240 | + | { | |
| 241 | + | let title = format!("DB pool pressure: {active}/{pool_size} active"); | |
| 242 | + | wam.create_ticket(&title, None, "high", "db-pool-pressure", None).await; | |
| 243 | + | last_pool_alert_at = Some(Instant::now()); | |
| 244 | 244 | } | |
| 245 | 245 | } | |
| 246 | 246 | } |
| @@ -66,20 +66,19 @@ pub(super) async fn admin_decide_appeal( | |||
| 66 | 66 | db::users::resolve_appeal(&state.db, user_id, form.decision, response_text).await?; | |
| 67 | 67 | ||
| 68 | 68 | // If approved, resume paused fan subscriptions | |
| 69 | - | if form.decision == db::AppealDecision::Approved { | |
| 70 | - | if let Some(ref stripe) = state.stripe { | |
| 71 | - | if let Some(ref account_id) = db_user.stripe_account_id { | |
| 72 | - | let resumed = db::subscriptions::resume_subscriptions_for_creator(&state.db, user_id).await?; | |
| 73 | - | for sub in &resumed { | |
| 74 | - | if let Err(e) = stripe.resume_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 75 | - | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to resume subscription on appeal approval"); | |
| 76 | - | } | |
| 77 | - | } | |
| 78 | - | if !resumed.is_empty() { | |
| 79 | - | tracing::info!(user_id = %user_id, resumed = resumed.len(), "resumed fan subscriptions on appeal approval"); | |
| 80 | - | } | |
| 69 | + | if form.decision == db::AppealDecision::Approved | |
| 70 | + | && let Some(ref stripe) = state.stripe | |
| 71 | + | && let Some(ref account_id) = db_user.stripe_account_id | |
| 72 | + | { | |
| 73 | + | let resumed = db::subscriptions::resume_subscriptions_for_creator(&state.db, user_id).await?; | |
| 74 | + | for sub in &resumed { | |
| 75 | + | if let Err(e) = stripe.resume_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 76 | + | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to resume subscription on appeal approval"); | |
| 81 | 77 | } | |
| 82 | 78 | } | |
| 79 | + | if !resumed.is_empty() { | |
| 80 | + | tracing::info!(user_id = %user_id, resumed = resumed.len(), "resumed fan subscriptions on appeal approval"); | |
| 81 | + | } | |
| 83 | 82 | } | |
| 84 | 83 | ||
| 85 | 84 | // Send decision email (fire-and-forget) |
| @@ -164,18 +164,18 @@ pub(super) async fn admin_suspend_user( | |||
| 164 | 164 | db::moderation::create_action(&state.db, id, admin.id, ModerationActionType::Suspension, reason, None).await?; | |
| 165 | 165 | ||
| 166 | 166 | // Pause fan subscriptions to this creator's projects | |
| 167 | - | if let Some(ref stripe) = state.stripe { | |
| 168 | - | if let Some(ref account_id) = db_user.stripe_account_id { | |
| 169 | - | let subs = db::subscriptions::get_active_subscriptions_by_creator(&state.db, id).await?; | |
| 170 | - | let count = subs.len(); | |
| 171 | - | for sub in &subs { | |
| 172 | - | if let Err(e) = stripe.pause_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 173 | - | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to pause subscription on Stripe"); | |
| 174 | - | } | |
| 167 | + | if let Some(ref stripe) = state.stripe | |
| 168 | + | && let Some(ref account_id) = db_user.stripe_account_id | |
| 169 | + | { | |
| 170 | + | let subs = db::subscriptions::get_active_subscriptions_by_creator(&state.db, id).await?; | |
| 171 | + | let count = subs.len(); | |
| 172 | + | for sub in &subs { | |
| 173 | + | if let Err(e) = stripe.pause_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 174 | + | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to pause subscription on Stripe"); | |
| 175 | 175 | } | |
| 176 | - | let paused = db::subscriptions::pause_subscriptions_for_creator(&state.db, id).await?; | |
| 177 | - | tracing::info!(user_id = %id, stripe_paused = count, db_paused = paused, "paused fan subscriptions for suspended creator"); | |
| 178 | 176 | } | |
| 177 | + | let paused = db::subscriptions::pause_subscriptions_for_creator(&state.db, id).await?; | |
| 178 | + | tracing::info!(user_id = %id, stripe_paused = count, db_paused = paused, "paused fan subscriptions for suspended creator"); | |
| 179 | 179 | } | |
| 180 | 180 | ||
| 181 | 181 | // Send notification email (fire-and-forget) | |
| @@ -209,16 +209,16 @@ pub(super) async fn admin_unsuspend_user( | |||
| 209 | 209 | db::moderation::resolve_actions_by_type(&state.db, id, ModerationActionType::Suspension).await?; | |
| 210 | 210 | ||
| 211 | 211 | // Resume paused fan subscriptions | |
| 212 | - | if let Some(ref stripe) = state.stripe { | |
| 213 | - | if let Some(ref account_id) = db_user.stripe_account_id { | |
| 214 | - | let resumed = db::subscriptions::resume_subscriptions_for_creator(&state.db, id).await?; | |
| 215 | - | for sub in &resumed { | |
| 216 | - | if let Err(e) = stripe.resume_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 217 | - | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to resume subscription on Stripe"); | |
| 218 | - | } | |
| 212 | + | if let Some(ref stripe) = state.stripe | |
| 213 | + | && let Some(ref account_id) = db_user.stripe_account_id | |
| 214 | + | { | |
| 215 | + | let resumed = db::subscriptions::resume_subscriptions_for_creator(&state.db, id).await?; | |
| 216 | + | for sub in &resumed { | |
| 217 | + | if let Err(e) = stripe.resume_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 218 | + | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to resume subscription on Stripe"); | |
| 219 | 219 | } | |
| 220 | - | tracing::info!(user_id = %id, resumed = resumed.len(), "resumed fan subscriptions for unsuspended creator"); | |
| 221 | 220 | } | |
| 221 | + | tracing::info!(user_id = %id, resumed = resumed.len(), "resumed fan subscriptions for unsuspended creator"); | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | 224 | tracing::info!(user_id = %id, "admin unsuspended user"); | |
| @@ -263,14 +263,14 @@ pub(super) async fn admin_terminate_user( | |||
| 263 | 263 | ).await?; | |
| 264 | 264 | ||
| 265 | 265 | // Cancel all fan subscriptions — both active and paused (suspension already paused them) | |
| 266 | - | if let Some(ref stripe) = state.stripe { | |
| 267 | - | if let Some(ref account_id) = db_user.stripe_account_id { | |
| 268 | - | let active_subs = db::subscriptions::get_active_subscriptions_by_creator(&state.db, id).await?; | |
| 269 | - | let paused_subs = db::subscriptions::get_paused_subscriptions_by_creator(&state.db, id).await?; | |
| 270 | - | for sub in active_subs.iter().chain(paused_subs.iter()) { | |
| 271 | - | if let Err(e) = stripe.cancel_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 272 | - | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to cancel subscription on termination"); | |
| 273 | - | } | |
| 266 | + | if let Some(ref stripe) = state.stripe | |
| 267 | + | && let Some(ref account_id) = db_user.stripe_account_id | |
| 268 | + | { | |
| 269 | + | let active_subs = db::subscriptions::get_active_subscriptions_by_creator(&state.db, id).await?; | |
| 270 | + | let paused_subs = db::subscriptions::get_paused_subscriptions_by_creator(&state.db, id).await?; | |
| 271 | + | for sub in active_subs.iter().chain(paused_subs.iter()) { | |
| 272 | + | if let Err(e) = stripe.cancel_subscription(&sub.stripe_subscription_id, account_id).await { | |
| 273 | + | tracing::error!(stripe_sub_id = %sub.stripe_subscription_id, error = ?e, "failed to cancel subscription on termination"); | |
| 274 | 274 | } | |
| 275 | 275 | } | |
| 276 | 276 | } |
| @@ -281,14 +281,14 @@ pub(super) async fn delete_insertion( | |||
| 281 | 281 | let file_size = insertion.as_ref().map(|i| i.file_size).unwrap_or(0); | |
| 282 | 282 | ||
| 283 | 283 | // Enqueue as a durable safety net | |
| 284 | - | if let Some(ref ins) = insertion { | |
| 285 | - | if let Err(e) = db::pending_s3_deletions::enqueue_deletions( | |
| 284 | + | if let Some(ref ins) = insertion | |
| 285 | + | && let Err(e) = db::pending_s3_deletions::enqueue_deletions( | |
| 286 | 286 | &state.db, | |
| 287 | 287 | &[(ins.storage_key.clone(), "main".to_string())], | |
| 288 | 288 | "insertion_delete", | |
| 289 | - | ).await { | |
| 290 | - | tracing::warn!(error = ?e, "failed to enqueue S3 deletion for insertion"); | |
| 291 | - | } | |
| 289 | + | ).await | |
| 290 | + | { | |
| 291 | + | tracing::warn!(error = ?e, "failed to enqueue S3 deletion for insertion"); | |
| 292 | 292 | } | |
| 293 | 293 | ||
| 294 | 294 | // Optionally delete the S3 object (best-effort) |
| @@ -58,10 +58,10 @@ pub(in crate::routes::api) async fn export_content( | |||
| 58 | 58 | let mut files: Vec<(String, String)> = Vec::new(); | |
| 59 | 59 | ||
| 60 | 60 | for item in &item_keys { | |
| 61 | - | if let Some(pid) = query.project_id { | |
| 62 | - | if item.project_id != pid { | |
| 63 | - | continue; | |
| 64 | - | } | |
| 61 | + | if let Some(pid) = query.project_id | |
| 62 | + | && item.project_id != pid | |
| 63 | + | { | |
| 64 | + | continue; | |
| 65 | 65 | } | |
| 66 | 66 | let slug = item.project_slug.as_str(); | |
| 67 | 67 | let title = sanitize_filename(&item.title); | |
| @@ -80,10 +80,10 @@ pub(in crate::routes::api) async fn export_content( | |||
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | 82 | for ver in &version_keys { | |
| 83 | - | if let Some(pid) = query.project_id { | |
| 84 | - | if ver.project_id != pid { | |
| 85 | - | continue; | |
| 86 | - | } | |
| 83 | + | if let Some(pid) = query.project_id | |
| 84 | + | && ver.project_id != pid | |
| 85 | + | { | |
| 86 | + | continue; | |
| 87 | 87 | } | |
| 88 | 88 | if let Some(ref key) = ver.s3_key { | |
| 89 | 89 | let slug = ver.project_slug.as_str(); |
| @@ -224,10 +224,10 @@ pub(super) async fn export_projects( | |||
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | 226 | // Include child item IDs for bundles | |
| 227 | - | if item.item_type == db::ItemType::Bundle { | |
| 228 | - | if let Some(child_ids) = bundle_map.get(&item.id) { | |
| 229 | - | item_json["bundle_items"] = serde_json::json!(child_ids); | |
| 230 | - | } | |
| 227 | + | if item.item_type == db::ItemType::Bundle | |
| 228 | + | && let Some(child_ids) = bundle_map.get(&item.id) | |
| 229 | + | { | |
| 230 | + | item_json["bundle_items"] = serde_json::json!(child_ids); | |
| 231 | 231 | } | |
| 232 | 232 | ||
| 233 | 233 | items_data.push(item_json); |
| @@ -73,7 +73,7 @@ pub(super) async fn create_guest_checkout( | |||
| 73 | 73 | let buyer_amount = body.amount_cents | |
| 74 | 74 | .unwrap_or(item.price_cents); | |
| 75 | 75 | pricing.validate_amount(buyer_amount) | |
| 76 | - | .map_err(|e| AppError::BadRequest(e))?; | |
| 76 | + | .map_err(AppError::BadRequest)?; | |
| 77 | 77 | buyer_amount | |
| 78 | 78 | } else { | |
| 79 | 79 | item.price_cents | |
| @@ -102,30 +102,30 @@ pub(super) async fn create_guest_checkout( | |||
| 102 | 102 | if pc.code_purpose == db::CodePurpose::FreeTrial { | |
| 103 | 103 | return Err(AppError::BadRequest("Trial codes can only be used for subscriptions".to_string())); | |
| 104 | 104 | } | |
| 105 | - | if let Some(starts) = pc.starts_at { | |
| 106 | - | if starts > chrono::Utc::now() { | |
| 107 | - | return Err(AppError::BadRequest("This promo code is not yet active".to_string())); | |
| 108 | - | } | |
| 105 | + | if let Some(starts) = pc.starts_at | |
| 106 | + | && starts > chrono::Utc::now() | |
| 107 | + | { | |
| 108 | + | return Err(AppError::BadRequest("This promo code is not yet active".to_string())); | |
| 109 | 109 | } | |
| 110 | - | if let Some(expires) = pc.expires_at { | |
| 111 | - | if expires < chrono::Utc::now() { | |
| 112 | - | return Err(AppError::BadRequest("This promo code has expired".to_string())); | |
| 113 | - | } | |
| 110 | + | if let Some(expires) = pc.expires_at | |
| 111 | + | && expires < chrono::Utc::now() | |
| 112 | + | { | |
| 113 | + | return Err(AppError::BadRequest("This promo code has expired".to_string())); | |
| 114 | 114 | } | |
| 115 | - | if let Some(max) = pc.max_uses { | |
| 116 | - | if pc.use_count >= max { | |
| 117 | - | return Err(AppError::BadRequest("This promo code has reached its usage limit".to_string())); | |
| 118 | - | } | |
| 115 | + | if let Some(max) = pc.max_uses | |
| 116 | + | && pc.use_count >= max | |
| 117 | + | { | |
| 118 | + | return Err(AppError::BadRequest("This promo code has reached its usage limit".to_string())); | |
| 119 | 119 | } | |
| 120 | - | if let Some(scoped_item) = pc.item_id { | |
| 121 | - | if scoped_item != item_id { | |
| 122 | - | return Err(AppError::BadRequest("This promo code is not valid for this item".to_string())); | |
| 123 | - | } | |
| 120 | + | if let Some(scoped_item) = pc.item_id | |
| 121 | + | && scoped_item != item_id | |
| 122 | + | { | |
| 123 | + | return Err(AppError::BadRequest("This promo code is not valid for this item".to_string())); | |
| 124 | 124 | } | |
| 125 | - | if let Some(scoped_project) = pc.project_id { | |
| 126 | - | if item.project_id != scoped_project { | |
| 127 | - | return Err(AppError::BadRequest("This promo code is not valid for this item".to_string())); | |
| 128 | - | } | |
| 125 | + | if let Some(scoped_project) = pc.project_id | |
| 126 | + | && item.project_id != scoped_project | |
| 127 | + | { | |
| 128 | + | return Err(AppError::BadRequest("This promo code is not valid for this item".to_string())); | |
| 129 | 129 | } | |
| 130 | 130 | ||
| 131 | 131 | // Apply discount to final price |
| @@ -237,17 +237,17 @@ pub(super) async fn create_promo_code( | |||
| 237 | 237 | }; | |
| 238 | 238 | ||
| 239 | 239 | // Validate starts_at < expires_at if both present | |
| 240 | - | if let (Some(start), Some(end)) = (starts_at, expires_at) { | |
| 241 | - | if start >= end { | |
| 242 | - | return Err(AppError::BadRequest("Start date must be before expiry date".to_string())); | |
| 243 | - | } | |
| 240 | + | if let (Some(start), Some(end)) = (starts_at, expires_at) | |
| 241 | + | && start >= end | |
| 242 | + | { | |
| 243 | + | return Err(AppError::BadRequest("Start date must be before expiry date".to_string())); | |
| 244 | 244 | } | |
| 245 | 245 | ||
| 246 | 246 | // Reject already-expired codes | |
| 247 | - | if let Some(exp) = expires_at { | |
| 248 | - | if exp < chrono::Utc::now() { | |
| 249 | - | return Err(AppError::BadRequest("Expiry date must be in the future".to_string())); | |
| 250 | - | } | |
| 247 | + | if let Some(exp) = expires_at | |
| 248 | + | && exp < chrono::Utc::now() | |
| 249 | + | { | |
| 250 | + | return Err(AppError::BadRequest("Expiry date must be in the future".to_string())); | |
| 251 | 251 | } | |
| 252 | 252 | ||
| 253 | 253 | let promo_code = match db::promo_codes::create_promo_code( | |
| @@ -424,7 +424,7 @@ pub(super) async fn update_promo_code( | |||
| 424 | 424 | )) | |
| 425 | 425 | }; | |
| 426 | 426 | ||
| 427 | - | let expires_at = req.expires_at.as_deref().map(|s| parse_date(s)).transpose()?; | |
| 427 | + | let expires_at = req.expires_at.as_deref().map(parse_date).transpose()?; | |
| 428 | 428 | let starts_at = req.starts_at.as_deref().map(|s| { | |
| 429 | 429 | let s = s.trim(); | |
| 430 | 430 | if s.is_empty() { |
| @@ -275,12 +275,11 @@ pub(in crate::routes::api) async fn pause_creator( | |||
| 275 | 275 | ||
| 276 | 276 | if let Some(ref stripe) = state.stripe { | |
| 277 | 277 | // Cancel the creator's own tier subscription on Stripe (platform-level) | |
| 278 | - | if let Some(ct_sub) = db::creator_tiers::get_creator_sub_by_user(&state.db, user.id).await? { | |
| 279 | - | if ct_sub.status == db::SubscriptionStatus::Active { | |
| 280 | - | if let Err(e) = stripe.cancel_platform_subscription(&ct_sub.stripe_subscription_id).await { | |
| 281 | - | tracing::warn!(error = ?e, "failed to cancel creator tier subscription on Stripe during pause"); | |
| 282 | - | } | |
| 283 | - | } | |
| 278 | + | if let Some(ct_sub) = db::creator_tiers::get_creator_sub_by_user(&state.db, user.id).await? | |
| 279 | + | && ct_sub.status == db::SubscriptionStatus::Active | |
| 280 | + | && let Err(e) = stripe.cancel_platform_subscription(&ct_sub.stripe_subscription_id).await | |
| 281 | + | { | |
| 282 | + | tracing::warn!(error = ?e, "failed to cancel creator tier subscription on Stripe during pause"); | |
| 284 | 283 | } | |
| 285 | 284 | ||
| 286 | 285 | // Set cancel_at_period_end on all active fan subscriptions (connected account) | |
| @@ -475,16 +474,16 @@ pub(in crate::routes::api) async fn submit_appeal( | |||
| 475 | 474 | let db_user = db::users::get_user_by_id(&state.db, user.id) | |
| 476 | 475 | .await? | |
| 477 | 476 | .ok_or(AppError::NotFound)?; | |
| 478 | - | if db_user.appeal_decision.as_deref() == Some("denied") { | |
| 479 | - | if let Some(decided_at) = db_user.appeal_decided_at { | |
| 480 | - | let days_since = (chrono::Utc::now() - decided_at).num_days(); | |
| 481 | - | if days_since < 30 { | |
| 482 | - | let msg = format!("Your appeal was denied. You may resubmit after {} days.", 30 - days_since); | |
| 483 | - | if is_htmx { | |
| 484 | - | return Ok(AlertTemplate::new("error", &msg).into_response()); | |
| 485 | - | } | |
| 486 | - | return Err(AppError::BadRequest(msg)); | |
| 477 | + | if db_user.appeal_decision.as_deref() == Some("denied") | |
| 478 | + | && let Some(decided_at) = db_user.appeal_decided_at | |
| 479 | + | { | |
| 480 | + | let days_since = (chrono::Utc::now() - decided_at).num_days(); | |
| 481 | + | if days_since < 30 { | |
| 482 | + | let msg = format!("Your appeal was denied. You may resubmit after {} days.", 30 - days_since); | |
| 483 | + | if is_htmx { | |
| 484 | + | return Ok(AlertTemplate::new("error", &msg).into_response()); | |
| 487 | 485 | } | |
| 486 | + | return Err(AppError::BadRequest(msg)); | |
| 488 | 487 | } | |
| 489 | 488 | } | |
| 490 | 489 | // Also reject if an appeal is already pending |