OneLauncher-Cracked/packages/oneclient_auth/src/store.rs

224 lines
7 KiB
Rust

use std::collections::HashMap;
use uuid::Uuid;
use oneclient_common::paths;
use oneclient_events::EventBus;
use crate::data::{AccountKind, MinecraftAccount};
use crate::error::{AuthError, AuthResult};
use crate::offline::{offline_account, validate_offline_username};
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct CredentialsStore {
pub users: HashMap<Uuid, MinecraftAccount>,
pub default_user: Option<Uuid>,
}
impl CredentialsStore {
#[tracing::instrument(level = "debug")]
pub async fn load() -> AuthResult<Self> {
let path = paths::auth_file()?;
if !path.exists() {
return Ok(Self::default());
}
match polyio::read_json(&path).await {
Ok(store) => Ok(store),
Err(err) => {
tracing::warn!("failed to read auth file: {err}");
Ok(Self::default())
}
}
}
#[tracing::instrument(level = "debug", skip_all)]
pub async fn save(&self) -> AuthResult<()> {
let path = paths::auth_file()?;
polyio::write_json_atomic(&path, self).await?;
Ok(())
}
pub fn has_microsoft_account(&self) -> bool {
self.users
.values()
.any(|account| account.kind == AccountKind::Microsoft)
}
pub fn list_accounts(&self) -> Vec<MinecraftAccount> {
self.users.values().cloned().collect()
}
pub fn get_account(&self, id: Uuid) -> Option<&MinecraftAccount> {
self.users.get(&id)
}
#[tracing::instrument(level = "debug", skip_all, fields(username = %account.username))]
pub async fn commit_account(
&mut self,
account: MinecraftAccount,
events: &EventBus,
) -> AuthResult<MinecraftAccount> {
self.users.insert(account.id, account.clone());
if self.default_user.is_none() {
self.default_user = Some(account.id);
}
self.save().await?;
tracing::debug!("committed account to credentials store");
events
.notify("Account added")
.body(format!("Signed in as {}", account.username))
.send();
Ok(account)
}
pub fn add_offline_account(&mut self, username: String) -> AuthResult<MinecraftAccount> {
let account = self.insert_offline_account(username)?;
Ok(account)
}
#[tracing::instrument(level = "debug", skip(self), fields(username = %username))]
pub async fn add_offline_account_and_save(
&mut self,
username: String,
) -> AuthResult<MinecraftAccount> {
let account = self.insert_offline_account(username)?;
self.save().await?;
Ok(account)
}
fn insert_offline_account(&mut self, username: String) -> AuthResult<MinecraftAccount> {
if !self.has_microsoft_account() {
return Err(AuthError::OfflineRequiresMicrosoft);
}
validate_offline_username(&username)?;
if self
.users
.values()
.any(|u| u.username.eq_ignore_ascii_case(&username))
{
return Err(AuthError::DuplicateUsername { username });
}
let account = offline_account(username);
self.users.insert(account.id, account.clone());
Ok(account)
}
#[tracing::instrument(level = "debug", skip_all, fields(%account.id))]
pub async fn commit_refreshed_account(&mut self, account: MinecraftAccount) -> AuthResult<()> {
self.users.insert(account.id, account);
self.save().await?;
tracing::debug!("stored refreshed Microsoft account");
Ok(())
}
/// Drops the dead tokens but keeps the entry and its default slot so the UI
/// can offer a sign-in for this exact account
#[tracing::instrument(level = "debug", skip(self), fields(%id))]
pub async fn mark_signed_out(&mut self, id: Uuid) -> AuthResult<()> {
let Some(account) = self.users.get_mut(&id) else {
return Err(AuthError::AccountNotFound(id));
};
account.signed_out = true;
account.access_token.clear();
account.refresh_token.clear();
self.save().await?;
Ok(())
}
/// Debug aid swaps in a token Microsoft will reject and backdates expiry so
/// the next launch walks the real `invalid_grant` path
#[tracing::instrument(level = "debug", skip(self), fields(%id))]
pub async fn corrupt_refresh_token(&mut self, id: Uuid) -> AuthResult<()> {
let Some(account) = self.users.get_mut(&id) else {
return Err(AuthError::AccountNotFound(id));
};
account.refresh_token = "oneclient-debug-invalid-refresh-token".to_string();
account.expires = chrono::DateTime::UNIX_EPOCH;
self.save().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), fields(%id))]
pub async fn remove_account(&mut self, id: Uuid) -> AuthResult<Option<MinecraftAccount>> {
let removed = self.users.remove(&id);
if self.default_user == Some(id) {
self.default_user = self.users.keys().copied().next();
}
self.save().await?;
Ok(removed)
}
#[tracing::instrument(level = "debug", skip(self), fields(?id))]
pub async fn set_default_user(&mut self, id: Option<Uuid>) -> AuthResult<()> {
if let Some(id) = id
&& !self.users.contains_key(&id)
{
return Err(AuthError::AccountNotFound(id));
}
self.default_user = id;
self.save().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip_all)]
pub async fn resolve_default_id(&mut self) -> AuthResult<Option<Uuid>> {
let id = self
.default_user
.or_else(|| self.users.keys().copied().next());
let Some(id) = id else {
return Ok(None);
};
if !self.users.contains_key(&id) {
return Ok(None);
}
if self.default_user != Some(id) {
self.default_user = Some(id);
self.save().await?;
}
Ok(Some(id))
}
/// Never touches the network callers that need a usable token go through
/// [`crate::AuthService::account_for_launch`]
#[tracing::instrument(level = "debug", skip_all)]
pub async fn default_account(&mut self) -> AuthResult<Option<MinecraftAccount>> {
let Some(id) = self.resolve_default_id().await? else {
return Ok(None);
};
let Some(account) = self.users.get(&id).cloned() else {
return Ok(None);
};
if account.is_offline() && !self.has_microsoft_account() {
return Err(AuthError::OfflineRequiresMicrosoft);
}
Ok(Some(account))
}
}
/// A transient failure must keep the existing token discarding it because
/// Wi-Fi dropped would sign the user out of a working account
pub(crate) fn is_transient_auth_error(err: &AuthError) -> bool {
matches!(
err,
AuthError::Minecraft(crate::error::MinecraftAuthError::RequestError { source, .. })
if source.is_connect() || source.is_timeout()
)
}