1use crate::{Authenticator, AuthError, AuthResult, UserProfile, UserRole};
13use lighty_core::hosts::HTTP_CLIENT as CLIENT;
14use serde::Deserialize;
15
16#[cfg(feature = "events")]
17use lighty_event::{EventBus, Event, AuthEvent};
18
19pub struct AzuriomAuth {
41 base_url: String,
42 email: String,
43 password: String,
44 two_factor_code: Option<String>,
45}
46
47impl AzuriomAuth {
48 pub fn new(base_url: impl Into<String>, email: impl Into<String>, password: impl Into<String>) -> Self {
55 Self {
56 base_url: base_url.into().trim_end_matches('/').to_string(),
57 email: email.into(),
58 password: password.into(),
59 two_factor_code: None,
60 }
61 }
62
63 pub fn set_two_factor_code(&mut self, code: impl Into<String>) {
68 self.two_factor_code = Some(code.into());
69 }
70
71 pub fn clear_two_factor_code(&mut self) {
73 self.two_factor_code = None;
74 }
75}
76
77
78#[derive(Debug, Deserialize)]
80struct AzuriomAuthResponse {
81 id: u64,
82 username: String,
83 uuid: String,
84 access_token: String,
85 email_verified: Option<bool>,
86 money: Option<f64>,
87 role: Option<AzuriomRole>,
88 banned: Option<bool>,
89}
90
91#[derive(Debug, Deserialize)]
93struct AzuriomRole {
94 name: String,
95 color: Option<String>,
96}
97
98#[derive(Debug, Deserialize)]
100struct AzuriomErrorResponse {
101 status: String, reason: String,
103 message: String,
104}
105impl Authenticator for AzuriomAuth {
106 async fn authenticate(
107 &mut self,
108 #[cfg(feature = "events")] event_bus: Option<&EventBus>,
109 ) -> AuthResult<UserProfile> {
110 let url = format!("{}/api/auth/authenticate", self.base_url);
111 lighty_core::trace_debug!(url = %url, email = %self.email, "Authenticating with Azuriom");
112
113 #[cfg(feature = "events")]
115 if let Some(bus) = event_bus {
116 bus.emit(Event::Auth(AuthEvent::AuthenticationStarted {
117 provider: "Azuriom".to_string(),
118 }));
119 }
120
121 let mut body = serde_json::json!({
123 "email": self.email,
124 "password": self.password,
125 });
126
127 if let Some(code) = &self.two_factor_code {
129 body["code"] = serde_json::json!(code);
130 }
131
132 let response = CLIENT
134 .post(&url)
135 .json(&body)
136 .send()
137 .await?;
138
139 let status = response.status();
140 let response_text = response.text().await?;
141
142 if status.is_success() {
144 let azuriom_response: AzuriomAuthResponse = serde_json::from_str(&response_text)
145 .map_err(|e| AuthError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
146
147 if azuriom_response.banned.unwrap_or(false) {
149 lighty_core::trace_error!(username = %azuriom_response.username, "Account is banned");
150 #[cfg(feature = "events")]
151 if let Some(bus) = event_bus {
152 bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
153 provider: "Azuriom".to_string(),
154 error: "Account is banned".to_string(),
155 }));
156 }
157 return Err(AuthError::AccountBanned(
158 azuriom_response.username.clone()
159 ));
160 }
161
162 lighty_core::trace_info!(username = %azuriom_response.username, uuid = %azuriom_response.uuid, "Successfully authenticated");
163
164 #[cfg(feature = "events")]
166 if let Some(bus) = event_bus {
167 bus.emit(Event::Auth(AuthEvent::AuthenticationSuccess {
168 provider: "Azuriom".to_string(),
169 username: azuriom_response.username.clone(),
170 uuid: azuriom_response.uuid.clone(),
171 }));
172 }
173
174 Ok(UserProfile {
175 id: Some(azuriom_response.id),
176 username: azuriom_response.username,
177 uuid: azuriom_response.uuid,
178 access_token: Some(azuriom_response.access_token),
179 email: Some(self.email.clone()),
180 email_verified: azuriom_response.email_verified.unwrap_or(true),
181 money: azuriom_response.money,
182 role: azuriom_response.role.map(|r| UserRole {
183 name: r.name,
184 color: r.color,
185 }),
186 banned: azuriom_response.banned.unwrap_or(false),
187 })
188 } else {
189 let error_response: AzuriomErrorResponse = serde_json::from_str(&response_text)
191 .map_err(|_| AuthError::InvalidResponse(format!("HTTP {}: {}", status, response_text)))?;
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 "requires_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")]
206 if let Some(bus) = event_bus {
207 bus.emit(Event::Auth(AuthEvent::AuthenticationFailed {
208 provider: "Azuriom".to_string(),
209 error: error_response.message,
210 }));
211 }
212
213 Err(error)
214 }
215 }
216
217 async fn verify(&self, token: &str) -> AuthResult<UserProfile> {
218 let url = format!("{}/api/auth/verify", self.base_url);
219 lighty_core::trace_debug!(url = %url, "Verifying token");
220
221 let response = CLIENT
222 .post(&url)
223 .json(&serde_json::json!({
224 "access_token": token
225 }))
226 .send()
227 .await?;
228
229 let status = response.status();
230 let response_text = response.text().await?;
231
232 if status.is_success() {
233 let azuriom_response: AzuriomAuthResponse = serde_json::from_str(&response_text)
234 .map_err(|e| AuthError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
235
236 lighty_core::trace_info!(username = %azuriom_response.username, "Token verified successfully");
237
238 Ok(UserProfile {
239 id: Some(azuriom_response.id),
240 username: azuriom_response.username,
241 uuid: azuriom_response.uuid,
242 access_token: Some(azuriom_response.access_token),
243 email: None, email_verified: azuriom_response.email_verified.unwrap_or(true),
245 money: azuriom_response.money,
246 role: azuriom_response.role.map(|r| UserRole {
247 name: r.name,
248 color: r.color,
249 }),
250 banned: azuriom_response.banned.unwrap_or(false),
251 })
252 } else {
253 lighty_core::trace_error!(status = %status, "Token verification failed");
254 Err(AuthError::InvalidToken)
255 }
256 }
257
258 async fn logout(&self, token: &str) -> AuthResult<()> {
259 let url = format!("{}/api/auth/logout", self.base_url);
260 lighty_core::trace_debug!(url = %url, "Logging out");
261
262 let response = CLIENT
263 .post(&url)
264 .json(&serde_json::json!({
265 "access_token": token
266 }))
267 .send()
268 .await?;
269
270 if response.status().is_success() {
271 lighty_core::trace_info!("Successfully logged out");
272 Ok(())
273 } else {
274 lighty_core::trace_error!(status = %response.status(), "Logout failed");
275 Err(AuthError::InvalidToken)
276 }
277 }
278}
279
280