torii_core/
validation.rs

1use crate::error::ValidationError;
2use regex::Regex;
3use std::sync::LazyLock;
4
5/// Centralized validation utilities for the Torii authentication framework
6///
7/// This module provides a single source of truth for all validation logic,
8/// reducing code duplication and ensuring consistent validation across the codebase.
9/// Lazy-loaded email validation regex
10///
11/// This regex validates email addresses according to a practical subset of RFC 5322.
12/// It's loaded once at runtime and reused for all email validation operations.
13static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
14    Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
15        .expect("Invalid email regex pattern")
16});
17
18/// Validates an email address
19///
20/// # Arguments
21///
22/// * `email` - The email address to validate
23///
24/// # Returns
25///
26/// Returns `Ok(())` if the email is valid, or a `ValidationError::InvalidEmail` if invalid.
27///
28/// # Examples
29///
30/// ```rust
31/// use torii_core::validation::validate_email;
32///
33/// assert!(validate_email("user@example.com").is_ok());
34/// assert!(validate_email("invalid-email").is_err());
35/// ```
36pub fn validate_email(email: &str) -> Result<(), ValidationError> {
37    if email.is_empty() {
38        return Err(ValidationError::MissingField(
39            "Email is required".to_string(),
40        ));
41    }
42
43    if email.len() > 254 {
44        return Err(ValidationError::InvalidEmail(
45            "Email is too long".to_string(),
46        ));
47    }
48
49    if EMAIL_REGEX.is_match(email) {
50        Ok(())
51    } else {
52        Err(ValidationError::InvalidEmail(format!(
53            "Invalid email format: {email}"
54        )))
55    }
56}
57
58/// Validates a password according to security requirements
59///
60/// # Arguments
61///
62/// * `password` - The password to validate
63///
64/// # Returns
65///
66/// Returns `Ok(())` if the password meets requirements, or a `ValidationError` if invalid.
67///
68/// # Password Requirements
69///
70/// - Minimum 8 characters
71/// - Maximum 128 characters
72/// - Cannot be empty or whitespace only
73///
74/// # Examples
75///
76/// ```rust
77/// use torii_core::validation::validate_password;
78///
79/// assert!(validate_password("securepassword123").is_ok());
80/// assert!(validate_password("weak").is_err());
81/// ```
82pub fn validate_password(password: &str) -> Result<(), ValidationError> {
83    if password.is_empty() {
84        return Err(ValidationError::MissingField(
85            "Password is required".to_string(),
86        ));
87    }
88
89    if password.trim().is_empty() {
90        return Err(ValidationError::InvalidPassword(
91            "Password cannot be only whitespace".to_string(),
92        ));
93    }
94
95    if password.len() < 8 {
96        return Err(ValidationError::InvalidPassword(
97            "Password must be at least 8 characters long".to_string(),
98        ));
99    }
100
101    if password.len() > 128 {
102        return Err(ValidationError::InvalidPassword(
103            "Password must be no more than 128 characters long".to_string(),
104        ));
105    }
106
107    Ok(())
108}
109
110/// Validates a user name
111///
112/// # Arguments
113///
114/// * `name` - The name to validate (optional)
115///
116/// # Returns
117///
118/// Returns `Ok(())` if the name is valid, or a `ValidationError` if invalid.
119///
120/// # Name Requirements
121///
122/// - If provided, cannot be empty or whitespace only
123/// - Maximum 100 characters
124///
125/// # Examples
126///
127/// ```rust
128/// use torii_core::validation::validate_name;
129///
130/// assert!(validate_name(Some("John Doe")).is_ok());
131/// assert!(validate_name(None).is_ok());
132/// assert!(validate_name(Some("")).is_err());
133/// ```
134pub fn validate_name(name: Option<&str>) -> Result<(), ValidationError> {
135    if let Some(name) = name {
136        if name.trim().is_empty() {
137            return Err(ValidationError::InvalidName(
138                "Name cannot be empty or whitespace only".to_string(),
139            ));
140        }
141
142        if name.len() > 100 {
143            return Err(ValidationError::InvalidName(
144                "Name must be no more than 100 characters long".to_string(),
145            ));
146        }
147    }
148
149    Ok(())
150}
151
152/// Validates a user ID string format
153///
154/// # Arguments
155///
156/// * `user_id` - The user ID string to validate
157///
158/// # Returns
159///
160/// Returns `Ok(())` if the user ID is valid, or a `ValidationError` if invalid.
161///
162/// # User ID Requirements
163///
164/// - Cannot be empty
165/// - Must be URL-safe (alphanumeric, hyphens, underscores only)
166/// - Maximum 50 characters
167///
168/// # Examples
169///
170/// ```rust
171/// use torii_core::validation::validate_user_id_string;
172///
173/// assert!(validate_user_id_string("usr_1234567890abcdef").is_ok());
174/// assert!(validate_user_id_string("").is_err());
175/// assert!(validate_user_id_string("user@invalid").is_err());
176/// ```
177pub fn validate_user_id_string(user_id: &str) -> Result<(), ValidationError> {
178    if user_id.is_empty() {
179        return Err(ValidationError::MissingField(
180            "User ID is required".to_string(),
181        ));
182    }
183
184    if user_id.len() > 50 {
185        return Err(ValidationError::InvalidUserId(
186            "User ID must be no more than 50 characters long".to_string(),
187        ));
188    }
189
190    // Check for URL-safe characters only (lowercase alphanumeric, hyphens, underscores)
191    if !user_id
192        .chars()
193        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
194    {
195        return Err(ValidationError::InvalidUserId(
196            "User ID must contain only lowercase letters, numbers, hyphens, and underscores"
197                .to_string(),
198        ));
199    }
200
201    Ok(())
202}
203
204/// Validates an OAuth provider name
205///
206/// # Arguments
207///
208/// * `provider` - The OAuth provider name to validate
209///
210/// # Returns
211///
212/// Returns `Ok(())` if the provider name is valid, or a `ValidationError` if invalid.
213///
214/// # Provider Requirements
215///
216/// - Cannot be empty
217/// - Must be lowercase alphanumeric with optional hyphens
218/// - Maximum 50 characters
219///
220/// # Examples
221///
222/// ```rust
223/// use torii_core::validation::validate_oauth_provider;
224///
225/// assert!(validate_oauth_provider("google").is_ok());
226/// assert!(validate_oauth_provider("github-enterprise").is_ok());
227/// assert!(validate_oauth_provider("").is_err());
228/// assert!(validate_oauth_provider("Invalid_Provider").is_err());
229/// ```
230pub fn validate_oauth_provider(provider: &str) -> Result<(), ValidationError> {
231    if provider.is_empty() {
232        return Err(ValidationError::MissingField(
233            "OAuth provider is required".to_string(),
234        ));
235    }
236
237    if provider.len() > 50 {
238        return Err(ValidationError::InvalidProvider(
239            "OAuth provider name must be no more than 50 characters long".to_string(),
240        ));
241    }
242
243    // Check for lowercase alphanumeric with hyphens only
244    if !provider
245        .chars()
246        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
247    {
248        return Err(ValidationError::InvalidProvider(
249            "OAuth provider name must contain only lowercase letters, numbers, and hyphens"
250                .to_string(),
251        ));
252    }
253
254    Ok(())
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_validate_email_valid() {
263        assert!(validate_email("user@example.com").is_ok());
264        assert!(validate_email("test.email+tag@domain.co.uk").is_ok());
265        assert!(validate_email("user123@test-domain.com").is_ok());
266    }
267
268    #[test]
269    fn test_validate_email_invalid() {
270        assert!(validate_email("").is_err());
271        assert!(validate_email("invalid-email").is_err());
272        assert!(validate_email("@domain.com").is_err());
273        assert!(validate_email("user@").is_err());
274        assert!(validate_email("user@domain").is_err());
275
276        // Test email too long
277        let long_email = format!("{}@example.com", "a".repeat(250));
278        assert!(validate_email(&long_email).is_err());
279    }
280
281    #[test]
282    fn test_validate_password_valid() {
283        assert!(validate_password("password123").is_ok());
284        assert!(validate_password("a_very_secure_password_with_symbols!@#").is_ok());
285        assert!(validate_password("12345678").is_ok()); // Minimum length
286    }
287
288    #[test]
289    fn test_validate_password_invalid() {
290        assert!(validate_password("").is_err());
291        assert!(validate_password("   ").is_err()); // Whitespace only
292        assert!(validate_password("short").is_err()); // Too short
293        assert!(validate_password(&"a".repeat(129)).is_err()); // Too long
294    }
295
296    #[test]
297    fn test_validate_name_valid() {
298        assert!(validate_name(None).is_ok());
299        assert!(validate_name(Some("John Doe")).is_ok());
300        assert!(validate_name(Some("Jane")).is_ok());
301        assert!(validate_name(Some("José María García-López")).is_ok());
302    }
303
304    #[test]
305    fn test_validate_name_invalid() {
306        assert!(validate_name(Some("")).is_err());
307        assert!(validate_name(Some("   ")).is_err()); // Whitespace only
308        assert!(validate_name(Some(&"a".repeat(101))).is_err()); // Too long
309    }
310
311    #[test]
312    fn test_validate_user_id_string_valid() {
313        assert!(validate_user_id_string("usr_1234567890abcdef").is_ok());
314        assert!(validate_user_id_string("user-123").is_ok());
315        assert!(validate_user_id_string("simple_id").is_ok());
316        assert!(validate_user_id_string("12345").is_ok());
317    }
318
319    #[test]
320    fn test_validate_user_id_string_invalid() {
321        assert!(validate_user_id_string("").is_err());
322        assert!(validate_user_id_string("user@invalid").is_err()); // Invalid character
323        assert!(validate_user_id_string("User_ID").is_err()); // Uppercase not allowed
324        assert!(validate_user_id_string(&"a".repeat(51)).is_err()); // Too long
325    }
326
327    #[test]
328    fn test_validate_oauth_provider_valid() {
329        assert!(validate_oauth_provider("google").is_ok());
330        assert!(validate_oauth_provider("github").is_ok());
331        assert!(validate_oauth_provider("microsoft-azure").is_ok());
332        assert!(validate_oauth_provider("auth0").is_ok());
333    }
334
335    #[test]
336    fn test_validate_oauth_provider_invalid() {
337        assert!(validate_oauth_provider("").is_err());
338        assert!(validate_oauth_provider("Google").is_err()); // Uppercase
339        assert!(validate_oauth_provider("provider_name").is_err()); // Underscore
340        assert!(validate_oauth_provider("provider.name").is_err()); // Dot
341        assert!(validate_oauth_provider(&"a".repeat(51)).is_err()); // Too long
342    }
343}