summaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-13 20:19:34 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-13 20:19:34 +0800
commit48d9d886c078a04ead31a9d10744a085307444fa (patch)
tree9d94019ec67f80a01c9587d330f287c2d05702f0 /src-tauri/src/core
parent5e09625902ad0fa1b1eb555a255a3720193dbb2c (diff)
downloadDropOut-48d9d886c078a04ead31a9d10744a085307444fa.tar.gz
DropOut-48d9d886c078a04ead31a9d10744a085307444fa.zip
feat: implement Microsoft account token refresh and storage management; add Java detection functionality
Diffstat (limited to 'src-tauri/src/core')
-rw-r--r--src-tauri/src/core/account_storage.rs159
-rw-r--r--src-tauri/src/core/auth.rs83
-rw-r--r--src-tauri/src/core/java.rs254
-rw-r--r--src-tauri/src/core/mod.rs2
4 files changed, 498 insertions, 0 deletions
diff --git a/src-tauri/src/core/account_storage.rs b/src-tauri/src/core/account_storage.rs
new file mode 100644
index 0000000..b8e15e1
--- /dev/null
+++ b/src-tauri/src/core/account_storage.rs
@@ -0,0 +1,159 @@
+use crate::core::auth::{Account, MicrosoftAccount, OfflineAccount};
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::path::PathBuf;
+
+/// Stored account data for persistence
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AccountStore {
+ pub accounts: Vec<StoredAccount>,
+ pub active_account_id: Option<String>,
+}
+
+impl Default for AccountStore {
+ fn default() -> Self {
+ Self {
+ accounts: Vec::new(),
+ active_account_id: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type")]
+pub enum StoredAccount {
+ Offline(OfflineAccount),
+ Microsoft(StoredMicrosoftAccount),
+}
+
+/// Microsoft account with refresh token for persistence
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StoredMicrosoftAccount {
+ pub username: String,
+ pub uuid: String,
+ pub access_token: String,
+ pub refresh_token: Option<String>,
+ pub ms_refresh_token: Option<String>, // Microsoft OAuth refresh token
+ pub expires_at: i64,
+}
+
+impl StoredAccount {
+ pub fn id(&self) -> String {
+ match self {
+ StoredAccount::Offline(a) => a.uuid.clone(),
+ StoredAccount::Microsoft(a) => a.uuid.clone(),
+ }
+ }
+
+ pub fn to_account(&self) -> Account {
+ match self {
+ StoredAccount::Offline(a) => Account::Offline(a.clone()),
+ StoredAccount::Microsoft(a) => Account::Microsoft(MicrosoftAccount {
+ username: a.username.clone(),
+ uuid: a.uuid.clone(),
+ access_token: a.access_token.clone(),
+ refresh_token: a.refresh_token.clone(),
+ expires_at: a.expires_at,
+ }),
+ }
+ }
+
+ pub fn from_account(account: &Account, ms_refresh_token: Option<String>) -> Self {
+ match account {
+ Account::Offline(a) => StoredAccount::Offline(a.clone()),
+ Account::Microsoft(a) => StoredAccount::Microsoft(StoredMicrosoftAccount {
+ username: a.username.clone(),
+ uuid: a.uuid.clone(),
+ access_token: a.access_token.clone(),
+ refresh_token: a.refresh_token.clone(),
+ ms_refresh_token,
+ expires_at: a.expires_at,
+ }),
+ }
+ }
+}
+
+pub struct AccountStorage {
+ file_path: PathBuf,
+}
+
+impl AccountStorage {
+ pub fn new(app_data_dir: PathBuf) -> Self {
+ Self {
+ file_path: app_data_dir.join("accounts.json"),
+ }
+ }
+
+ pub fn load(&self) -> AccountStore {
+ if self.file_path.exists() {
+ let content = fs::read_to_string(&self.file_path).unwrap_or_default();
+ serde_json::from_str(&content).unwrap_or_default()
+ } else {
+ AccountStore::default()
+ }
+ }
+
+ pub fn save(&self, store: &AccountStore) -> Result<(), String> {
+ let content = serde_json::to_string_pretty(store).map_err(|e| e.to_string())?;
+ if let Some(parent) = self.file_path.parent() {
+ fs::create_dir_all(parent).map_err(|e| e.to_string())?;
+ }
+ fs::write(&self.file_path, content).map_err(|e| e.to_string())?;
+ Ok(())
+ }
+
+ pub fn add_or_update_account(
+ &self,
+ account: &Account,
+ ms_refresh_token: Option<String>,
+ ) -> Result<(), String> {
+ let mut store = self.load();
+ let stored = StoredAccount::from_account(account, ms_refresh_token);
+ let id = stored.id();
+
+ // Remove existing account with same ID
+ store.accounts.retain(|a| a.id() != id);
+ store.accounts.push(stored);
+ store.active_account_id = Some(id);
+
+ self.save(&store)
+ }
+
+ pub fn remove_account(&self, uuid: &str) -> Result<(), String> {
+ let mut store = self.load();
+ store.accounts.retain(|a| a.id() != uuid);
+ if store.active_account_id.as_deref() == Some(uuid) {
+ store.active_account_id = store.accounts.first().map(|a| a.id());
+ }
+ self.save(&store)
+ }
+
+ pub fn get_active_account(&self) -> Option<(StoredAccount, Option<String>)> {
+ let store = self.load();
+ if let Some(active_id) = &store.active_account_id {
+ store.accounts.iter().find(|a| &a.id() == active_id).map(|a| {
+ let ms_token = match a {
+ StoredAccount::Microsoft(m) => m.ms_refresh_token.clone(),
+ _ => None,
+ };
+ (a.clone(), ms_token)
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn set_active_account(&self, uuid: &str) -> Result<(), String> {
+ let mut store = self.load();
+ if store.accounts.iter().any(|a| a.id() == uuid) {
+ store.active_account_id = Some(uuid.to_string());
+ self.save(&store)
+ } else {
+ Err("Account not found".to_string())
+ }
+ }
+
+ pub fn get_all_accounts(&self) -> Vec<StoredAccount> {
+ self.load().accounts
+ }
+}
diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs
index e26f850..624f1de 100644
--- a/src-tauri/src/core/auth.rs
+++ b/src-tauri/src/core/auth.rs
@@ -101,6 +101,89 @@ pub struct TokenError {
pub error: String,
}
+/// Refresh Microsoft OAuth token using refresh_token
+pub async fn refresh_microsoft_token(refresh_token: &str) -> Result<TokenResponse, String> {
+ let client = get_client();
+ let url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
+
+ let params = [
+ ("grant_type", "refresh_token"),
+ ("client_id", CLIENT_ID),
+ ("refresh_token", refresh_token),
+ ("scope", SCOPE),
+ ];
+
+ let resp = client
+ .post(url)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .body(serde_urlencoded::to_string(&params).map_err(|e| e.to_string())?)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let text = resp.text().await.map_err(|e| e.to_string())?;
+
+ if let Ok(token_resp) = serde_json::from_str::<TokenResponse>(&text) {
+ println!("[Auth] Token refreshed successfully!");
+ return Ok(token_resp);
+ }
+
+ if let Ok(err_resp) = serde_json::from_str::<TokenError>(&text) {
+ println!("[Auth] Token refresh error: {}", err_resp.error);
+ return Err(format!("Token refresh failed: {}", err_resp.error));
+ }
+
+ Err(format!("Unknown refresh response: {}", text))
+}
+
+/// Check if a Microsoft account token is expired or about to expire
+pub fn is_token_expired(expires_at: i64) -> bool {
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs() as i64;
+
+ // Consider expired if less than 5 minutes remaining
+ expires_at - now < 300
+}
+
+/// Full refresh flow: refresh MS token -> Xbox -> XSTS -> Minecraft
+pub async fn refresh_full_auth(ms_refresh_token: &str) -> Result<(MicrosoftAccount, String), String> {
+ println!("[Auth] Starting full token refresh...");
+
+ // 1. Refresh Microsoft token
+ let token_resp = refresh_microsoft_token(ms_refresh_token).await?;
+
+ // 2. Xbox Live Auth
+ let (xbl_token, uhs) = method_xbox_live(&token_resp.access_token).await?;
+
+ // 3. XSTS Auth
+ let xsts_token = method_xsts(&xbl_token).await?;
+
+ // 4. Minecraft Auth
+ let mc_token = login_minecraft(&xsts_token, &uhs).await?;
+
+ // 5. Get Profile
+ let profile = fetch_profile(&mc_token).await?;
+
+ // 6. Create Account
+ let account = MicrosoftAccount {
+ username: profile.name,
+ uuid: profile.id,
+ access_token: mc_token,
+ refresh_token: token_resp.refresh_token.clone(),
+ expires_at: (std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs() + token_resp.expires_in) as i64,
+ };
+
+ // Return new MS refresh token for storage
+ let new_ms_refresh = token_resp.refresh_token.unwrap_or_else(|| ms_refresh_token.to_string());
+
+ Ok((account, new_ms_refresh))
+}
+
// Xbox Live Auth
#[derive(Debug, Serialize, Deserialize)]
pub struct XboxLiveResponse {
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
new file mode 100644
index 0000000..e0962fa
--- /dev/null
+++ b/src-tauri/src/core/java.rs
@@ -0,0 +1,254 @@
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use std::process::Command;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct JavaInstallation {
+ pub path: String,
+ pub version: String,
+ pub is_64bit: bool,
+}
+
+/// Detect Java installations on the system
+pub fn detect_java_installations() -> Vec<JavaInstallation> {
+ let mut installations = Vec::new();
+ let candidates = get_java_candidates();
+
+ for candidate in candidates {
+ if let Some(java) = check_java_installation(&candidate) {
+ // Avoid duplicates
+ if !installations.iter().any(|j: &JavaInstallation| j.path == java.path) {
+ installations.push(java);
+ }
+ }
+ }
+
+ // Sort by version (newer first)
+ installations.sort_by(|a, b| {
+ let v_a = parse_java_version(&a.version);
+ let v_b = parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+/// Get list of candidate Java paths to check
+fn get_java_candidates() -> Vec<PathBuf> {
+ let mut candidates = Vec::new();
+
+ // Check PATH first
+ if let Ok(output) = Command::new(if cfg!(windows) { "where" } else { "which" })
+ .arg("java")
+ .output()
+ {
+ if output.status.success() {
+ let paths = String::from_utf8_lossy(&output.stdout);
+ for line in paths.lines() {
+ let path = PathBuf::from(line.trim());
+ if path.exists() {
+ candidates.push(path);
+ }
+ }
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ // Common Linux Java paths
+ let linux_paths = [
+ "/usr/lib/jvm",
+ "/usr/java",
+ "/opt/java",
+ "/opt/jdk",
+ "/opt/openjdk",
+ ];
+
+ for base in &linux_paths {
+ if let Ok(entries) = std::fs::read_dir(base) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+
+ // Flatpak / Snap locations
+ let home = std::env::var("HOME").unwrap_or_default();
+ let snap_java = PathBuf::from(&home).join(".sdkman/candidates/java");
+ if snap_java.exists() {
+ if let Ok(entries) = std::fs::read_dir(&snap_java) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ // macOS Java paths
+ let mac_paths = [
+ "/Library/Java/JavaVirtualMachines",
+ "/System/Library/Java/JavaVirtualMachines",
+ "/usr/local/opt/openjdk/bin/java",
+ "/opt/homebrew/opt/openjdk/bin/java",
+ ];
+
+ for path in &mac_paths {
+ let p = PathBuf::from(path);
+ if p.is_dir() {
+ if let Ok(entries) = std::fs::read_dir(&p) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ } else if p.exists() {
+ candidates.push(p);
+ }
+ }
+
+ // Homebrew ARM64
+ let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk");
+ if homebrew_arm.exists() {
+ if let Ok(entries) = std::fs::read_dir(&homebrew_arm) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("libexec/openjdk.jdk/Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ // Windows Java paths
+ let program_files = std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
+ let program_files_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| "C:\\Program Files (x86)".to_string());
+ let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
+
+ let win_paths = [
+ format!("{}\\Java", program_files),
+ format!("{}\\Java", program_files_x86),
+ format!("{}\\Eclipse Adoptium", program_files),
+ format!("{}\\AdoptOpenJDK", program_files),
+ format!("{}\\Microsoft\\jdk", program_files),
+ format!("{}\\Zulu", program_files),
+ format!("{}\\Amazon Corretto", program_files),
+ format!("{}\\BellSoft\\LibericaJDK", program_files),
+ format!("{}\\Programs\\Eclipse Adoptium", local_app_data),
+ ];
+
+ for base in &win_paths {
+ let base_path = PathBuf::from(base);
+ if base_path.exists() {
+ if let Ok(entries) = std::fs::read_dir(&base_path) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin\\java.exe");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+ }
+
+ // Also check JAVA_HOME
+ if let Ok(java_home) = std::env::var("JAVA_HOME") {
+ let java_path = PathBuf::from(&java_home).join("bin\\java.exe");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+
+ // JAVA_HOME environment variable (cross-platform)
+ if let Ok(java_home) = std::env::var("JAVA_HOME") {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+ let java_path = PathBuf::from(&java_home).join("bin").join(bin_name);
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+
+ candidates
+}
+
+/// Check a specific Java installation and get its version info
+fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
+ let output = Command::new(path)
+ .arg("-version")
+ .output()
+ .ok()?;
+
+ // Java outputs version info to stderr
+ let version_output = String::from_utf8_lossy(&output.stderr);
+
+ // Parse version string (e.g., "openjdk version \"17.0.1\"" or "java version \"1.8.0_301\"")
+ let version = parse_version_string(&version_output)?;
+ let is_64bit = version_output.contains("64-Bit");
+
+ Some(JavaInstallation {
+ path: path.to_string_lossy().to_string(),
+ version,
+ is_64bit,
+ })
+}
+
+/// Parse version string from java -version output
+fn parse_version_string(output: &str) -> Option<String> {
+ for line in output.lines() {
+ if line.contains("version") {
+ // Find the quoted version string
+ if let Some(start) = line.find('"') {
+ if let Some(end) = line[start + 1..].find('"') {
+ return Some(line[start + 1..start + 1 + end].to_string());
+ }
+ }
+ }
+ }
+ None
+}
+
+/// Parse version for comparison (returns major version number)
+fn parse_java_version(version: &str) -> u32 {
+ // Handle both old format (1.8.0_xxx) and new format (11.0.x, 17.0.x)
+ let parts: Vec<&str> = version.split('.').collect();
+ if let Some(first) = parts.first() {
+ if *first == "1" {
+ // Old format: 1.8.0 -> major is 8
+ parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0)
+ } else {
+ // New format: 17.0.1 -> major is 17
+ first.parse().unwrap_or(0)
+ }
+ } else {
+ 0
+ }
+}
+
+/// Get the best Java for a specific Minecraft version
+pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaInstallation> {
+ let installations = detect_java_installations();
+
+ if let Some(required) = required_major_version {
+ // Find exact match or higher
+ installations.into_iter().find(|java| {
+ let major = parse_java_version(&java.version);
+ major >= required as u32
+ })
+ } else {
+ // Return newest
+ installations.into_iter().next()
+ }
+}
diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs
index 746afe6..475a304 100644
--- a/src-tauri/src/core/mod.rs
+++ b/src-tauri/src/core/mod.rs
@@ -1,6 +1,8 @@
+pub mod account_storage;
pub mod auth;
pub mod config;
pub mod downloader;
pub mod game_version;
+pub mod java;
pub mod manifest;
pub mod rules;