Skip to main content

max / makenotwork

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