feat(auth): enable offline mode accounts and launch without Microsoft account
This commit is contained in:
parent
b979feff0c
commit
1e66c9216c
3 changed files with 98 additions and 25 deletions
|
|
@ -383,10 +383,6 @@ impl AuthService {
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
|
||||||
if account.is_offline() && !self.has_microsoft_account().await {
|
|
||||||
return Err(AuthError::OfflineRequiresMicrosoft);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(account)
|
Ok(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,13 @@ impl CredentialsStore {
|
||||||
return Ok(Self::default());
|
return Ok(Self::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
match polyio::read_json(&path).await {
|
match polyio::read_json::<Self>(&path).await {
|
||||||
Ok(store) => Ok(store),
|
Ok(mut store) => {
|
||||||
|
if store.default_user.is_none() && !store.users.is_empty() {
|
||||||
|
store.default_user = store.users.keys().copied().next();
|
||||||
|
}
|
||||||
|
Ok(store)
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("failed to read auth file: {err}");
|
tracing::warn!("failed to read auth file: {err}");
|
||||||
Ok(Self::default())
|
Ok(Self::default())
|
||||||
|
|
@ -91,22 +96,22 @@ impl CredentialsStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_offline_account(&mut self, username: String) -> AuthResult<MinecraftAccount> {
|
fn insert_offline_account(&mut self, username: String) -> AuthResult<MinecraftAccount> {
|
||||||
if !self.has_microsoft_account() {
|
|
||||||
return Err(AuthError::OfflineRequiresMicrosoft);
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_offline_username(&username)?;
|
validate_offline_username(&username)?;
|
||||||
|
|
||||||
if self
|
if let Some(existing) = self
|
||||||
.users
|
.users
|
||||||
.values()
|
.values()
|
||||||
.any(|u| u.username.eq_ignore_ascii_case(&username))
|
.find(|u| u.username.eq_ignore_ascii_case(&username))
|
||||||
|
.cloned()
|
||||||
{
|
{
|
||||||
return Err(AuthError::DuplicateUsername { username });
|
self.default_user = Some(existing.id);
|
||||||
|
return Ok(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
let account = offline_account(username);
|
let account = offline_account(username);
|
||||||
self.users.insert(account.id, account.clone());
|
let id = account.id;
|
||||||
|
self.users.insert(id, account.clone());
|
||||||
|
self.default_user = Some(id);
|
||||||
Ok(account)
|
Ok(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,10 +210,6 @@ impl CredentialsStore {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
if account.is_offline() && !self.has_microsoft_account() {
|
|
||||||
return Err(AuthError::OfflineRequiresMicrosoft);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(account))
|
Ok(Some(account))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,3 +223,50 @@ pub(crate) fn is_transient_auth_error(err: &AuthError) -> bool {
|
||||||
if source.is_connect() || source.is_timeout()
|
if source.is_connect() || source.is_timeout()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offline_account_creation_sets_default() {
|
||||||
|
let mut store = CredentialsStore {
|
||||||
|
users: HashMap::new(),
|
||||||
|
default_user: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let account = store.insert_offline_account("Player".to_string()).unwrap();
|
||||||
|
assert_eq!(account.username, "Player");
|
||||||
|
assert_eq!(store.default_user, Some(account.id));
|
||||||
|
assert_eq!(store.users.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_custom_username_switches_default() {
|
||||||
|
let mut store = CredentialsStore {
|
||||||
|
users: HashMap::new(),
|
||||||
|
default_user: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let p1 = store.insert_offline_account("Player".to_string()).unwrap();
|
||||||
|
assert_eq!(store.default_user, Some(p1.id));
|
||||||
|
|
||||||
|
let p2 = store.insert_offline_account("Steve".to_string()).unwrap();
|
||||||
|
assert_eq!(store.default_user, Some(p2.id));
|
||||||
|
assert_eq!(store.users.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_case_insensitive_username_reuse() {
|
||||||
|
let mut store = CredentialsStore {
|
||||||
|
users: HashMap::new(),
|
||||||
|
default_user: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let p1 = store.insert_offline_account("Steve".to_string()).unwrap();
|
||||||
|
let p2 = store.insert_offline_account("steve".to_string()).unwrap();
|
||||||
|
assert_eq!(p1.id, p2.id);
|
||||||
|
assert_eq!(store.users.len(), 1);
|
||||||
|
assert_eq!(store.default_user, Some(p1.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,14 @@ fn offline_uuid_matches_vanilla_algorithm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn offline_account_blocked_without_microsoft() {
|
async fn offline_account_allowed_without_microsoft() {
|
||||||
isolate_launcher_dir();
|
isolate_launcher_dir();
|
||||||
|
|
||||||
let mut store = CredentialsStore::default();
|
let mut store = CredentialsStore::default();
|
||||||
let err = store.add_offline_account("Steve".into()).unwrap_err();
|
let offline = store.add_offline_account("Steve".into()).unwrap();
|
||||||
|
|
||||||
assert!(err.to_string().contains("Microsoft"));
|
assert_eq!(offline.kind, AccountKind::Offline);
|
||||||
|
assert_eq!(offline.username, "Steve");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -51,7 +52,7 @@ async fn offline_account_allowed_when_microsoft_exists() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn default_offline_fails_when_last_microsoft_removed() {
|
async fn default_offline_works_when_last_microsoft_removed() {
|
||||||
isolate_launcher_dir();
|
isolate_launcher_dir();
|
||||||
|
|
||||||
let mut store = CredentialsStore::default();
|
let mut store = CredentialsStore::default();
|
||||||
|
|
@ -60,11 +61,13 @@ async fn default_offline_fails_when_last_microsoft_removed() {
|
||||||
store.users.insert(msa_id, msa);
|
store.users.insert(msa_id, msa);
|
||||||
|
|
||||||
let offline = store.add_offline_account("Steve".into()).unwrap();
|
let offline = store.add_offline_account("Steve".into()).unwrap();
|
||||||
store.default_user = Some(offline.id);
|
let offline_id = offline.id;
|
||||||
|
store.default_user = Some(offline_id);
|
||||||
store.users.remove(&msa_id);
|
store.users.remove(&msa_id);
|
||||||
|
|
||||||
let err = store.default_account().await.unwrap_err();
|
let account = store.default_account().await.unwrap().unwrap();
|
||||||
assert!(err.to_string().contains("Microsoft"));
|
assert_eq!(account.id, offline_id);
|
||||||
|
assert_eq!(account.kind, AccountKind::Offline);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -147,3 +150,29 @@ fn offline_accounts_never_need_sign_in() {
|
||||||
|
|
||||||
assert!(!account.needs_sign_in());
|
assert!(!account.needs_sign_in());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn offline_account_for_launch_succeeds_without_microsoft() {
|
||||||
|
isolate_launcher_dir();
|
||||||
|
|
||||||
|
let (events, _rx) = oneclient_events::EventBus::channel();
|
||||||
|
let net = oneclient_net::RequestClient::new(oneclient_net::NetConfig::default()).expect("net client");
|
||||||
|
let mut store = CredentialsStore::default();
|
||||||
|
let offline = store.add_offline_account("Steve".into()).unwrap();
|
||||||
|
let service = oneclient_auth::AuthService::with_store(store, net, events);
|
||||||
|
|
||||||
|
let launched = service
|
||||||
|
.account_for_launch(offline.id)
|
||||||
|
.await
|
||||||
|
.expect("offline account must be allowed for launch without Microsoft account");
|
||||||
|
assert_eq!(launched.id, offline.id);
|
||||||
|
assert_eq!(launched.username, "Steve");
|
||||||
|
assert_eq!(launched.kind, AccountKind::Offline);
|
||||||
|
|
||||||
|
let default_launched = service
|
||||||
|
.default_account_for_launch()
|
||||||
|
.await
|
||||||
|
.expect("default offline account must be allowed for launch")
|
||||||
|
.expect("default account should exist");
|
||||||
|
assert_eq!(default_launched.id, offline.id);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue