Launcher: panel accounts via authlib-injector, skin & cape editor
Microsoft sign-in is gone. Server accounts now receive Yggdrasil tokens from the panel and launch with authlib-injector pointed at the panel's auth server, so online-mode servers verify players against it. - Download authlib-injector from the panel mirror (fallback: official), SHA-256 verified and cached, with prefetched metadata - New Skin & cape settings tab: upload, arm style, reset, cape picker with front/back previews - Avatars render from the panel's head endpoint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
18 files changed
+454
-208
No files matched your search
@@ -5,9 +5,8 @@ use crate::game;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{build, AppState};
|
||||
use crate::updater;
|
||||
use scopenet_core::msa::DeviceCode;
|
||||
use scopenet_core::ping::ServerStatus;
|
||||
use scopenet_shared::LauncherManifest;
|
||||
use scopenet_shared::{LauncherManifest, PlayerProfile};
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
@@ -140,35 +139,6 @@ pub fn add_offline(state: State<'_, AppState>, username: String) -> Res<Account>
|
||||
accounts::add_offline(&state, &username).map_err(aerr)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ms_start(state: State<'_, AppState>) -> Res<DeviceCode> {
|
||||
let client_id = accounts::ms_client_id(&state).map_err(aerr)?;
|
||||
let code = scopenet_core::msa::start_device_code(&state.http, &client_id).await.map_err(aerr)?;
|
||||
*state.device_code.lock().unwrap() = Some(code.clone());
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ms_finish(state: State<'_, AppState>) -> Res<Account> {
|
||||
let client_id = accounts::ms_client_id(&state).map_err(aerr)?;
|
||||
let code = state.device_code.lock().unwrap().clone().ok_or("start the sign-in first")?;
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
*state.ms_cancel.lock().unwrap() = Some(tx);
|
||||
let session = tokio::select! {
|
||||
r = scopenet_core::msa::finish_device_code(&state.http, &client_id, &code) => r.map_err(aerr)?,
|
||||
_ = rx => return Err("cancelled".into()),
|
||||
};
|
||||
*state.device_code.lock().unwrap() = None;
|
||||
accounts::save_ms_session(&state, session).map_err(aerr)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn ms_cancel(state: State<'_, AppState>) {
|
||||
if let Some(tx) = state.ms_cancel.lock().unwrap().take() {
|
||||
tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn select_account(state: State<'_, AppState>, id: String) -> Res<()> {
|
||||
let mut accounts = state.accounts.write().unwrap();
|
||||
@@ -192,6 +162,58 @@ pub fn remove_account(state: State<'_, AppState>, id: String) -> Res<()> {
|
||||
state.save_accounts().map_err(aerr)
|
||||
}
|
||||
|
||||
// ---- skin & cape (panel accounts) ----
|
||||
|
||||
async fn account_api(state: &AppState, method: reqwest::Method, path: &str) -> Res<reqwest::RequestBuilder> {
|
||||
let panel = state.panel_url().ok_or("no panel configured")?;
|
||||
let token = accounts::panel_token(state).ok_or("sign in with a server account to change your skin")?;
|
||||
Ok(state.http.request(method, format!("{panel}/api/v1{path}")).bearer_auth(token))
|
||||
}
|
||||
|
||||
async fn profile_response(resp: reqwest::Response) -> Res<PlayerProfile> {
|
||||
if !resp.status().is_success() {
|
||||
let body: serde_json::Value = resp.json().await.unwrap_or_default();
|
||||
return Err(body["error"].as_str().unwrap_or("the server refused the change").to_string());
|
||||
}
|
||||
resp.json().await.map_err(err)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn account_profile(state: State<'_, AppState>) -> Res<PlayerProfile> {
|
||||
let req = account_api(&state, reqwest::Method::GET, "/account/profile").await?;
|
||||
profile_response(req.send().await.map_err(err)?).await
|
||||
}
|
||||
|
||||
/// `data` is the PNG as base64 (read in the UI from a file picker).
|
||||
#[tauri::command]
|
||||
pub async fn upload_skin(state: State<'_, AppState>, data: String, model: String) -> Res<PlayerProfile> {
|
||||
use base64::Engine;
|
||||
let bytes = base64::engine::general_purpose::STANDARD.decode(data.trim()).map_err(|_| "couldn't read that image".to_string())?;
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.text("model", model)
|
||||
.part("file", reqwest::multipart::Part::bytes(bytes).file_name("skin.png").mime_str("image/png").map_err(err)?);
|
||||
let req = account_api(&state, reqwest::Method::POST, "/account/skin").await?;
|
||||
profile_response(req.multipart(form).send().await.map_err(err)?).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_skin_model(state: State<'_, AppState>, model: String) -> Res<PlayerProfile> {
|
||||
let req = account_api(&state, reqwest::Method::PUT, "/account/skin/model").await?;
|
||||
profile_response(req.json(&serde_json::json!({ "model": model })).send().await.map_err(err)?).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_skin(state: State<'_, AppState>) -> Res<PlayerProfile> {
|
||||
let req = account_api(&state, reqwest::Method::DELETE, "/account/skin").await?;
|
||||
profile_response(req.send().await.map_err(err)?).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_cape(state: State<'_, AppState>, cape_id: Option<i64>) -> Res<PlayerProfile> {
|
||||
let req = account_api(&state, reqwest::Method::PUT, "/account/cape").await?;
|
||||
profile_response(req.json(&serde_json::json!({ "cape_id": cape_id })).send().await.map_err(err)?).await
|
||||
}
|
||||
|
||||
// ---- game ----
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in new issue
Block a user