Skip to main content

lighty_auth/
offline.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! Offline authentication: deterministic UUID v5 derived from the username.
5
6use crate::{Authenticator, AuthError, AuthProvider, AuthResult, UserProfile, generate_offline_uuid};
7
8#[cfg(feature = "events")]
9use lighty_event::{EventBus, Event, AuthEvent};
10
11/// Offline authenticator — no network calls, suitable for offline play or testing.
12pub struct OfflineAuth {
13    username: String,
14}
15
16impl OfflineAuth {
17    /// Create a new offline authenticator.
18    pub fn new(username: impl Into<String>) -> Self {
19        Self {
20            username: username.into(),
21        }
22    }
23
24    /// Get the username.
25    pub fn username(&self) -> &str {
26        &self.username
27    }
28}
29
30impl Authenticator for OfflineAuth {
31    async fn authenticate(
32        &mut self,
33        #[cfg(feature = "events")] event_bus: Option<&EventBus>,
34    ) -> AuthResult<UserProfile> {
35        #[cfg(feature = "events")]
36        if let Some(bus) = event_bus {
37            bus.emit(Event::Auth(AuthEvent::AuthenticationStarted {
38                provider: "Offline".to_string(),
39            }));
40        }
41
42        if self.username.is_empty() {
43            #[cfg(feature = "events")]
44            if let Some(bus) = event_bus {
45                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
46                    provider: "Offline".to_string(),
47                    error: "Username cannot be empty".to_string(),
48                }));
49            }
50            return Err(AuthError::InvalidCredentials);
51        }
52
53        if self.username.len() < 3 || self.username.len() > 16 {
54            let error_msg = "Username must be between 3 and 16 characters".to_string();
55            #[cfg(feature = "events")]
56            if let Some(bus) = event_bus {
57                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
58                    provider: "Offline".to_string(),
59                    error: error_msg.clone(),
60                }));
61            }
62            return Err(AuthError::Custom(error_msg));
63        }
64
65        if !self.username.chars().all(|c| c.is_alphanumeric() || c == '_') {
66            let error_msg = "Username can only contain letters, numbers, and underscores".to_string();
67            #[cfg(feature = "events")]
68            if let Some(bus) = event_bus {
69                bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
70                    provider: "Offline".to_string(),
71                    error: error_msg.clone(),
72                }));
73            }
74            return Err(AuthError::Custom(error_msg));
75        }
76
77        let uuid = generate_offline_uuid(&self.username);
78
79        #[cfg(feature = "events")]
80        if let Some(bus) = event_bus {
81            bus.emit(Event::Auth(AuthEvent::AuthenticationSuccess {
82                provider: "Offline".to_string(),
83                username: self.username.clone(),
84                uuid: uuid.clone(),
85            }));
86        }
87
88        Ok(UserProfile {
89            id: None,
90            username: self.username.clone(),
91            uuid,
92            access_token: None,
93            #[cfg(feature = "keyring")]
94            token_handle: None,
95            xuid: None,
96            email: None,
97            email_verified: false,
98            money: None,
99            role: None,
100            banned: false,
101            provider: AuthProvider::Offline,
102        })
103    }
104}