Skip to main content

max / makenotwork

3.6 KB · 129 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 mod id_types;
8 mod validated_types;
9 mod enums;
10 mod models;
11 pub mod users;
12 pub(crate) mod projects;
13 pub mod items;
14 pub mod versions;
15 pub(crate) mod chapters;
16 pub(crate) mod item_sections;
17 pub(crate) mod project_sections;
18 pub mod transactions;
19 pub(crate) mod discover;
20 pub(crate) mod custom_links;
21 pub(crate) mod auth;
22 pub mod waitlist;
23 pub(crate) mod blog_posts;
24 pub(crate) mod license_keys;
25 pub(crate) mod synckit;
26 pub mod synckit_billing;
27 pub(crate) mod oauth;
28 pub(crate) mod promo_codes;
29 pub(crate) mod follows;
30 pub(crate) mod subscriptions;
31 pub(crate) mod tags;
32 pub(crate) mod categories;
33 pub(crate) mod sessions;
34 pub(crate) mod totp;
35 pub(crate) mod passkeys;
36 pub(crate) mod health;
37 pub(crate) mod monitor;
38 pub(crate) mod scanning;
39 pub(crate) mod scan_jobs;
40 pub(crate) mod scan_admin_actions;
41 pub(crate) mod content_insertions;
42 pub(crate) mod invites;
43 pub(crate) mod analytics;
44 pub(crate) mod email_suppressions;
45 pub mod git_repos;
46 pub mod repo_collaborators;
47 pub mod ssh_keys;
48 pub mod issues;
49 pub(crate) mod reports;
50 pub(crate) mod fan_plus;
51 pub(crate) mod collections;
52 pub(crate) mod ota;
53 pub(crate) mod builds;
54 pub(crate) mod creator_tiers;
55 pub(crate) mod mailing_lists;
56 pub mod custom_domains;
57 pub mod patches;
58 pub mod bundles;
59 pub(crate) mod email_signups;
60 pub(crate) mod imports;
61 pub(crate) mod media_files;
62 pub(crate) mod tips;
63 pub(crate) mod project_members;
64 pub(crate) mod idempotency;
65 pub(crate) mod pending_refunds;
66 pub(crate) mod webhook_events;
67 pub(crate) mod scheduler_jobs;
68 pub(crate) mod moderation;
69 pub(crate) mod wishlists;
70 pub(crate) mod cart;
71 pub mod page_views;
72 pub(crate) mod pending_s3_deletions;
73 pub(crate) mod pending_uploads;
74
75 pub use id_types::*;
76 pub use validated_types::*;
77 pub use enums::*;
78 pub use models::*;
79
80 use crate::error::Result;
81 use sqlx::PgPool;
82
83 /// Check the sandbox per-IP cap under an advisory lock on a single connection.
84 ///
85 /// Acquires a session-level advisory lock, runs the count query, and unlocks; /// all on the same connection. Returns the active sandbox count.
86 ///
87 /// This avoids the bug where `advisory_lock` + `advisory_unlock` through a pool
88 /// use different connections, leaving locks permanently held.
89 ///
90 /// Uses `pg_try_advisory_lock` to avoid blocking under burst load; if the lock
91 /// is already held, returns an error rather than waiting.
92 pub async fn check_sandbox_cap(pool: &PgPool, lock_key: i64, ip: &str) -> Result<i64> {
93 let mut conn = pool.acquire().await.map_err(|e| {
94 crate::error::AppError::Internal(anyhow::anyhow!("pool acquire: {}", e))
95 })?;
96
97 // Try to acquire lock (non-blocking) — all on the same connection
98 let acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
99 .bind(lock_key)
100 .fetch_one(&mut *conn)
101 .await?;
102
103 if !acquired {
104 return Err(crate::error::AppError::Internal(
105 anyhow::anyhow!("sandbox cap check: could not acquire advisory lock"),
106 ));
107 }
108
109 let count: i64 = sqlx::query_scalar(
110 r#"
111 SELECT COUNT(*) FROM users u
112 JOIN user_sessions us ON us.user_id = u.id
113 WHERE u.is_sandbox = TRUE
114 AND u.sandbox_expires_at > NOW()
115 AND us.ip_address = $1
116 "#,
117 )
118 .bind(ip)
119 .fetch_one(&mut *conn)
120 .await?;
121
122 sqlx::query("SELECT pg_advisory_unlock($1)")
123 .bind(lock_key)
124 .execute(&mut *conn)
125 .await?;
126
127 Ok(count)
128 }
129