1use std::sync::Arc;
4
5#[cfg(feature = "online-mode")]
6use azalea_auth::AccessTokenResponse;
7#[cfg(feature = "online-mode")]
8use azalea_auth::certs::{Certificates, FetchCertificatesError};
9use bevy_ecs::component::Component;
10use parking_lot::Mutex;
11#[cfg(feature = "online-mode")]
12use thiserror::Error;
13use uuid::Uuid;
14
15#[derive(Clone, Component, Debug)]
37pub struct Account {
38 pub username: String,
40 pub access_token: Option<Arc<Mutex<String>>>,
46 pub uuid: Option<Uuid>,
48
49 pub account_opts: AccountOpts,
56
57 #[cfg(feature = "online-mode")]
62 pub certs: Arc<Mutex<Option<Certificates>>>,
63}
64
65#[derive(Clone, Debug)]
67pub enum AccountOpts {
68 Offline {
69 username: String,
70 },
71 #[cfg(feature = "online-mode")]
72 Microsoft {
73 email: String,
74 },
75 #[cfg(feature = "online-mode")]
76 MicrosoftWithAccessToken {
77 msa: Arc<Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>>,
78 },
79}
80
81impl Account {
82 pub fn offline(username: &str) -> Self {
87 Self {
88 username: username.to_owned(),
89 access_token: None,
90 uuid: None,
91 account_opts: AccountOpts::Offline {
92 username: username.to_owned(),
93 },
94 #[cfg(feature = "online-mode")]
95 certs: Arc::new(Mutex::new(None)),
96 }
97 }
98
99 #[cfg(feature = "online-mode")]
105 pub async fn microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> {
106 Self::microsoft_with_custom_client_id_and_scope(cache_key, None, None).await
107 }
108
109 #[cfg(feature = "online-mode")]
114 pub async fn microsoft_with_custom_client_id_and_scope(
115 cache_key: &str,
116 client_id: Option<&str>,
117 scope: Option<&str>,
118 ) -> Result<Self, azalea_auth::AuthError> {
119 let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
120 panic!(
121 "No {} environment variable found",
122 minecraft_folder_path::home_env_var()
123 )
124 });
125 let auth_result = azalea_auth::auth(
126 cache_key,
127 azalea_auth::AuthOpts {
128 cache_file: Some(minecraft_dir.join("azalea-auth.json")),
129 client_id,
130 scope,
131 ..Default::default()
132 },
133 )
134 .await?;
135 Ok(Self {
136 username: auth_result.profile.name,
137 access_token: Some(Arc::new(Mutex::new(auth_result.access_token))),
138 uuid: Some(auth_result.profile.id),
139 account_opts: AccountOpts::Microsoft {
140 email: cache_key.to_owned(),
141 },
142 certs: Arc::new(Mutex::new(None)),
144 })
145 }
146
147 #[cfg(feature = "online-mode")]
172 pub async fn with_microsoft_access_token(
173 msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
174 ) -> Result<Self, azalea_auth::AuthError> {
175 Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await
176 }
177
178 #[cfg(feature = "online-mode")]
181 pub async fn with_microsoft_access_token_and_custom_client_id_and_scope(
182 mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
183 client_id: Option<&str>,
184 scope: Option<&str>,
185 ) -> Result<Self, azalea_auth::AuthError> {
186 let client = reqwest::Client::new();
187
188 if msa.is_expired() {
189 use tracing::trace;
190
191 trace!("refreshing Microsoft auth token");
192 msa = azalea_auth::refresh_ms_auth_token(
193 &client,
194 &msa.data.refresh_token,
195 client_id,
196 scope,
197 )
198 .await?;
199 }
200
201 let msa_token = &msa.data.access_token;
202
203 let res = azalea_auth::get_minecraft_token(&client, msa_token).await?;
204
205 let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?;
206
207 Ok(Self {
208 username: profile.name,
209 access_token: Some(Arc::new(Mutex::new(res.minecraft_access_token))),
210 uuid: Some(profile.id),
211 account_opts: AccountOpts::MicrosoftWithAccessToken {
212 msa: Arc::new(Mutex::new(msa)),
213 },
214 certs: Arc::new(Mutex::new(None)),
215 })
216 }
217 #[cfg(feature = "online-mode")]
223 pub async fn refresh(&self) -> Result<(), azalea_auth::AuthError> {
224 match &self.account_opts {
225 AccountOpts::Offline { .. } => Ok(()),
227 AccountOpts::Microsoft { email } => {
228 let new_account = Account::microsoft(email).await?;
229 let access_token_mutex = self.access_token.as_ref().unwrap();
230 let new_access_token = new_account.access_token.unwrap().lock().clone();
231 *access_token_mutex.lock() = new_access_token;
232 Ok(())
233 }
234 AccountOpts::MicrosoftWithAccessToken { msa } => {
235 let msa_value = msa.lock().clone();
236 let new_account = Account::with_microsoft_access_token(msa_value).await?;
237
238 let access_token_mutex = self.access_token.as_ref().unwrap();
239 let new_access_token = new_account.access_token.unwrap().lock().clone();
240
241 *access_token_mutex.lock() = new_access_token;
242 let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
243 new_account.account_opts
244 else {
245 unreachable!()
246 };
247 *msa.lock() = new_msa.lock().clone();
248
249 Ok(())
250 }
251 }
252 }
253
254 #[cfg(not(feature = "online-mode"))]
257 pub async fn refresh(&self) -> Result<(), ()> {
258 Ok(())
259 }
260
261 pub fn uuid_or_offline(&self) -> Uuid {
266 self.uuid
267 .unwrap_or_else(|| azalea_crypto::offline::generate_uuid(&self.username))
268 }
269}
270
271#[cfg(feature = "online-mode")]
272#[derive(Debug, Error)]
273pub enum RequestCertError {
274 #[error("Failed to fetch certificates")]
275 FetchCertificates(#[from] FetchCertificatesError),
276 #[error("You can't request certificates for an offline account")]
277 NoAccessToken,
278}
279
280impl Account {
281 #[cfg(feature = "online-mode")]
284 pub async fn request_certs(&mut self) -> Result<(), RequestCertError> {
285 let access_token = self
286 .access_token
287 .as_ref()
288 .ok_or(RequestCertError::NoAccessToken)?
289 .lock()
290 .clone();
291 let certs = azalea_auth::certs::fetch_certificates(&access_token).await?;
292 *self.certs.lock() = Some(certs);
293
294 Ok(())
295 }
296}