fix: 未登录时阻止下载StarLight实例
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
This commit is contained in:
1 parent
efbf998ebb
commit
bb6b8cb8d2
5 files changed
+140
-10
No files matched your search
@@ -3,6 +3,7 @@ import test from 'node:test'
|
||||
|
||||
import {
|
||||
clearHostedSession,
|
||||
HOSTED_ACCOUNT_REQUIRED_MESSAGE,
|
||||
hostedCreate,
|
||||
hostedDefault,
|
||||
hostedSync,
|
||||
@@ -46,6 +47,10 @@ test('hosted installation sends the JWT to native commands while Local needs no
|
||||
let createdId = 'instance'
|
||||
let instanceExists = false
|
||||
let tokenRequests = 0
|
||||
let accounts: Array<{
|
||||
account_type: string
|
||||
yggdrasil?: { api_root: string }
|
||||
}> = [{ account_type: 'microsoft' }]
|
||||
const frame = {
|
||||
postMessage(data: { type: string; requestId: string }) {
|
||||
if (data.type !== 'starlight-pack-token-request') return
|
||||
@@ -70,6 +75,14 @@ test('hosted installation sends the JWT to native commands while Local needs no
|
||||
__TAURI_INTERNALS__: {
|
||||
async invoke(command: string, args: Record<string, unknown>) {
|
||||
calls.push({ command, args })
|
||||
if (command === 'plugin:install|hosted_has_game_account')
|
||||
return accounts.some(
|
||||
(account) =>
|
||||
account.account_type === 'microsoft' ||
|
||||
(account.account_type === 'yggdrasil' &&
|
||||
account.yggdrasil?.api_root.replace(/\/+$/, '') ===
|
||||
'https://skin.starlight.cool/yggdrasil'),
|
||||
)
|
||||
if (command === 'plugin:install|hosted_sync' && syncResponse) return syncResponse()
|
||||
if (command === 'plugin:instance|instance_get')
|
||||
return instanceExists ? { id: args.instanceId } : null
|
||||
@@ -99,7 +112,13 @@ test('hosted installation sends the JWT to native commands while Local needs no
|
||||
await setInstanceMode('instance', 'starlight')
|
||||
assert.equal(tokenRequests, 4)
|
||||
assert.deepEqual(
|
||||
calls.map(({ command }) => command),
|
||||
calls
|
||||
.map(({ command }) => command)
|
||||
.filter(
|
||||
(command) =>
|
||||
command.startsWith('plugin:install|') &&
|
||||
command !== 'plugin:install|hosted_has_game_account',
|
||||
),
|
||||
[
|
||||
'hosted_set_session',
|
||||
'hosted_default',
|
||||
@@ -210,9 +229,35 @@ test('hosted installation sends the JWT to native commands while Local needs no
|
||||
assert.equal(tokenRequests, requestsBeforeLogout)
|
||||
const attempts: string[] = []
|
||||
const stopListening = onHostedPackAttemptStarted((id) => attempts.push(id))
|
||||
accounts = []
|
||||
const installCallsBeforeAccountFailure = calls.filter(
|
||||
({ command }) =>
|
||||
command.startsWith('plugin:install|') &&
|
||||
command !== 'plugin:install|hosted_has_game_account',
|
||||
).length
|
||||
await assert.rejects(hostedSync('failed-instance'), {
|
||||
message: HOSTED_ACCOUNT_REQUIRED_MESSAGE,
|
||||
})
|
||||
await assert.rejects(hostedCreate(), {
|
||||
message: HOSTED_ACCOUNT_REQUIRED_MESSAGE,
|
||||
})
|
||||
assert.deepEqual(attempts, [])
|
||||
assert.equal(
|
||||
calls.filter(
|
||||
({ command }) =>
|
||||
command.startsWith('plugin:install|') &&
|
||||
command !== 'plugin:install|hosted_has_game_account',
|
||||
).length,
|
||||
installCallsBeforeAccountFailure,
|
||||
)
|
||||
const unauthenticatedCreation = useHostedCreation()
|
||||
await unauthenticatedCreation.install()
|
||||
assert.equal(unauthenticatedCreation.installError.value, HOSTED_ACCOUNT_REQUIRED_MESSAGE)
|
||||
assert.equal(unauthenticatedCreation.createdInstance.value, undefined)
|
||||
accounts = [{ account_type: 'microsoft' }]
|
||||
const unauthenticatedRetry = hostedSync('failed-instance')
|
||||
assert.deepEqual(attempts, ['failed-instance'])
|
||||
await assert.rejects(unauthenticatedRetry)
|
||||
assert.deepEqual(attempts, ['failed-instance'])
|
||||
stopListening()
|
||||
await assert.rejects(hostedSync('failed-instance'))
|
||||
assert.equal(attempts.length, 1)
|
||||
|
||||
@@ -10,6 +10,10 @@ const createdInstance = ref<string>()
|
||||
const completed = ref(false)
|
||||
let generation = 0
|
||||
|
||||
function errorMessage(cause: unknown) {
|
||||
return cause instanceof Error ? cause.message : String(cause)
|
||||
}
|
||||
|
||||
export function forgetHostedCreation(instanceId: string) {
|
||||
if (createdInstance.value !== instanceId) return
|
||||
generation++
|
||||
@@ -27,7 +31,7 @@ export function markHostedCreationCompleted(instanceId: string) {
|
||||
export function markHostedCreationFailed(instanceId: string, cause: unknown) {
|
||||
if (createdInstance.value !== instanceId) return
|
||||
completed.value = false
|
||||
installError.value = String(cause)
|
||||
installError.value = errorMessage(cause)
|
||||
}
|
||||
|
||||
export function useHostedCreation() {
|
||||
@@ -63,7 +67,7 @@ export function useHostedCreation() {
|
||||
} catch (cause) {
|
||||
if (attempt === generation) {
|
||||
if (createdInstance.value) markHostedCreationFailed(createdInstance.value, cause)
|
||||
else installError.value = String(cause)
|
||||
else installError.value = errorMessage(cause)
|
||||
}
|
||||
} finally {
|
||||
installing.value = false
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
waitForSkinSiteSession,
|
||||
} from '../composables/skin-site-session.ts'
|
||||
|
||||
export const HOSTED_ACCOUNT_REQUIRED_MESSAGE = '您需要登录正版或皮肤站账号后才能操作'
|
||||
|
||||
let sessionUpdate: Promise<void> = Promise.resolve()
|
||||
const attemptListeners = new Set<(instanceId: string) => void>()
|
||||
|
||||
@@ -21,6 +23,13 @@ function startHostedPackAttempt(instanceId: string) {
|
||||
for (const listener of attemptListeners) listener(instanceId)
|
||||
}
|
||||
|
||||
export async function requireHostedGameAccount(): Promise<void> {
|
||||
const available = await invokeHosted<boolean>('plugin:install|hosted_has_game_account')
|
||||
if (!available) {
|
||||
throw new Error(HOSTED_ACCOUNT_REQUIRED_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearHostedSession(): Promise<void> {
|
||||
const update = sessionUpdate
|
||||
.catch(() => {})
|
||||
@@ -100,13 +109,16 @@ export interface HostedSyncResult {
|
||||
}
|
||||
export const hostedDefault = () =>
|
||||
invokeWithSession<HostedPublication>('plugin:install|hosted_default')
|
||||
export const hostedCreate = (gameDirRoot?: string | null) =>
|
||||
invokeWithSession<string>('plugin:install|hosted_create', {
|
||||
export const hostedCreate = async (gameDirRoot?: string | null) => {
|
||||
await requireHostedGameAccount()
|
||||
return invokeWithSession<string>('plugin:install|hosted_create', {
|
||||
gameDirRoot: gameDirRoot ?? null,
|
||||
})
|
||||
}
|
||||
export const hostedBinding = (instanceId: string) =>
|
||||
invokeHosted<HostedBinding | null>('plugin:install|hosted_binding', { instanceId })
|
||||
export const hostedSync = (instanceId: string) => {
|
||||
export const hostedSync = async (instanceId: string) => {
|
||||
await requireHostedGameAccount()
|
||||
startHostedPackAttempt(instanceId)
|
||||
return invokeWithSession<HostedSyncResult>('plugin:install|hosted_sync', { instanceId })
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
hosted_default,
|
||||
hosted_set_session,
|
||||
hosted_has_game_account,
|
||||
hosted_create,
|
||||
hosted_binding,
|
||||
hosted_sync,
|
||||
@@ -65,6 +66,11 @@ pub async fn hosted_default() -> Result<theseus::pack::hosted::Publication> {
|
||||
Ok(theseus::pack::hosted::default_publication().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hosted_has_game_account() -> Result<bool> {
|
||||
Ok(theseus::pack::hosted::has_game_account().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hosted_create(game_dir_root: Option<String>) -> Result<String> {
|
||||
Ok(theseus::pack::hosted::create(game_dir_root).await?)
|
||||
|
||||
@@ -5,8 +5,9 @@ mod transport;
|
||||
use crate::{
|
||||
State,
|
||||
state::{
|
||||
AppliedContentSetPatch, EditInstance, InstanceInstallStage,
|
||||
InstanceLaunchOverridesPatch, InstanceMode, ModLoader,
|
||||
AppliedContentSetPatch, Credentials, EditInstance,
|
||||
InstanceInstallStage, InstanceLaunchOverridesPatch, InstanceMode,
|
||||
MinecraftAccountType, ModLoader,
|
||||
},
|
||||
util::fetch::{
|
||||
DownloadRequest, Integrity, ResourceClass, configured_client,
|
||||
@@ -31,6 +32,9 @@ use tokio::{
|
||||
};
|
||||
|
||||
const API: &str = "https://skin.starlight.cool/starlight/mod/packs";
|
||||
const STARLIGHT_YGGDRASIL_API_ROOT: &str =
|
||||
"https://skin.starlight.cool/yggdrasil";
|
||||
const ACCOUNT_REQUIRED_MESSAGE: &str = "您需要登录正版或皮肤站账号后才能操作";
|
||||
const BINDING: &str = ".starlight-pack.json";
|
||||
const JOURNAL: &str = ".starlight-pack-pending.json";
|
||||
const MAX_HOSTED_CONCURRENT_FILES: usize = 8;
|
||||
@@ -426,10 +430,46 @@ pub async fn default_publication() -> crate::Result<Publication> {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_supported_game_account(
|
||||
account_type: MinecraftAccountType,
|
||||
yggdrasil_api_root: Option<&str>,
|
||||
) -> bool {
|
||||
account_type == MinecraftAccountType::Microsoft
|
||||
|| (account_type == MinecraftAccountType::Yggdrasil
|
||||
&& yggdrasil_api_root.map(|value| value.trim_end_matches('/'))
|
||||
== Some(STARLIGHT_YGGDRASIL_API_ROOT))
|
||||
}
|
||||
|
||||
async fn has_game_account_in(state: &State) -> crate::Result<bool> {
|
||||
let accounts = Credentials::get_all_without_refresh(&state.pool).await?;
|
||||
Ok(accounts.values().any(|account| {
|
||||
is_supported_game_account(
|
||||
account.account_type,
|
||||
account
|
||||
.yggdrasil
|
||||
.as_ref()
|
||||
.map(|value| value.api_root.as_str()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn has_game_account() -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
has_game_account_in(&state).await
|
||||
}
|
||||
|
||||
async fn require_game_account(state: &State) -> crate::Result<()> {
|
||||
if !has_game_account_in(state).await? {
|
||||
return Err(invalid(ACCOUNT_REQUIRED_MESSAGE));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create(game_dir_root: Option<String>) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
require_game_account(&state).await?;
|
||||
let publication = default_publication().await?;
|
||||
let runtime = &publication.manifest.runtime;
|
||||
let state = State::get().await?;
|
||||
// The pack's game files live in their own folder under the chosen root,
|
||||
// e.g. `<root>/<pack name>`. Avoid a `versions/<name>` layout: that shape
|
||||
// is reserved for externally linked launcher instances and would make the
|
||||
@@ -728,6 +768,8 @@ async fn recover(instance_id: &str) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
pub async fn synchronize(instance_id: &str) -> crate::Result<SyncResult> {
|
||||
let state = State::get().await?;
|
||||
require_game_account(&state).await?;
|
||||
let _guard = instance_gate(instance_id).lock_owned().await;
|
||||
if instance_mode(instance_id).await? != InstanceMode::StarLight {
|
||||
return Err(invalid(
|
||||
@@ -1492,6 +1534,27 @@ async fn synchronize_with_progress(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hosted_operations_accept_only_microsoft_or_starlight_accounts() {
|
||||
assert!(is_supported_game_account(
|
||||
MinecraftAccountType::Microsoft,
|
||||
None
|
||||
));
|
||||
assert!(is_supported_game_account(
|
||||
MinecraftAccountType::Yggdrasil,
|
||||
Some("https://skin.starlight.cool/yggdrasil/")
|
||||
));
|
||||
assert!(!is_supported_game_account(
|
||||
MinecraftAccountType::Yggdrasil,
|
||||
Some("https://auth.example.com/yggdrasil")
|
||||
));
|
||||
assert!(!is_supported_game_account(
|
||||
MinecraftAccountType::Offline,
|
||||
None
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn running_pack_checks_tag_changes_without_writing_live_files() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in new issue
Block a user