ironflow_api/rate_limit/
account.rs1use serde::Deserialize;
8use serde_json::from_slice;
9
10use super::RateLimitKey;
11
12pub(super) const ACCOUNT_BODY_LIMIT: usize = 64 * 1024;
15
16const MAX_EMAIL_LEN: usize = 320;
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum AccountLimit {
31 #[default]
33 Off,
34 ByEmail,
38}
39
40#[derive(Deserialize)]
41struct TargetAccount {
42 email: String,
43}
44
45pub(super) fn account_key(route: &str, body: &[u8]) -> Option<RateLimitKey> {
50 let TargetAccount { email } = from_slice(body).ok()?;
51 let email = email.trim().to_lowercase();
52 let usable = !email.is_empty() && email.len() <= MAX_EMAIL_LEN;
53 usable.then(|| RateLimitKey::Account {
54 route: route.to_string(),
55 email,
56 })
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn sign_in(email: &str) -> Option<RateLimitKey> {
64 Some(RateLimitKey::Account {
65 route: "/sign-in".to_string(),
66 email: email.to_string(),
67 })
68 }
69
70 #[test]
71 fn email_is_trimmed_and_lowercased() {
72 assert_eq!(
73 account_key(
74 "/sign-in",
75 br#"{"email":" Alice@IronFlow.dev ","password":"x"}"#
76 ),
77 sign_in("alice@ironflow.dev")
78 );
79 }
80
81 #[test]
82 fn body_without_email_has_no_account() {
83 assert_eq!(account_key("/sign-in", br#"{"password":"x"}"#), None);
84 assert_eq!(account_key("/sign-in", br#"{"email":" "}"#), None);
85 assert_eq!(account_key("/sign-in", br#"{"email":42}"#), None);
86 }
87
88 #[test]
89 fn non_json_body_has_no_account() {
90 assert_eq!(account_key("/sign-in", b"email=alice@ironflow.dev"), None);
91 assert_eq!(account_key("/sign-in", b""), None);
92 }
93
94 #[test]
95 fn oversized_email_has_no_account() {
96 let long = format!(r#"{{"email":"{}@x.dev"}}"#, "a".repeat(MAX_EMAIL_LEN));
97 assert_eq!(account_key("/sign-in", long.as_bytes()), None);
98 }
99
100 #[test]
101 fn unicode_email_is_lowercased() {
102 assert_eq!(
103 account_key("/sign-in", r#"{"email":"ÉLODIE@x.dev"}"#.as_bytes()),
104 sign_in("élodie@x.dev")
105 );
106 }
107
108 #[test]
109 fn routes_get_distinct_account_keys() {
110 let body = br#"{"email":"alice@ironflow.dev"}"#;
111 let sign_up = account_key("/sign-up", body);
112 assert_eq!(account_key("/sign-in", body), sign_in("alice@ironflow.dev"));
113 assert_eq!(
114 sign_up,
115 Some(RateLimitKey::Account {
116 route: "/sign-up".to_string(),
117 email: "alice@ironflow.dev".to_string(),
118 })
119 );
120 assert_ne!(sign_up, account_key("/sign-in", body));
121 }
122}