1use crate::auth::route_token;
7use crate::{Authenticator, AuthError, AuthProvider, AuthResult, UserProfile, UserRole};
8use lighty_core::hosts::HTTP_CLIENT as CLIENT;
9use serde::Deserialize;
10
11#[cfg(feature = "events")]
12use lighty_event::{EventBus, Event, AuthEvent};
13
14pub struct AzuriomAuth {
16 base_url: String,
17 email: String,
18 password: String,
19 two_factor_code: Option<String>,
20 #[cfg(feature = "keyring")]
21 keyring_service: Option<String>,
22}
23
24impl AzuriomAuth {
25 pub fn new(base_url: impl Into<String>, email: impl Into<String>, password: impl Into<String>) -> Self {
27 Self {
28 base_url: base_url.into().trim_end_matches('/').to_string(),
29 email: email.into(),
30 password: password.into(),
31 two_factor_code: None,
32 #[cfg(feature = "keyring")]
33 keyring_service: None,
34 }
35 }
36
37 pub fn set_two_factor_code(&mut self, code: impl Into<String>) {
39 self.two_factor_code = Some(code.into());
40 }
41
42 pub fn clear_two_factor_code(&mut self) {
44 self.two_factor_code = None;
45 }
46
47 #[cfg(feature = "keyring")]
52 pub fn with_keyring(mut self, service: impl Into<String>) -> Self {
53 self.keyring_service = Some(service.into());
54 self
55 }
56
57 fn keyring_service(&self) -> Option<&str> {
58 #[cfg(feature = "keyring")]
59 {
60 self.keyring_service.as_deref()
61 }
62 #[cfg(not(feature = "keyring"))]
63 {
64 None
65 }
66 }
67}
68
69
70#[derive(Debug, Deserialize)]
72struct AzuriomAuthResponse {
73 id: u64,
74 username: String,
75 uuid: String,
76 access_token: String,
77 email_verified: Option<bool>,
78 money: Option<f64>,
79 role: Option<AzuriomRole>,
80 banned: Option<bool>,
81}
82
83#[derive(Debug, Deserialize)]
85struct AzuriomRole {
86 name: String,
87 color: Option<String>,
88}
89
90#[derive(Debug, Deserialize)]
92struct AzuriomErrorResponse {
93 status: String,
94 reason: String,
95 message: String,
96}
97impl Authenticator for AzuriomAuth {
98 async fn authenticate(
99 &mut self,
100 #[cfg(feature = "events")] event_bus: Option<&EventBus>,
101 ) -> AuthResult<UserProfile> {
102 let url = format!("{}/api/auth/authenticate", self.base_url);
103 lighty_core::trace_debug!(url = %url, email = %self.email, "Authenticating with Azuriom");
104
105 #[cfg(feature = "events")]
106 if let Some(bus) = event_bus {
107 bus.emit(Event::Auth(AuthEvent::AuthenticationStarted {
108 provider: "Azuriom".to_string(),
109 }));
110 }
111
112 let mut body = serde_json::json!({
113 "email": self.email,
114 "password": self.password,
115 });
116
117 if let Some(code) = &self.two_factor_code {
118 body["code"] = serde_json::json!(code);
119 }
120
121 let response = CLIENT
122 .post(&url)
123 .json(&body)
124 .send()
125 .await?;
126
127 let status = response.status();
128 let response_text = response.text().await?;
129
130 if status.is_success() {
131 let azuriom_response: AzuriomAuthResponse = serde_json::from_str(&response_text)
132 .map_err(|e| AuthError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
133
134 if azuriom_response.banned.unwrap_or(false) {
135 lighty_core::trace_error!(username = %azuriom_response.username, "Account is banned");
136 #[cfg(feature = "events")]
137 if let Some(bus) = event_bus {
138 bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
139 provider: "Azuriom".to_string(),
140 error: "Account is banned".to_string(),
141 }));
142 }
143 return Err(AuthError::AccountBanned(
144 azuriom_response.username.clone()
145 ));
146 }
147
148 lighty_core::trace_info!(username = %azuriom_response.username, uuid = %azuriom_response.uuid, "Successfully authenticated");
149
150 #[cfg(feature = "events")]
151 if let Some(bus) = event_bus {
152 bus.emit(Event::Auth(AuthEvent::AuthenticationSuccess {
153 provider: "Azuriom".to_string(),
154 username: azuriom_response.username.clone(),
155 uuid: azuriom_response.uuid.clone(),
156 }));
157 }
158
159 let routed = route_token(
160 azuriom_response.access_token,
161 self.keyring_service(),
162 &format!("azuriom:{}", azuriom_response.uuid),
163 )?;
164 Ok(UserProfile {
165 id: Some(azuriom_response.id),
166 username: azuriom_response.username,
167 uuid: azuriom_response.uuid,
168 access_token: routed.access_token,
169 #[cfg(feature = "keyring")]
170 token_handle: routed.token_handle,
171 xuid: None,
172 email: Some(self.email.clone()),
173 email_verified: azuriom_response.email_verified.unwrap_or(true),
174 money: azuriom_response.money,
175 role: azuriom_response.role.map(|r| UserRole {
176 name: r.name,
177 color: r.color,
178 }),
179 banned: azuriom_response.banned.unwrap_or(false),
180 provider: AuthProvider::Azuriom { base_url: self.base_url.clone() },
181 })
182 } else {
183 let error_response: AzuriomErrorResponse = serde_json::from_str(&response_text)
184 .map_err(|_| AuthError::InvalidResponse(format!("HTTP {}: {}", status, response_text)))?;
185
186 if error_response.status != "error" {
187 return Err(AuthError::InvalidResponse(format!(
188 "HTTP {}: expected status='error', got status='{}'",
189 status, error_response.status
190 )));
191 }
192
193 lighty_core::trace_error!(reason = %error_response.reason, message = %error_response.message, "Authentication failed");
194
195 let error = match error_response.reason.as_str() {
196 "invalid_credentials" => AuthError::InvalidCredentials,
197 "2fa" => AuthError::TwoFactorRequired,
198 "invalid_2fa" => AuthError::Invalid2FACode,
199 "email_not_verified" => AuthError::EmailNotVerified,
200 "banned" => AuthError::AccountBanned(String::new()),
201 _ => AuthError::Custom(error_response.message.clone()),
202 };
203
204 #[cfg(feature = "events")]
205 if let Some(bus) = event_bus {
206 bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
207 provider: "Azuriom".to_string(),
208 error: error_response.message,
209 }));
210 }
211
212 Err(error)
213 }
214 }
215
216 async fn verify(&self, token: &str) -> AuthResult<UserProfile> {
217 let url = format!("{}/api/auth/verify", self.base_url);
218 lighty_core::trace_debug!(url = %url, "Verifying token");
219
220 let response = CLIENT
221 .post(&url)
222 .json(&serde_json::json!({
223 "access_token": token
224 }))
225 .send()
226 .await?;
227
228 let status = response.status();
229 let response_text = response.text().await?;
230
231 if status.is_success() {
232 let azuriom_response: AzuriomAuthResponse = serde_json::from_str(&response_text)
233 .map_err(|e| AuthError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
234
235 lighty_core::trace_info!(username = %azuriom_response.username, "Token verified successfully");
236
237 let routed = route_token(
238 azuriom_response.access_token,
239 self.keyring_service(),
240 &format!("azuriom:{}", azuriom_response.uuid),
241 )?;
242 Ok(UserProfile {
243 id: Some(azuriom_response.id),
244 username: azuriom_response.username,
245 uuid: azuriom_response.uuid,
246 access_token: routed.access_token,
247 #[cfg(feature = "keyring")]
248 token_handle: routed.token_handle,
249 xuid: None,
250 email: None,
251 email_verified: azuriom_response.email_verified.unwrap_or(true),
252 money: azuriom_response.money,
253 role: azuriom_response.role.map(|r| UserRole {
254 name: r.name,
255 color: r.color,
256 }),
257 banned: azuriom_response.banned.unwrap_or(false),
258 provider: AuthProvider::Azuriom { base_url: self.base_url.clone() },
259 })
260 } else {
261 lighty_core::trace_error!(status = %status, "Token verification failed");
262 Err(AuthError::InvalidToken)
263 }
264 }
265
266 async fn logout(&self, token: &str) -> AuthResult<()> {
267 let url = format!("{}/api/auth/logout", self.base_url);
268 lighty_core::trace_debug!(url = %url, "Logging out");
269
270 let response = CLIENT
271 .post(&url)
272 .json(&serde_json::json!({
273 "access_token": token
274 }))
275 .send()
276 .await?;
277
278 if response.status().is_success() {
279 lighty_core::trace_info!("Successfully logged out");
280 Ok(())
281 } else {
282 lighty_core::trace_error!(status = %response.status(), "Logout failed");
283 Err(AuthError::InvalidToken)
284 }
285 }
286}
287
288