poly+ backend
This commit is contained in:
parent
a822f8d098
commit
1635ca38ce
8 changed files with 223 additions and 10 deletions
|
|
@ -1,6 +1,13 @@
|
|||
// === API ===
|
||||
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &str = "1426999264633946334";
|
||||
pub const PLUS_BACKEND_URL: &str = match option_env!("ONECLIENT_PLUS_BACKEND_URL") {
|
||||
Some(url) => url,
|
||||
#[cfg(debug_assertions)]
|
||||
None => "https://plus-staging.polyfrost.org",
|
||||
#[cfg(not(debug_assertions))]
|
||||
None => "https://plus.polyfrost.org",
|
||||
};
|
||||
|
||||
// === Meta ===
|
||||
// Cannot contain trailing slash
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ async fn initialize_core() -> LauncherResult<()> {
|
|||
launcher_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
launcher_website: "https://polyfrost.org/".to_string(),
|
||||
discord_client_id: Some(constants::DISCORD_CLIENT_ID.to_string()),
|
||||
plus_backend_url: Some(constants::PLUS_BACKEND_URL.to_string()),
|
||||
fetch_attempts: 3,
|
||||
logger_filter: Some(format!(
|
||||
"{}={level},onelauncher_core={level}",
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'oneclient':'{"getBundlesFor":["cluster_id"],"installUpdate":[],"cacheArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"],"getVersions":[],"updateBundlePackages":["cluster_id"],"isBundleSyncing":[],"checkForUpdate":[],"refreshArt":["path"],"copyClusterContent":["source_id","target_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getClustersGroupedByMajor":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getFullDebugInfo":[],"getArch":[],"getFullDebugInfoParsed":[],"openDevTools":[],"getOsDistro":[],"getLocale":[],"getGitCommitHash":[],"isInDev":[],"getOsVersion":[],"getBuildTimestamp":[],"getFamily":[],"getFullDebugInfoParsedString":[],"hasDevSettings":[],"getPlatform":[],"getPackageVersion":[],"getType":[]}', 'core':'{"isClusterRunning":["cluster_id"],"readSettings":[],"launchCluster":["id","uuid","search_for_java"],"writeSettings":["setting"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getLinkedPackages":["cluster_id"],"getLogs":["id"],"setClusterStage":["id","stage"],"killProcess":["pid"],"updateClusterById":["id","request"],"refreshAccounts":[],"getProcessLogTail":["id","max_lines"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getGameVersions":[],"getWorlds":["id"],"removePackage":["cluster_id","package_hash"],"togglePackage":["cluster_id","package_hash"],"openMsaLogin":[],"getLogByName":["id","name"],"installModpack":["modpack","cluster_id"],"createCluster":["options"],"getProfileOrDefault":["name"],"getPackageBody":["provider","body"],"removeUser":["uuid"],"getUsersFromAuthor":["provider","author"],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getPackage":["provider","slug"],"searchPackages":["provider","query"],"setDefaultUser":["uuid"],"getLoadersForVersion":["mc_version"],"createSettingsProfile":["name"],"getMultiplePackages":["provider","slugs"],"syncCluster":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"getUser":["uuid"],"getDefaultUser":["fallback"],"getRunningProcesses":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removeCape":["access_token"],"setDiscordRPCMessage":["message"],"getRunningProcessesByClusterId":["cluster_id"],"open":["input"],"getClusterById":["id"],"updateClusterProfile":["name","profile"],"changeCape":["access_token","cape_uuid"],"convertUsernameUUID":["username_uuid"],"removeCluster":["id"],"getScreenshots":["id"],"getClusters":[],"getUsers":[],"getGlobalProfile":[],"refreshAccount":["uuid"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}' }
|
||||
const ARGS_MAP = { 'core':'{"searchPackages":["provider","query"],"refreshAccounts":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"syncCluster":["cluster_id"],"removePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"getProcessLogTail":["id","max_lines"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeCape":["access_token","cape_uuid"],"getPackage":["provider","slug"],"setDiscordRPCMessage":["message"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCape":["access_token"],"readSettings":[],"getClusters":[],"launchCluster":["id","uuid","search_for_java"],"getLogs":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getGameVersions":[],"removeCluster":["id"],"getClusterById":["id"],"convertUsernameUUID":["username_uuid"],"getLogByName":["id","name"],"createSettingsProfile":["name"],"updateClusterById":["id","request"],"getProfileOrDefault":["name"],"isClusterRunning":["cluster_id"],"openMsaLogin":[],"setDefaultUser":["uuid"],"writeSettings":["setting"],"getWorlds":["id"],"getUser":["uuid"],"fetchLoggedInProfile":["access_token"],"open":["input"],"getRunningProcessesByClusterId":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"getUsersFromAuthor":["provider","author"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsers":[],"removeUser":["uuid"],"setClusterStage":["id","stage"],"createCluster":["options"],"killProcess":["pid"],"getLinkedPackages":["cluster_id"],"getDefaultUser":["fallback"],"getMultiplePackages":["provider","slugs"],"getRunningProcesses":[],"getScreenshots":["id"],"getGlobalProfile":[],"getPackageBody":["provider","body"],"updateClusterProfile":["name","profile"],"refreshAccount":["uuid"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'debug':'{"getType":[],"getOsDistro":[],"openDevTools":[],"getFamily":[],"getPackageVersion":[],"getGitCommitHash":[],"getFullDebugInfoParsed":[],"getOsVersion":[],"getArch":[],"getFullDebugInfoParsedString":[],"hasDevSettings":[],"getPlatform":[],"isInDev":[],"getFullDebugInfo":[],"getLocale":[],"getBuildTimestamp":[]}', 'oneclient':'{"refreshArt":["path"],"isBundleSyncing":[],"installUpdate":[],"updateBundlePackages":["cluster_id"],"getBundlesFor":["cluster_id"],"checkForUpdate":[],"getClustersGroupedByMajor":[],"extractBundleOverrides":["bundle_path","cluster_id"],"copyClusterContent":["source_id","target_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getVersions":[],"cacheArt":["path"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
|
|
@ -380,11 +380,6 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
|
|||
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
|
||||
setDiscordRPCMessage: (message: string) => Promise<null>,
|
||||
open: (input: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
|
|
@ -412,7 +407,12 @@ updateBundlePackages: (clusterId: number) => Promise<ApplyBundleUpdatesResult>,
|
|||
isBundleSyncing: () => Promise<boolean>,
|
||||
cacheArt: (path: string) => Promise<string>,
|
||||
refreshArt: (path: string) => Promise<null>,
|
||||
copyClusterContent: (sourceId: number, targetId: number) => Promise<CopyClusterResult> } };
|
||||
copyClusterContent: (sourceId: number, targetId: number) => Promise<CopyClusterResult> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { AccountAvatar, Overlay } from '@/components';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand, useCommandMut } from '@onelauncher/common';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
RefreshCw01Icon,
|
||||
} from '@untitled-theme/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string')
|
||||
return error;
|
||||
|
||||
if (error instanceof Error)
|
||||
return error.message;
|
||||
|
||||
if (typeof error === 'object' && error !== null)
|
||||
return JSON.stringify(error);
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function isExpiredRefreshError(error: unknown): boolean {
|
||||
const message = getErrorMessage(error);
|
||||
return (
|
||||
message.includes('invalid_grant')
|
||||
|| message.includes('grant is expired')
|
||||
|| message.includes('RefreshOAuthToken')
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthRefreshPrompt() {
|
||||
const queryClient = useQueryClient();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
const refresh = useCommand(
|
||||
['refreshAccounts'],
|
||||
bindings.core.refreshAccounts,
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
const shouldPrompt = useMemo(
|
||||
() => !dismissed && refresh.isError && isExpiredRefreshError(refresh.error),
|
||||
[dismissed, refresh.error, refresh.isError],
|
||||
);
|
||||
|
||||
const {
|
||||
data: profile,
|
||||
isPending,
|
||||
mutate: signInAgain,
|
||||
} = useCommandMut(bindings.core.openMsaLogin, {
|
||||
onSuccess() {
|
||||
setDismissed(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['getUsers'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['getDefaultUser'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['refreshAccounts'] });
|
||||
},
|
||||
});
|
||||
|
||||
if (!shouldPrompt)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
isDismissable
|
||||
isOpen={shouldPrompt}
|
||||
onOpenChange={open => setDismissed(!open)}
|
||||
>
|
||||
<Overlay.Dialog isDismissable>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-warning/20">
|
||||
<AlertTriangleIcon className="w-4 h-4 text-code-warn" />
|
||||
</div>
|
||||
<Overlay.Title>Sign In Again</Overlay.Title>
|
||||
</div>
|
||||
|
||||
<p className="text-fg-secondary text-sm text-center max-w-sm">
|
||||
Your Microsoft session expired. Sign in again to keep your account and
|
||||
launch access up to date.
|
||||
</p>
|
||||
|
||||
{profile && (
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<AccountAvatar
|
||||
className="aspect-square h-12 rounded-sm"
|
||||
uuid={profile.id}
|
||||
/>
|
||||
<div className="text-left flex flex-col">
|
||||
<p className="flex items-center gap-1 text-fg-primary font-semibold">
|
||||
{profile.username}
|
||||
</p>
|
||||
<p className="text-fg-secondary text-sm">{profile.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
color="primary"
|
||||
isPending={isPending}
|
||||
onPress={() => signInAgain()}
|
||||
size="large"
|
||||
>
|
||||
<RefreshCw01Icon className="w-4 h-4" />
|
||||
Sign in again
|
||||
</Button>
|
||||
</Overlay.Dialog>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './AccountPopup';
|
||||
export * from './AddAccountModal';
|
||||
export * from './AuthRefreshPrompt';
|
||||
export * from './BundleModListModal';
|
||||
export * from './DebugInfo';
|
||||
export * from './ImportSkinModal';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import type { NavigateOptions, ToOptions } from '@tanstack/react-router';
|
||||
import { Toasts } from '@/components';
|
||||
import { AuthRefreshPrompt, Toasts } from '@/components';
|
||||
import { MigrationModal } from '@/components/overlay/MigrationModal';
|
||||
import { useDebugKeybind } from '@/hooks/useDebugInfo';
|
||||
import { useDiscordRPC } from '@/hooks/useDiscordRPC';
|
||||
|
|
@ -49,6 +49,7 @@ function RootRoute() {
|
|||
</div>
|
||||
|
||||
<Toasts />
|
||||
<AuthRefreshPrompt />
|
||||
|
||||
{setting('seen_onboarding') && (
|
||||
<MigrationModal
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub struct CoreOptions {
|
|||
pub msa_client_id: String,
|
||||
pub msa_redirect_uri: String,
|
||||
pub curseforge_api_key: Option<String>,
|
||||
pub plus_backend_url: Option<String>,
|
||||
pub logger_span_formatting: Option<FmtSpan>,
|
||||
pub logger_filter: Option<String>,
|
||||
}
|
||||
|
|
@ -37,6 +38,7 @@ impl Default for CoreOptions {
|
|||
msa_client_id: String::from("00000000402b5328"),
|
||||
msa_redirect_uri: String::from("https://login.live.com/oauth20_desktop.srf"),
|
||||
curseforge_api_key: None,
|
||||
plus_backend_url: None,
|
||||
logger_span_formatting: None,
|
||||
logger_filter: None,
|
||||
}
|
||||
|
|
@ -57,6 +59,7 @@ impl Default for CoreOptions {
|
|||
.ok()
|
||||
.unwrap_or_else(|| String::from("https://login.live.com/oauth20_desktop.srf")),
|
||||
curseforge_api_key: std::env::var("CURSEFORGE_API_KEY").ok(),
|
||||
plus_backend_url: std::env::var("PLUS_BACKEND_URL").ok(),
|
||||
logger_span_formatting: None,
|
||||
logger_filter: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,14 @@ impl CredentialsStore {
|
|||
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
|
||||
};
|
||||
|
||||
if let Err(err) = record_plus_login(&credentials).await {
|
||||
tracing::warn!(
|
||||
"[auth] finish() - failed to record Poly+ login analytics for user {}: {:?}",
|
||||
credentials.id,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
self.users.insert(profile_id, credentials.clone());
|
||||
|
||||
if self.default_user.is_none() {
|
||||
|
|
@ -253,6 +261,14 @@ impl CredentialsStore {
|
|||
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
|
||||
};
|
||||
|
||||
if let Err(err) = record_plus_login(&val).await {
|
||||
tracing::warn!(
|
||||
"[auth] refresh_token() - failed to record Poly+ login analytics for user {}: {:?}",
|
||||
val.id,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
self.users.insert(val.id, val.clone());
|
||||
self.save().await?;
|
||||
|
||||
|
|
@ -505,7 +521,7 @@ struct OAuthToken {
|
|||
// pub foci: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[tracing::instrument(skip(code, verify))]
|
||||
async fn oauth_token(
|
||||
code: &str,
|
||||
verify: &str,
|
||||
|
|
@ -563,7 +579,7 @@ async fn oauth_token(
|
|||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[tracing::instrument(skip(refresh_token))]
|
||||
async fn oauth_refresh(
|
||||
refresh_token: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthError> {
|
||||
|
|
@ -866,6 +882,76 @@ async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, Mi
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PlusLoginResponse {
|
||||
#[allow(dead_code)]
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(credentials))]
|
||||
async fn record_plus_login(credentials: &MinecraftCredentials) -> anyhow::Result<()> {
|
||||
let Some(base_url) = Core::get().plus_backend_url.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let server_id = generate_plus_server_id();
|
||||
let selected_profile = credentials.id.simple().to_string();
|
||||
|
||||
let join_res = auth_retry(|| {
|
||||
crate::utils::http::REQWEST_CLIENT
|
||||
.post("https://sessionserver.mojang.com/session/minecraft/join")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&credentials.access_token)
|
||||
.json(&json!({
|
||||
"accessToken": credentials.access_token,
|
||||
"selectedProfile": selected_profile,
|
||||
"serverId": server_id,
|
||||
}))
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !join_res.status().is_success() {
|
||||
let status = join_res.status();
|
||||
let body = join_res.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Mojang join rejected Poly+ analytics login: status={status}, body={body}");
|
||||
}
|
||||
|
||||
let mut login_url =
|
||||
reqwest::Url::parse(&format!("{}/account/login", base_url.trim_end_matches('/')))?;
|
||||
login_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("username", &credentials.username)
|
||||
.append_pair("server_id", &server_id);
|
||||
|
||||
let plus_res = auth_retry(|| {
|
||||
crate::utils::http::REQWEST_CLIENT
|
||||
.post(login_url.clone())
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !plus_res.status().is_success() {
|
||||
let status = plus_res.status();
|
||||
let body = plus_res.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Poly+ login rejected analytics login: status={status}, body={body}");
|
||||
}
|
||||
|
||||
let _response = plus_res.json::<PlusLoginResponse>().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_plus_server_id() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let bytes: Vec<u8> = (0..20).map(|_| rng.r#gen::<u8>()).collect();
|
||||
bytes.iter().fold(String::new(), |mut output, b| {
|
||||
let _ = write!(output, "{b:02x}");
|
||||
output
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(request_fn))]
|
||||
async fn auth_retry<F>(request_fn: impl Fn() -> F) -> Result<reqwest::Response, reqwest::Error>
|
||||
where
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue