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