OneLauncher-Cracked/packages/oneclient_auth/src/store.rs
2026-07-28 12:11:02 +02:00

200 lines
6 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(())
}
#[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))
}
/// The current account as stored. 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))
}
}
/// Whether a renewal failure was the network rather than a rejected token.
///
/// A transient failure must keep the existing (possibly stale) token: throwing
/// it away 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()
)
}