From 82303dc94ad0841698682adcc0b6fe294f87bd65 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 21:41:38 +0200 Subject: [PATCH] fix(accounts): refresh status before enabling accounts Validate Real-Debrid, Mega-Debrid, and Debrid-Link accounts silently before exposing a newly enabled account to active downloads. Keep the row toggle optimistic, skip network checks while disabling, persist direct account-check results, and avoid the browser-login race for an already authenticated Real-Debrid web session. --- src/main/app-controller.ts | 14 +++++++--- src/renderer/App.tsx | 31 +++++++++++++++++------ src/renderer/account-ui.ts | 10 ++++++++ tests/account-check-ipc-sanitizer.test.ts | 15 +++++++++++ tests/account-ui.test.ts | 25 ++++++++++++++++++ 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index fa75f99..abf7486 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -598,15 +598,21 @@ export class AppController { ? parseMegaDebridAccounts(`${input.identity.trim()}:${input.secret}`)[0] : getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId); if (!account) throw new Error("Account-Payload ist ungültig"); - const status = await checkMegaDebridAccount(account); - return sanitizeDebridAccountStatus(status, redactions); + const status = sanitizeDebridAccountStatus(await checkMegaDebridAccount(account), redactions); + if (!input.secret && getMegaDebridAccountsForMode(this.settings, mode).some((entry) => entry.id === status.accountId)) { + this.manager.applyDebridAccountStatuses([status]); + } + return status; } const key = input.secret?.trim() ? parseDebridLinkApiKeys(input.secret)[0] : parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId); if (!key) throw new Error("Account-Payload ist ungültig"); - const status = await checkDebridLinkKey(key); - return sanitizeDebridAccountStatus(status, redactions); + const status = sanitizeDebridAccountStatus(await checkDebridLinkKey(key), redactions); + if (!input.secret && parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).some((entry) => entry.id === status.accountId)) { + this.manager.applyDebridAccountStatuses([status]); + } + return status; } public resetProviderDailyUsage(provider: DebridProvider): AppSettings { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index ddb5548..5f52ace 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -41,7 +41,7 @@ import { } from "../shared/provider-daily-limits"; import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order"; import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection"; -import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, getAvailableAccountOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, sortAccountServices, updateAccountRowSelection } from "./account-ui"; +import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, getAvailableAccountOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runAccountEnableRefresh, runOptimisticAccountUpdate, sortAccountServices, updateAccountRowSelection } from "./account-ui"; import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit"; import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit"; import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons"; @@ -2859,7 +2859,10 @@ export function App(): ReactElement { }); }; - const persistAccountToggle = async (nextDraft: RendererSettingsDraft): Promise => { + const persistAccountToggle = async ( + nextDraft: RendererSettingsDraft, + refreshBeforePersist?: () => Promise + ): Promise => { const previousDraft = settingsDraft; const previousDirty = settingsDirtyRef.current; const previousSaveState = settingsSaveState; @@ -2871,7 +2874,10 @@ export function App(): ReactElement { setSettingsSaveState("saving"); setSettingsDraft(nextDraft); }, - () => persistSpecificSettings(nextDraft), + () => runAccountEnableRefresh( + refreshBeforePersist, + () => persistSpecificSettings(nextDraft) + ), () => { if (settingsDraftRevisionRef.current !== revision) return; settingsDraftRevisionRef.current += 1; @@ -2897,7 +2903,10 @@ export function App(): ReactElement { disabledProviders: nextState.disabledProviders, debridLinkDisabledKeyIds: nextState.disabledAccountIds }; - await persistAccountToggle(nextDraft); + await persistAccountToggle( + nextDraft, + enabled ? async () => { await window.rd.checkAccountCredentials({ kind: "debridlink-api", accountId: key.id }); } : undefined + ); showToast( enabled ? `${entry.serviceLabel} ${key.label} aktiviert` @@ -2943,7 +2952,10 @@ export function App(): ReactElement { megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])], megaDebridApiDisabledAccountIds: apiDisabledIds, megaDebridWebDisabledAccountIds: webDisabledIds - }); + }, enabled + ? async () => { await window.rd.checkAccountCredentials({ kind, accountId }); } + : undefined + ); showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000); }, (error) => { showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200); @@ -2984,7 +2996,7 @@ export function App(): ReactElement { }); }; - const onToggleRealDebridAccountEnabled = async (accountId: string, enabled: boolean): Promise => { + const onToggleRealDebridAccountEnabled = async (kind: "realdebrid-api" | "realdebrid-web", accountId: string, enabled: boolean): Promise => { await performQuickAction(async () => { const nextState = buildScopedAccountEnabledState( settingsDraft.disabledProviders || [], @@ -2997,7 +3009,10 @@ export function App(): ReactElement { ...settingsDraft, disabledProviders: nextState.disabledProviders, realDebridDisabledAccountIds: nextState.disabledAccountIds - }); + }, enabled + ? async () => { await window.rd.checkAccountCredentials({ kind, accountId }); } + : undefined + ); showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000); }, (error) => { showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200); @@ -3007,7 +3022,7 @@ export function App(): ReactElement { const toggleAccountTableRow = (row: AccountTableRow): void => { setAccountContextMenu(null); if (row.toggleKind === "rd" && row.accountId) { - void onToggleRealDebridAccountEnabled(row.accountId, row.disabled); + void onToggleRealDebridAccountEnabled(row.entry.kind as "realdebrid-api" | "realdebrid-web", row.accountId, row.disabled); } else if (row.toggleKind === "mega" && row.accountId) { void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled); } else if (row.toggleKind === "dl" && row.dlKey) { diff --git a/src/renderer/account-ui.ts b/src/renderer/account-ui.ts index 8c28187..47045bd 100644 --- a/src/renderer/account-ui.ts +++ b/src/renderer/account-ui.ts @@ -145,6 +145,16 @@ export async function runOptimisticAccountUpdate( } } +export async function runAccountEnableRefresh( + refresh: (() => Promise) | undefined, + persist: () => Promise +): Promise { + if (refresh) { + await refresh(); + } + return persist(); +} + export function buildScopedAccountEnabledState( currentDisabledProviders: DebridProvider[], providerIds: DebridProvider[], diff --git a/tests/account-check-ipc-sanitizer.test.ts b/tests/account-check-ipc-sanitizer.test.ts index 521d4d4..bddc784 100644 --- a/tests/account-check-ipc-sanitizer.test.ts +++ b/tests/account-check-ipc-sanitizer.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { AppController } from "../src/main/app-controller"; import { defaultSettings } from "../src/main/constants"; +import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; import type { AppSettings, DebridAccountStatus } from "../src/shared/types"; vi.mock("electron", () => ({ @@ -88,4 +89,18 @@ Backup-Passphrase=${echoedPassphrase}`; expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]); expect((status as DebridAccountStatus).message).toContain("[geschützt]"); }); + + it("persists a direct status refresh for an existing account", async () => { + const settings = defaultSettings(); + settings.debridLinkApiKeys = "existing-account-token"; + const accountId = parseDebridLinkApiKeys(settings.debridLinkApiKeys)[0].id; + mockFetchOnce(200, { success: true, value: { username: "active-user", accountType: 1 } }); + const controller = createController(settings); + + const status = await controller.checkAccountCredentials({ kind: "debridlink-api", accountId }); + + expect(status).toEqual(expect.objectContaining({ accountId, valid: true, username: "active-user" })); + expect((controller as unknown as { manager: { applyDebridAccountStatuses: ReturnType } }).manager.applyDebridAccountStatuses) + .toHaveBeenCalledWith([expect.objectContaining({ accountId, valid: true })]); + }); }); diff --git a/tests/account-ui.test.ts b/tests/account-ui.test.ts index 67d6ff1..b84bb3f 100644 --- a/tests/account-ui.test.ts +++ b/tests/account-ui.test.ts @@ -9,6 +9,7 @@ import { pruneAccountRowSelection, resolveAccountUsername, resolveVisibleAccountKind, + runAccountEnableRefresh, sortAccountServices } from "../src/renderer/account-ui"; import * as accountUi from "../src/renderer/account-ui"; @@ -134,3 +135,27 @@ describe("account usernames", () => { expect(resolveAccountUsername("", undefined)).toBe("—"); }); }); + +describe("account activation refresh", () => { + it("checks an account before enabling it", async () => { + const events: string[] = []; + + await runAccountEnableRefresh( + async () => { events.push("check"); }, + async () => { events.push("persist"); } + ); + + expect(events).toEqual(["check", "persist"]); + }); + + it("does not check an account while disabling it", async () => { + const events: string[] = []; + + await runAccountEnableRefresh( + undefined, + async () => { events.push("persist"); } + ); + + expect(events).toEqual(["persist"]); + }); +});