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.
This commit is contained in:
@@ -598,15 +598,21 @@ export class AppController {
|
|||||||
? parseMegaDebridAccounts(`${input.identity.trim()}:${input.secret}`)[0]
|
? parseMegaDebridAccounts(`${input.identity.trim()}:${input.secret}`)[0]
|
||||||
: getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId);
|
: getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId);
|
||||||
if (!account) throw new Error("Account-Payload ist ungültig");
|
if (!account) throw new Error("Account-Payload ist ungültig");
|
||||||
const status = await checkMegaDebridAccount(account);
|
const status = sanitizeDebridAccountStatus(await checkMegaDebridAccount(account), redactions);
|
||||||
return sanitizeDebridAccountStatus(status, 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()
|
const key = input.secret?.trim()
|
||||||
? parseDebridLinkApiKeys(input.secret)[0]
|
? parseDebridLinkApiKeys(input.secret)[0]
|
||||||
: parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId);
|
: parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId);
|
||||||
if (!key) throw new Error("Account-Payload ist ungültig");
|
if (!key) throw new Error("Account-Payload ist ungültig");
|
||||||
const status = await checkDebridLinkKey(key);
|
const status = sanitizeDebridAccountStatus(await checkDebridLinkKey(key), redactions);
|
||||||
return sanitizeDebridAccountStatus(status, 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 {
|
public resetProviderDailyUsage(provider: DebridProvider): AppSettings {
|
||||||
|
|||||||
+23
-8
@@ -41,7 +41,7 @@ import {
|
|||||||
} from "../shared/provider-daily-limits";
|
} from "../shared/provider-daily-limits";
|
||||||
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
||||||
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection";
|
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 { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
|
||||||
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
||||||
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
|
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
|
||||||
@@ -2859,7 +2859,10 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistAccountToggle = async (nextDraft: RendererSettingsDraft): Promise<RendererSettings> => {
|
const persistAccountToggle = async (
|
||||||
|
nextDraft: RendererSettingsDraft,
|
||||||
|
refreshBeforePersist?: () => Promise<void>
|
||||||
|
): Promise<RendererSettings> => {
|
||||||
const previousDraft = settingsDraft;
|
const previousDraft = settingsDraft;
|
||||||
const previousDirty = settingsDirtyRef.current;
|
const previousDirty = settingsDirtyRef.current;
|
||||||
const previousSaveState = settingsSaveState;
|
const previousSaveState = settingsSaveState;
|
||||||
@@ -2871,7 +2874,10 @@ export function App(): ReactElement {
|
|||||||
setSettingsSaveState("saving");
|
setSettingsSaveState("saving");
|
||||||
setSettingsDraft(nextDraft);
|
setSettingsDraft(nextDraft);
|
||||||
},
|
},
|
||||||
() => persistSpecificSettings(nextDraft),
|
() => runAccountEnableRefresh(
|
||||||
|
refreshBeforePersist,
|
||||||
|
() => persistSpecificSettings(nextDraft)
|
||||||
|
),
|
||||||
() => {
|
() => {
|
||||||
if (settingsDraftRevisionRef.current !== revision) return;
|
if (settingsDraftRevisionRef.current !== revision) return;
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
@@ -2897,7 +2903,10 @@ export function App(): ReactElement {
|
|||||||
disabledProviders: nextState.disabledProviders,
|
disabledProviders: nextState.disabledProviders,
|
||||||
debridLinkDisabledKeyIds: nextState.disabledAccountIds
|
debridLinkDisabledKeyIds: nextState.disabledAccountIds
|
||||||
};
|
};
|
||||||
await persistAccountToggle(nextDraft);
|
await persistAccountToggle(
|
||||||
|
nextDraft,
|
||||||
|
enabled ? async () => { await window.rd.checkAccountCredentials({ kind: "debridlink-api", accountId: key.id }); } : undefined
|
||||||
|
);
|
||||||
showToast(
|
showToast(
|
||||||
enabled
|
enabled
|
||||||
? `${entry.serviceLabel} ${key.label} aktiviert`
|
? `${entry.serviceLabel} ${key.label} aktiviert`
|
||||||
@@ -2943,7 +2952,10 @@ export function App(): ReactElement {
|
|||||||
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
megaDebridWebDisabledAccountIds: webDisabledIds
|
megaDebridWebDisabledAccountIds: webDisabledIds
|
||||||
});
|
}, enabled
|
||||||
|
? async () => { await window.rd.checkAccountCredentials({ kind, accountId }); }
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
@@ -2984,7 +2996,7 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleRealDebridAccountEnabled = async (accountId: string, enabled: boolean): Promise<void> => {
|
const onToggleRealDebridAccountEnabled = async (kind: "realdebrid-api" | "realdebrid-web", accountId: string, enabled: boolean): Promise<void> => {
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const nextState = buildScopedAccountEnabledState(
|
const nextState = buildScopedAccountEnabledState(
|
||||||
settingsDraft.disabledProviders || [],
|
settingsDraft.disabledProviders || [],
|
||||||
@@ -2997,7 +3009,10 @@ export function App(): ReactElement {
|
|||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
disabledProviders: nextState.disabledProviders,
|
disabledProviders: nextState.disabledProviders,
|
||||||
realDebridDisabledAccountIds: nextState.disabledAccountIds
|
realDebridDisabledAccountIds: nextState.disabledAccountIds
|
||||||
});
|
}, enabled
|
||||||
|
? async () => { await window.rd.checkAccountCredentials({ kind, accountId }); }
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
@@ -3007,7 +3022,7 @@ export function App(): ReactElement {
|
|||||||
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
if (row.toggleKind === "rd" && row.accountId) {
|
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) {
|
} else if (row.toggleKind === "mega" && row.accountId) {
|
||||||
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
||||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||||
|
|||||||
@@ -145,6 +145,16 @@ export async function runOptimisticAccountUpdate<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function runAccountEnableRefresh<T>(
|
||||||
|
refresh: (() => Promise<void>) | undefined,
|
||||||
|
persist: () => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
if (refresh) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
return persist();
|
||||||
|
}
|
||||||
|
|
||||||
export function buildScopedAccountEnabledState(
|
export function buildScopedAccountEnabledState(
|
||||||
currentDisabledProviders: DebridProvider[],
|
currentDisabledProviders: DebridProvider[],
|
||||||
providerIds: DebridProvider[],
|
providerIds: DebridProvider[],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { AppController } from "../src/main/app-controller";
|
import { AppController } from "../src/main/app-controller";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
import type { AppSettings, DebridAccountStatus } from "../src/shared/types";
|
import type { AppSettings, DebridAccountStatus } from "../src/shared/types";
|
||||||
|
|
||||||
vi.mock("electron", () => ({
|
vi.mock("electron", () => ({
|
||||||
@@ -88,4 +89,18 @@ Backup-Passphrase=${echoedPassphrase}`;
|
|||||||
expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]);
|
expectNoSecret(status, [secret, encodedSecret, echoedHeaderSecret, echoedPassphrase]);
|
||||||
expect((status as DebridAccountStatus).message).toContain("[geschützt]");
|
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<typeof vi.fn> } }).manager.applyDebridAccountStatuses)
|
||||||
|
.toHaveBeenCalledWith([expect.objectContaining({ accountId, valid: true })]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
pruneAccountRowSelection,
|
pruneAccountRowSelection,
|
||||||
resolveAccountUsername,
|
resolveAccountUsername,
|
||||||
resolveVisibleAccountKind,
|
resolveVisibleAccountKind,
|
||||||
|
runAccountEnableRefresh,
|
||||||
sortAccountServices
|
sortAccountServices
|
||||||
} from "../src/renderer/account-ui";
|
} from "../src/renderer/account-ui";
|
||||||
import * as accountUi from "../src/renderer/account-ui";
|
import * as accountUi from "../src/renderer/account-ui";
|
||||||
@@ -134,3 +135,27 @@ describe("account usernames", () => {
|
|||||||
expect(resolveAccountUsername("", undefined)).toBe("—");
|
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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user