1use std::fmt;
5use std::future::Future;
6use secrecy::SecretString;
7use serde::{Deserialize, Serialize};
8use crate::AuthError;
9
10#[cfg(feature = "keyring")]
11use crate::keyring::TokenHandle;
12
13#[cfg(feature = "events")]
14use lighty_event::EventBus;
15
16pub type AuthResult<T> = Result<T, AuthError>;
17
18#[derive(Clone)]
23pub struct UserProfile {
24 pub id: Option<u64>,
25 pub username: String,
26 pub uuid: String,
27 pub access_token: Option<SecretString>,
31 #[cfg(feature = "keyring")]
34 pub token_handle: Option<TokenHandle>,
35 pub xuid: Option<String>,
36 pub email: Option<String>,
37 pub email_verified: bool,
38 pub money: Option<f64>,
39 pub role: Option<UserRole>,
40 pub banned: bool,
41 pub provider: AuthProvider,
42}
43
44impl UserProfile {
45 pub fn offline(username: impl Into<String>, uuid: impl Into<String>) -> Self {
48 Self {
49 id: None,
50 username: username.into(),
51 uuid: uuid.into(),
52 access_token: None,
53 #[cfg(feature = "keyring")]
54 token_handle: None,
55 xuid: None,
56 email: None,
57 email_verified: false,
58 money: None,
59 role: None,
60 banned: false,
61 provider: AuthProvider::Offline,
62 }
63 }
64}
65
66impl fmt::Debug for UserProfile {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("UserProfile")
69 .field("id", &self.id)
70 .field("username", &self.username)
71 .field("uuid", &self.uuid)
72 .field("access_token", &self.access_token.as_ref().map(|_| "[REDACTED]"))
73 .field("xuid", &self.xuid)
74 .field("email", &self.email)
75 .field("email_verified", &self.email_verified)
76 .field("money", &self.money)
77 .field("role", &self.role)
78 .field("banned", &self.banned)
79 .field("provider", &self.provider)
80 .finish()
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct UserRole {
87 pub name: String,
88 pub color: Option<String>,
89}
90
91#[derive(Debug, Clone)]
97pub enum AuthProvider {
98 Offline,
99 Azuriom {
100 base_url: String,
101 },
102 Microsoft {
103 client_id: String,
104 refresh_token: Option<SecretString>,
105 },
106 Custom {
107 base_url: String,
108 },
109}
110
111pub(crate) struct TokenRouting {
113 pub access_token: Option<SecretString>,
114 #[cfg(feature = "keyring")]
115 pub token_handle: Option<TokenHandle>,
116}
117
118pub(crate) fn route_token(
123 token: String,
124 _keyring_service: Option<&str>,
125 _keyring_key: &str,
126) -> Result<TokenRouting, AuthError> {
127 let secret = SecretString::from(token);
128 #[cfg(feature = "keyring")]
129 if let Some(service) = _keyring_service {
130 let handle = TokenHandle::new(service, _keyring_key);
131 handle.store(&secret)?;
132 return Ok(TokenRouting {
133 access_token: None,
134 token_handle: Some(handle),
135 });
136 }
137 Ok(TokenRouting {
138 access_token: Some(secret),
139 #[cfg(feature = "keyring")]
140 token_handle: None,
141 })
142}
143
144impl PartialEq for AuthProvider {
145 fn eq(&self, other: &Self) -> bool {
146 use secrecy::ExposeSecret;
147 match (self, other) {
148 (Self::Offline, Self::Offline) => true,
149 (Self::Azuriom { base_url: a }, Self::Azuriom { base_url: b }) => a == b,
150 (Self::Custom { base_url: a }, Self::Custom { base_url: b }) => a == b,
151 (
152 Self::Microsoft { client_id: ca, refresh_token: ta },
153 Self::Microsoft { client_id: cb, refresh_token: tb },
154 ) => {
155 ca == cb
156 && ta.as_ref().map(|s| s.expose_secret().to_string())
157 == tb.as_ref().map(|s| s.expose_secret().to_string())
158 }
159 _ => false,
160 }
161 }
162}
163
164impl Eq for AuthProvider {}
165
166pub trait Authenticator {
168 fn authenticate(
170 &mut self,
171 #[cfg(feature = "events")] event_bus: Option<&EventBus>,
172 ) -> impl Future<Output = AuthResult<UserProfile>> + Send;
173
174 fn verify(&self, token: &str) -> impl Future<Output = AuthResult<UserProfile>> + Send {
176 async move {
177 let _ = token;
178 Err(AuthError::Custom("Verification not supported for this provider".into()))
179 }
180 }
181
182 fn logout(&self, token: &str) -> impl Future<Output = AuthResult<()>> + Send {
184 async move {
185 let _ = token;
186 Ok(())
187 }
188 }
189}
190
191pub fn generate_offline_uuid(username: &str) -> String {
193 const NAMESPACE: &[u8] = b"OfflinePlayer:";
194
195 let mut data = Vec::with_capacity(NAMESPACE.len() + username.len());
196 data.extend_from_slice(NAMESPACE);
197 data.extend_from_slice(username.as_bytes());
198
199 let hash = lighty_core::calculate_sha1_bytes_raw(&data);
200
201 format!(
204 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-5{:01x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
205 hash[0], hash[1], hash[2], hash[3],
206 hash[4], hash[5],
207 hash[6] & 0x0f, hash[7],
208 (hash[8] & 0x3f) | 0x80, hash[9],
209 hash[10], hash[11], hash[12], hash[13], hash[14], hash[15]
210 )
211}