diff --git a/src/main/account-check.ts b/src/main/account-check.ts index 7c716fe..51b2fe7 100644 --- a/src/main/account-check.ts +++ b/src/main/account-check.ts @@ -245,15 +245,17 @@ export async function checkDebridLinkKey( const premiumLeftSec = Number(value.premiumLeft || 0); const accountType = Number(value.accountType || 0); const premiumUntilMs = Number.isFinite(premiumLeftSec) && premiumLeftSec > 0 ? now + premiumLeftSec * 1000 : 0; - const isPremium = premiumUntilMs > now || accountType > 0; - const username = String(value.username || "").trim() || undefined; - return { - ...base, - valid: true, - isPremium, - premiumUntilMs: premiumUntilMs > 0 ? premiumUntilMs : (accountType > 0 ? null : 0), - email: username, - message: premiumUntilMs > 0 + const isPremium = premiumUntilMs > now || accountType > 0; + const username = String(value.username || "").trim() || undefined; + const email = String(value.email || "").trim() || undefined; + return { + ...base, + valid: true, + isPremium, + premiumUntilMs: premiumUntilMs > 0 ? premiumUntilMs : (accountType > 0 ? null : 0), + username, + email, + message: premiumUntilMs > 0 ? formatRemaining(premiumUntilMs, now) : (accountType > 0 ? "Premium aktiv" : "Kein Premium (Free)") }; diff --git a/src/main/account-status-sanitizer.ts b/src/main/account-status-sanitizer.ts index 2f4a50f..8dd7128 100644 --- a/src/main/account-status-sanitizer.ts +++ b/src/main/account-status-sanitizer.ts @@ -103,6 +103,7 @@ export function sanitizeDebridAccountStatus(status: DebridAccountStatus, redacti accountId: sanitizeAccountStatusText(status.accountId, redactions), label: sanitizeAccountStatusText(status.label, redactions), maskedLogin: sanitizeAccountStatusText(status.maskedLogin, redactions), + username: status.username ? sanitizeAccountStatusText(status.username, redactions) : undefined, email: status.email ? sanitizeAccountStatusText(status.email, redactions) : undefined, message: sanitizeAccountStatusText(status.message, redactions) }; diff --git a/src/main/storage.ts b/src/main/storage.ts index e329a53..0c1b1ff 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -280,22 +280,30 @@ function normalizeDebridAccountStatuses( continue; } const entry = raw as Partial; - if (typeof entry.accountId !== "string" || typeof entry.checkedAt !== "number") { - continue; - } - result[key] = { - accountId: entry.accountId, - provider: entry.provider === "debridlink" - ? "debridlink" - : entry.provider === "realdebrid" - ? "realdebrid" - : "megadebrid", + if (typeof entry.accountId !== "string" || typeof entry.checkedAt !== "number") { + continue; + } + const provider = entry.provider === "debridlink" + ? "debridlink" + : entry.provider === "realdebrid" + ? "realdebrid" + : "megadebrid"; + let username = typeof entry.username === "string" ? entry.username : undefined; + let email = typeof entry.email === "string" ? entry.email : undefined; + if (provider === "debridlink" && !username && email && !email.includes("@")) { + username = email; + email = undefined; + } + result[key] = { + accountId: entry.accountId, + provider, label: String(entry.label || ""), maskedLogin: String(entry.maskedLogin || ""), valid: Boolean(entry.valid), - isPremium: Boolean(entry.isPremium), - premiumUntilMs: typeof entry.premiumUntilMs === "number" ? entry.premiumUntilMs : null, - email: typeof entry.email === "string" ? entry.email : undefined, + isPremium: Boolean(entry.isPremium), + premiumUntilMs: typeof entry.premiumUntilMs === "number" ? entry.premiumUntilMs : null, + username, + email, message: String(entry.message || ""), checkedAt: entry.checkedAt }; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index e63c2b0..3908234 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2952,8 +2952,9 @@ export function App(): ReactElement { setAccountContextMenu(null); void (async () => { const row = rows[0]; + const checkedStatus = row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId] : undefined; const username = rows.length === 1 - ? resolveAccountUsername(row.username, row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId]?.email : undefined) + ? resolveAccountUsername(row.username, checkedStatus?.username || checkedStatus?.email) : "—"; const confirmed = await askConfirmPrompt({ title: rows.length === 1 ? `${row.hosterLabel} entfernen` : `${rows.length} Accounts entfernen`, @@ -4893,6 +4894,7 @@ export function App(): ReactElement { state, message: checkedStatus?.message || row.entry.statusLabel, premiumUntilMs: checkedStatus?.premiumUntilMs ?? null, + username: checkedStatus?.username, email: checkedStatus?.email }, dailyLimitBytes: row.dailyLimitBytes, @@ -5331,7 +5333,7 @@ export function App(): ReactElement { open: true, hoster: accountEditOption.serviceLabel, mode: accountEditOption.modeLabel, - identity: resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.email), + identity: resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.username || accountEditStatus?.email), enabled: !accountEditRow?.disabled, fields: accountEditFields, error: "", diff --git a/src/renderer/views/settings/settings-model.ts b/src/renderer/views/settings/settings-model.ts index 8bec352..ee111b0 100644 --- a/src/renderer/views/settings/settings-model.ts +++ b/src/renderer/views/settings/settings-model.ts @@ -136,6 +136,7 @@ export interface AccountRowSource { state: AccountStatusSourceState; message: string; premiumUntilMs: number | null; + username?: string; email?: string; }; dailyLimitBytes?: number; @@ -654,8 +655,9 @@ function projectCredential(kind: AccountRowSource["credentialKind"]): string { return kind === "password" ? "••••••" : "Geschützter Zugang"; } -function projectAccountIdentity(username: string, checkedEmail?: string): { username: string; email: string } { - const stored = username.trim(); +function projectAccountIdentity(username: string, checkedUsername?: string, checkedEmail?: string): { username: string; email: string } { + const verifiedUsername = checkedUsername?.trim() || ""; + const stored = verifiedUsername || username.trim(); const verifiedEmail = checkedEmail?.trim() || ""; const storedIsEmail = stored.includes("@"); return { @@ -676,7 +678,7 @@ export function projectAccountRows( const premiumUntilMs = source.status.premiumUntilMs && source.status.premiumUntilMs > nowMs ? source.status.premiumUntilMs : null; - const identity = projectAccountIdentity(source.username, source.status.email); + const identity = projectAccountIdentity(source.username, source.status.username, source.status.email); return { id, service: source.service, diff --git a/src/shared/types.ts b/src/shared/types.ts index 7886ebe..6202d48 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -61,10 +61,11 @@ export interface DebridAccountStatus { label: string; maskedLogin: string; valid: boolean; - isPremium: boolean; - premiumUntilMs: number | null; - email?: string; - message: string; + isPremium: boolean; + premiumUntilMs: number | null; + username?: string; + email?: string; + message: string; checkedAt: number; } diff --git a/tests/account-check.test.ts b/tests/account-check.test.ts index 06d7ccd..6a76537 100644 --- a/tests/account-check.test.ts +++ b/tests/account-check.test.ts @@ -81,14 +81,23 @@ describe("checkMegaDebridAccount", () => { }); describe("checkDebridLinkKey", () => { - it("reports valid + premium from premiumLeft seconds", async () => { - const premiumLeft = 60 * 24 * 60 * 60; - mockFetchOnce(200, { success: true, value: { username: "u", accountType: 1, premiumLeft } }); - const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW); - expect(st.valid).toBe(true); - expect(st.isPremium).toBe(true); - expect(st.premiumUntilMs).toBe(NOW + premiumLeft * 1000); - }); + it("reports valid + premium from premiumLeft seconds", async () => { + const premiumLeft = 60 * 24 * 60 * 60; + mockFetchOnce(200, { success: true, value: { username: "u", email: "u@example.test", accountType: 1, premiumLeft } }); + const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW); + expect(st.valid).toBe(true); + expect(st.isPremium).toBe(true); + expect(st.premiumUntilMs).toBe(NOW + premiumLeft * 1000); + expect(st.username).toBe("u"); + expect(st.email).toBe("u@example.test"); + }); + + it("does not present a Debrid-Link username as an email address", async () => { + mockFetchOnce(200, { success: true, value: { username: "xsucukde5", accountType: 0, premiumLeft: 0 } }); + const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW); + expect(st.username).toBe("xsucukde5"); + expect(st.email).toBeUndefined(); + }); it("reports valid but free (premiumLeft 0, accountType 0)", async () => { mockFetchOnce(200, { success: true, value: { username: "u", accountType: 0, premiumLeft: 0 } }); diff --git a/tests/renderer-settings.test.ts b/tests/renderer-settings.test.ts index 64aeac8..3fbf0f6 100644 --- a/tests/renderer-settings.test.ts +++ b/tests/renderer-settings.test.ts @@ -15,6 +15,7 @@ describe("renderer settings validation", () => { valid: true, isPremium: true, premiumUntilMs: null, + username: "web-user", message: "Premium aktiv", checkedAt: 1_700_000_000_000 } @@ -22,6 +23,7 @@ describe("renderer settings validation", () => { const rendererSettings = createRendererSettings(current); expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("email", undefined); + expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("username", "web-user"); const validated = validateRendererSettingsUpdate(rendererSettings, current); expect(validated.debridAccountStatuses).toEqual(rendererSettings.debridAccountStatuses); diff --git a/tests/settings-view.test.tsx b/tests/settings-view.test.tsx index 858c171..2f239fb 100644 --- a/tests/settings-view.test.tsx +++ b/tests/settings-view.test.tsx @@ -153,10 +153,10 @@ function accountSources(): AccountRowSource[] { mode: "API-Key", icon: "./provider-icons/debrid-link.ico", enabled: true, - status: { state: "free", message: "Free Account", premiumUntilMs: null }, + status: { state: "free", message: "Free Account", premiumUntilMs: null, username: "xsucukde5" }, dailyLimitBytes: 0, dailyUsageBytes: 0, - username: "free-user", + username: "", credentialKind: "api-key", canCheck: true }, @@ -352,6 +352,8 @@ describe("settings model", () => { expect(rows[0].username).toBe("stored-user"); expect(rows[0].email).toBe("verified@example.test"); expect(rows[0].credential).toBe("••••••"); + expect(rows[1].username).toBe("xsucukde5"); + expect(rows[1].email).toBe("—"); expect(rows[1].credential).toBe("API-Key"); expect(rows.map((row) => row.status.tone)).toEqual(["ok", "free", "invalid", "unknown", "disabled"]); expect(rows.map((row) => row.status.text)).toEqual([ diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 1fd8b3f..0c9471a 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -673,7 +673,8 @@ describe("settings storage", () => { valid: true, isPremium: true, premiumUntilMs: checkedAt + 1000, - email: "web-user", + username: "web-user", + email: "w***r@example.test", message: "Premium aktiv", checkedAt } @@ -684,9 +685,36 @@ describe("settings storage", () => { accountId: "svc-realdebrid", provider: "realdebrid", valid: true, + username: "web-user", + email: "w***r@example.test", checkedAt }); }); + + it("migrates a legacy Debrid-Link username out of the email field", () => { + const [key] = parseDebridLinkApiKeys("dl-key-one"); + const normalized = normalizeSettings({ + ...defaultSettings(), + debridLinkApiKeys: "dl-key-one", + debridAccountStatuses: { + [key.id]: { + accountId: key.id, + provider: "debridlink", + label: "Key 1", + maskedLogin: key.masked, + valid: true, + isPremium: false, + premiumUntilMs: 0, + email: "xsucukde5", + message: "Kein Premium (Free)", + checkedAt: Date.now() + } + } + }); + + expect(normalized.debridAccountStatuses[key.id].username).toBe("xsucukde5"); + expect(normalized.debridAccountStatuses[key.id].email).toBeUndefined(); + }); it("defaults AllDebrid web login to disabled and normalizes the flag", () => { expect(defaultSettings().allDebridUseWebLogin).toBe(false);