Skip to main content

max / makenotwork

5.0 KB · 147 lines History Blame Raw
1 //! Database access layer.
2 //!
3 //! Each submodule handles a specific domain: users, projects, items, etc.
4 //! Types (id_types, validated_types, enums, models) are re-exported flat.
5 //! Query functions live in their submodules: `db::users::get_user_by_id()`.
6
7 pub mod acknowledgements; // pub so the integration test crate can drive the nag/escalate cycle directly
8 pub mod admin_alerts;
9 pub(crate) mod analytics;
10 pub(crate) mod auth;
11 pub mod blog_posts;
12 pub(crate) mod builds;
13 pub mod bundles;
14 pub(crate) mod cart;
15 pub(crate) mod categories;
16 pub(crate) mod chapters;
17 pub mod collections; // pub so the integration test crate can exercise the layer directly
18 pub(crate) mod content_insertions;
19 pub mod creator_tiers;
20 pub mod custom_domains;
21 pub(crate) mod custom_links;
22 pub mod custom_pages;
23 pub mod discover; // pub so the integration test crate can assert facet-count correctness directly
24 pub(crate) mod email_signups;
25 pub(crate) mod email_suppressions;
26 mod enums;
27 pub(crate) mod fan_plus;
28 pub(crate) mod follows;
29 pub mod gallery_images;
30 pub mod git_access_tokens;
31 pub mod git_notes; // pub so mnw-admin can rebuild the index from the repositories
32 pub mod git_repos;
33 pub(crate) mod health;
34 mod id_types;
35 pub mod idempotency; // pub so the integration test crate can exercise it directly
36 pub mod imports; // pub so the integration test crate can exercise the reaper/heartbeat layer directly
37 pub mod invites; // pub so the integration test crate can exercise redemption races directly
38 pub mod issues;
39 pub(crate) mod item_sections;
40 pub mod items;
41 pub mod license_keys;
42 pub mod lists;
43 pub mod mailing_lists;
44 pub(crate) mod media_files;
45 mod models;
46 pub(crate) mod moderation;
47 pub(crate) mod monitor;
48 pub(crate) mod oauth;
49 pub(crate) mod ota;
50 pub mod page_views;
51 pub mod pagination;
52 pub mod passkeys; // pub so the integration test crate can exercise the layer directly
53 pub mod patches;
54 pub mod pending_refunds;
55 pub mod pending_s3_deletions;
56 pub(crate) mod pending_uploads;
57 pub mod platform_credits;
58 pub(crate) mod project_members;
59 pub(crate) mod project_sections;
60 pub mod projects;
61 pub(crate) mod promo_codes;
62 pub mod repo_collaborators;
63 pub(crate) mod reports;
64 pub(crate) mod scan_admin_actions;
65 pub mod scan_jobs; // pub so the integration test crate can exercise the reaper/heartbeat layer directly
66 pub mod scanning; // pub so the integration test crate can exercise the quarantine purge/stamp layer directly
67 pub(crate) mod scheduler_jobs;
68 pub mod sessions;
69 pub mod ssh_keys;
70 mod subscription_writer;
71 pub(crate) mod subscriptions;
72 pub mod synckit; // pub so the integration test crate can exercise compaction directly
73 pub mod synckit_billing;
74 pub mod tags; // pub so the integration test crate can assert facet-count correctness directly
75 pub mod tips;
76 pub(crate) mod totp;
77 pub mod transactions;
78 pub mod users;
79 mod validated_types;
80 pub mod versions;
81 pub mod waitlist;
82 pub mod webhook_events;
83 pub(crate) mod wishlists;
84
85 pub use enums::*;
86 pub use id_types::*;
87 pub use models::*;
88 pub use validated_types::*;
89
90 use crate::error::Result;
91 use sqlx::PgPool;
92
93 /// Check the sandbox per-IP cap under an advisory lock on a single connection.
94 ///
95 /// Acquires a session-level advisory lock, runs the count query, and unlocks; /// all on the same connection. Returns the active sandbox count.
96 ///
97 /// This avoids the bug where `advisory_lock` + `advisory_unlock` through a pool
98 /// use different connections, leaving locks permanently held.
99 ///
100 /// Uses `pg_try_advisory_lock` to avoid blocking under burst load; if the lock
101 /// is already held, returns an error rather than waiting.
102 pub async fn check_sandbox_cap(pool: &PgPool, lock_key: i64, ip: &str) -> Result<i64> {
103 let mut conn = pool
104 .acquire()
105 .await
106 .map_err(|e| crate::error::AppError::Internal(anyhow::anyhow!("pool acquire: {e}")))?;
107
108 // Try to acquire lock (non-blocking), all on the same connection
109 let acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
110 .bind(lock_key)
111 .fetch_one(&mut *conn)
112 .await?;
113
114 if !acquired {
115 return Err(crate::error::AppError::Internal(anyhow::anyhow!(
116 "sandbox cap check: could not acquire advisory lock"
117 )));
118 }
119
120 let count_result: Result<i64> = sqlx::query_scalar(
121 r"
122 SELECT COUNT(*) FROM users u
123 JOIN user_sessions us ON us.user_id = u.id
124 WHERE u.is_sandbox = TRUE
125 AND u.sandbox_expires_at > NOW()
126 AND us.ip_address = $1
127 ",
128 )
129 .bind(ip)
130 .fetch_one(&mut *conn)
131 .await
132 .map_err(Into::into);
133
134 // Release the advisory lock on EVERY exit path, not just the success one.
135 // If the COUNT above errored, an early `?` would return the connection to
136 // the pool with the session-level lock still held, it would only clear
137 // when `max_lifetime` rotates the connection out (up to 30 min later),
138 // silently wedging the per-IP lock key in the meantime. Best-effort unlock
139 // (a failed unlock is itself cleared by connection rotation).
140 let _ = sqlx::query("SELECT pg_advisory_unlock($1)")
141 .bind(lock_key)
142 .execute(&mut *conn)
143 .await;
144
145 count_result
146 }
147