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,
37}
38
39#[derive(Deserialize)]
40struct TargetAccount {
41 email: String,
42}
43
44pub(super) fn account_key(body: &[u8]) -> Option<RateLimitKey> {
49 let TargetAccount { email } = from_slice(body).ok()?;
50 let email = email.trim().to_lowercase();
51 let usable = !email.is_empty() && email.len() <= MAX_EMAIL_LEN;
52 usable.then_some(RateLimitKey::Account(email))
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn email_is_trimmed_and_lowercased() {
61 assert_eq!(
62 account_key(br#"{"email":" Alice@IronFlow.dev ","password":"x"}"#),
63 Some(RateLimitKey::Account("alice@ironflow.dev".to_string()))
64 );
65 }
66
67 #[test]
68 fn body_without_email_has_no_account() {
69 assert_eq!(account_key(br#"{"password":"x"}"#), None);
70 assert_eq!(account_key(br#"{"email":" "}"#), None);
71 assert_eq!(account_key(br#"{"email":42}"#), None);
72 }
73
74 #[test]
75 fn non_json_body_has_no_account() {
76 assert_eq!(account_key(b"email=alice@ironflow.dev"), None);
77 assert_eq!(account_key(b""), None);
78 }
79
80 #[test]
81 fn oversized_email_has_no_account() {
82 let long = format!(r#"{{"email":"{}@x.dev"}}"#, "a".repeat(MAX_EMAIL_LEN));
83 assert_eq!(account_key(long.as_bytes()), None);
84 }
85
86 #[test]
87 fn unicode_email_is_lowercased() {
88 assert_eq!(
89 account_key(r#"{"email":"ÉLODIE@x.dev"}"#.as_bytes()),
90 Some(RateLimitKey::Account("élodie@x.dev".to_string()))
91 );
92 }
93}