lighty_auth/
offline.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! Offline authentication - no network required
5//!
6//! Generates a deterministic UUID v5 (SHA1-based) from the username.
7//! No token validation or verification.
8
9use crate::{Authenticator, AuthError, AuthResult, UserProfile, generate_offline_uuid};
10
11#[cfg(feature = "events")]
12use lighty_event::{EventBus, Event, AuthEvent};
13
14/// Offline authenticator
15///
16/// Generates a deterministic UUID from the username without any network calls.
17/// Suitable for offline play or testing.
18///
19/// # Example
20/// ```no_run
21/// use lighty_auth::offline::OfflineAuth;
22/// use lighty_auth::Authenticator;
23///
24/// #[tokio::main]
25/// async fn main() {
26///     let mut auth = OfflineAuth::new("Player123");
27///     let profile = auth.authenticate().await.unwrap();
28///     println!("UUID: {}", profile.uuid);
29/// }
30/// ```
31pub struct OfflineAuth {
32    username: String,
33}
34
35impl OfflineAuth {
36    /// Create a new offline authenticator
37    ///
38    /// # Arguments
39    /// - `username`: The username to authenticate with
40    ///
41    /// # Returns
42    /// A new `OfflineAuth` instance
43    pub fn new(username: impl Into<String>) -> Self {
44        Self {
45            username: username.into(),
46        }
47    }
48
49    /// Get the username
50    pub fn username(&self) -> &str {
51        &self.username
52    }
53}
54
55impl Authenticator for OfflineAuth {
56    async fn authenticate(
57        &mut self,
58        #[cfg(feature = "events")] event_bus: Option<&EventBus>,
59    ) -> AuthResult<UserProfile> {
60        // Emit authentication started
61        #[cfg(feature = "events")]
62        if let Some(bus) = event_bus {
63            bus.emit(Event::Auth(AuthEvent::AuthenticationStarted {
64                provider: "Offline".to_string(),
65            }));
66        }
67
68        // Validate username
69        if self.username.is_empty() {
70            #[cfg(feature = "events")]
71            if let Some(bus) = event_bus {
72                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
73                    provider: "Offline".to_string(),
74                    error: "Username cannot be empty".to_string(),
75                }));
76            }
77            return Err(AuthError::InvalidCredentials);
78        }
79
80        if self.username.len() < 3 || self.username.len() > 16 {
81            let error_msg = "Username must be between 3 and 16 characters".to_string();
82            #[cfg(feature = "events")]
83            if let Some(bus) = event_bus {
84                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
85                    provider: "Offline".to_string(),
86                    error: error_msg.clone(),
87                }));
88            }
89            return Err(AuthError::Custom(error_msg));
90        }
91
92        // Check for valid characters (alphanumeric + underscore)
93        if !self.username.chars().all(|c| c.is_alphanumeric() || c == '_') {
94            let error_msg = "Username can only contain letters, numbers, and underscores".to_string();
95            #[cfg(feature = "events")]
96            if let Some(bus) = event_bus {
97                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
98                    provider: "Offline".to_string(),
99                    error: error_msg.clone(),
100                }));
101            }
102            return Err(AuthError::Custom(error_msg));
103        }
104
105        // Generate deterministic UUID
106        let uuid = generate_offline_uuid(&self.username);
107
108        // Emit authentication success
109        #[cfg(feature = "events")]
110        if let Some(bus) = event_bus {
111            bus.emit(Event::Auth(AuthEvent::AuthenticationSuccess {
112                provider: "Offline".to_string(),
113                username: self.username.clone(),
114                uuid: uuid.clone(),
115            }));
116        }
117
118        Ok(UserProfile {
119            id: None,
120            username: self.username.clone(),
121            uuid,
122            access_token: None,
123            email: None,
124            email_verified: false,
125            money: None,
126            role: None,
127            banned: false,
128        })
129    }
130}