1use crate::pool::{Exec, SqlxPool};
4use crate::sql::Sql;
5use crate::store::migrator::{Migration, apply};
6use alibi_core::middleware::rate_limit::bucket::{self, LongestWindow, Step};
7use alibi_core::store::SchemaMigrator;
8use alibi_core::{AuthResult, EndpointRateLimit, RateLimitDecision, RateLimitStorage};
9use async_trait::async_trait;
10use std::sync::Arc;
11
12const RATE_LIMIT_SCHEMA: Migration = Migration {
13 name: "m20261002_000001_rate_limit",
14 sqlite: r#"CREATE TABLE IF NOT EXISTS "rate_limit" ( "key" varchar NOT NULL PRIMARY KEY, "count" double NOT NULL, "expires_at" integer, "last_request" integer NOT NULL );"#,
15 postgres: r#"CREATE TABLE IF NOT EXISTS "rate_limit" ( "key" varchar NOT NULL PRIMARY KEY, "count" double precision NOT NULL, "expires_at" bigint, "last_request" bigint NOT NULL );"#,
16};
17
18#[derive(Clone, Debug, PartialEq, sqlx::FromRow)]
20pub struct RateLimitRow {
21 pub key: String,
22 pub count: f64,
23 pub last_request: i64,
24 pub expires_at: Option<i64>,
25}
26
27#[derive(Clone, Debug)]
31pub struct SqlxRateLimitStorage {
32 pool: SqlxPool,
33 longest_window: Arc<LongestWindow>,
34}
35
36impl SqlxRateLimitStorage {
37 #[must_use]
38 pub fn new(pool: impl Into<SqlxPool>) -> Self {
39 Self {
40 pool: pool.into(),
41 longest_window: Arc::new(LongestWindow::new()),
42 }
43 }
44
45 const fn exec(&self) -> Exec<'_> {
46 Exec::pool(&self.pool)
47 }
48
49 async fn prune(&self, now: i64) {
50 let Some(cutoff) = self.longest_window.prune_cutoff(now) else {
51 return;
52 };
53 let mut sql = Sql::with(self.exec().engine(), "DELETE FROM \"rate_limit\" WHERE ");
54 sql.compare("rate_limit", "last_request", " < ", cutoff);
55 sql.push(" AND ");
56 sql.compare("rate_limit", "expires_at", " <= ", now);
57 if self.exec().execute(sql).await.is_err() {
60 tracing::warn!("Rate-limit cleanup failed");
61 }
62 }
63}
64
65#[async_trait]
68impl SchemaMigrator for SqlxRateLimitStorage {
69 async fn migrate(&self) -> AuthResult<()> {
70 apply(
71 &self.pool,
72 "better_auth_rate_limit_migrations",
73 &[RATE_LIMIT_SCHEMA],
74 )
75 .await
76 }
77}
78
79#[async_trait]
80impl RateLimitStorage for SqlxRateLimitStorage {
81 fn observe_window(&self, window: f64) {
82 self.longest_window.observe(window);
83 }
84
85 async fn consume(&self, key: &str, rule: &EndpointRateLimit) -> AuthResult<RateLimitDecision> {
86 self.observe_window(rule.window_seconds);
87 let backend = self.exec().engine();
88 loop {
89 let mut select = Sql::with(
90 backend,
91 "SELECT \"rate_limit\".\"key\", \"rate_limit\".\"count\", \"rate_limit\".\"last_request\", \"rate_limit\".\"expires_at\" FROM \"rate_limit\" WHERE ",
92 );
93 select.compare("rate_limit", "key", " = ", key);
94 select.push(" LIMIT 1");
95 let observed = self.exec().fetch_optional::<RateLimitRow>(select).await?;
96 let now = chrono::Utc::now().timestamp_millis();
97 let Some(observed) = observed else {
98 let mut insert = Sql::with(
99 backend,
100 "INSERT INTO \"rate_limit\" (\"key\", \"count\", \"last_request\", \"expires_at\") VALUES (",
101 );
102 insert.bind(key);
103 insert.push(", ");
104 insert.bind(1.0_f64);
105 insert.push(", ");
106 insert.bind(now);
107 insert.push(", ");
108 insert.bind(bucket::expires_at(now, rule.window_seconds));
109 insert.push(") ON CONFLICT (\"key\") DO NOTHING");
110 if self.exec().execute(insert).await? == 1 {
111 self.prune(now).await;
112 return Ok(RateLimitDecision::Allowed);
113 }
114 continue;
115 };
116 let (count, reset) =
117 match bucket::step(observed.count, observed.last_request, now, rule) {
118 Step::Blocked { retry_after } => {
119 return Ok(RateLimitDecision::Blocked { retry_after });
120 }
121 Step::Allowed { count, reset } => (count, reset),
122 };
123 let mut update = Sql::with(backend, "UPDATE \"rate_limit\" SET \"count\" = ");
124 update.bind(count);
125 update.push(", \"last_request\" = ");
126 update.bind(now);
127 update.push(", \"expires_at\" = ");
128 update.bind(bucket::expires_at(now, rule.window_seconds));
129 update.push(" WHERE ");
130 update.compare("rate_limit", "key", " = ", key);
131 update.push(" AND ");
132 update.compare("rate_limit", "last_request", " = ", observed.last_request);
133 update.push(" AND ");
134 update.compare("rate_limit", "count", " = ", observed.count);
135 if self.exec().execute(update).await? == 1 {
136 if reset {
137 self.prune(now).await;
138 }
139 return Ok(RateLimitDecision::Allowed);
140 }
141 }
142 }
143}