fix(accounts): separate Debrid-Link username and email

Persist the username and email returned by the Debrid-Link account endpoint as distinct identity fields and project them into their matching account-table columns. Migrate cached legacy statuses that stored a username in the email field, preserve both fields through renderer validation and sanitization, and cover API responses with and without an email address.
This commit is contained in:
Sucukdeluxe
2026-08-15 02:11:59 +02:00
parent 90fc77f203
commit d91e2a4ab9
10 changed files with 99 additions and 42 deletions
+3 -1
View File
@@ -247,12 +247,14 @@ export async function checkDebridLinkKey(
const premiumUntilMs = Number.isFinite(premiumLeftSec) && premiumLeftSec > 0 ? now + premiumLeftSec * 1000 : 0; const premiumUntilMs = Number.isFinite(premiumLeftSec) && premiumLeftSec > 0 ? now + premiumLeftSec * 1000 : 0;
const isPremium = premiumUntilMs > now || accountType > 0; const isPremium = premiumUntilMs > now || accountType > 0;
const username = String(value.username || "").trim() || undefined; const username = String(value.username || "").trim() || undefined;
const email = String(value.email || "").trim() || undefined;
return { return {
...base, ...base,
valid: true, valid: true,
isPremium, isPremium,
premiumUntilMs: premiumUntilMs > 0 ? premiumUntilMs : (accountType > 0 ? null : 0), premiumUntilMs: premiumUntilMs > 0 ? premiumUntilMs : (accountType > 0 ? null : 0),
email: username, username,
email,
message: premiumUntilMs > 0 message: premiumUntilMs > 0
? formatRemaining(premiumUntilMs, now) ? formatRemaining(premiumUntilMs, now)
: (accountType > 0 ? "Premium aktiv" : "Kein Premium (Free)") : (accountType > 0 ? "Premium aktiv" : "Kein Premium (Free)")
+1
View File
@@ -103,6 +103,7 @@ export function sanitizeDebridAccountStatus(status: DebridAccountStatus, redacti
accountId: sanitizeAccountStatusText(status.accountId, redactions), accountId: sanitizeAccountStatusText(status.accountId, redactions),
label: sanitizeAccountStatusText(status.label, redactions), label: sanitizeAccountStatusText(status.label, redactions),
maskedLogin: sanitizeAccountStatusText(status.maskedLogin, redactions), maskedLogin: sanitizeAccountStatusText(status.maskedLogin, redactions),
username: status.username ? sanitizeAccountStatusText(status.username, redactions) : undefined,
email: status.email ? sanitizeAccountStatusText(status.email, redactions) : undefined, email: status.email ? sanitizeAccountStatusText(status.email, redactions) : undefined,
message: sanitizeAccountStatusText(status.message, redactions) message: sanitizeAccountStatusText(status.message, redactions)
}; };
+13 -5
View File
@@ -283,19 +283,27 @@ function normalizeDebridAccountStatuses(
if (typeof entry.accountId !== "string" || typeof entry.checkedAt !== "number") { if (typeof entry.accountId !== "string" || typeof entry.checkedAt !== "number") {
continue; continue;
} }
result[key] = { const provider = entry.provider === "debridlink"
accountId: entry.accountId,
provider: entry.provider === "debridlink"
? "debridlink" ? "debridlink"
: entry.provider === "realdebrid" : entry.provider === "realdebrid"
? "realdebrid" ? "realdebrid"
: "megadebrid", : "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 || ""), label: String(entry.label || ""),
maskedLogin: String(entry.maskedLogin || ""), maskedLogin: String(entry.maskedLogin || ""),
valid: Boolean(entry.valid), valid: Boolean(entry.valid),
isPremium: Boolean(entry.isPremium), isPremium: Boolean(entry.isPremium),
premiumUntilMs: typeof entry.premiumUntilMs === "number" ? entry.premiumUntilMs : null, premiumUntilMs: typeof entry.premiumUntilMs === "number" ? entry.premiumUntilMs : null,
email: typeof entry.email === "string" ? entry.email : undefined, username,
email,
message: String(entry.message || ""), message: String(entry.message || ""),
checkedAt: entry.checkedAt checkedAt: entry.checkedAt
}; };
+4 -2
View File
@@ -2952,8 +2952,9 @@ export function App(): ReactElement {
setAccountContextMenu(null); setAccountContextMenu(null);
void (async () => { void (async () => {
const row = rows[0]; const row = rows[0];
const checkedStatus = row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId] : undefined;
const username = rows.length === 1 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({ const confirmed = await askConfirmPrompt({
title: rows.length === 1 ? `${row.hosterLabel} entfernen` : `${rows.length} Accounts entfernen`, title: rows.length === 1 ? `${row.hosterLabel} entfernen` : `${rows.length} Accounts entfernen`,
@@ -4893,6 +4894,7 @@ export function App(): ReactElement {
state, state,
message: checkedStatus?.message || row.entry.statusLabel, message: checkedStatus?.message || row.entry.statusLabel,
premiumUntilMs: checkedStatus?.premiumUntilMs ?? null, premiumUntilMs: checkedStatus?.premiumUntilMs ?? null,
username: checkedStatus?.username,
email: checkedStatus?.email email: checkedStatus?.email
}, },
dailyLimitBytes: row.dailyLimitBytes, dailyLimitBytes: row.dailyLimitBytes,
@@ -5331,7 +5333,7 @@ export function App(): ReactElement {
open: true, open: true,
hoster: accountEditOption.serviceLabel, hoster: accountEditOption.serviceLabel,
mode: accountEditOption.modeLabel, mode: accountEditOption.modeLabel,
identity: resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.email), identity: resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.username || accountEditStatus?.email),
enabled: !accountEditRow?.disabled, enabled: !accountEditRow?.disabled,
fields: accountEditFields, fields: accountEditFields,
error: "", error: "",
@@ -136,6 +136,7 @@ export interface AccountRowSource {
state: AccountStatusSourceState; state: AccountStatusSourceState;
message: string; message: string;
premiumUntilMs: number | null; premiumUntilMs: number | null;
username?: string;
email?: string; email?: string;
}; };
dailyLimitBytes?: number; dailyLimitBytes?: number;
@@ -654,8 +655,9 @@ function projectCredential(kind: AccountRowSource["credentialKind"]): string {
return kind === "password" ? "••••••" : "Geschützter Zugang"; return kind === "password" ? "••••••" : "Geschützter Zugang";
} }
function projectAccountIdentity(username: string, checkedEmail?: string): { username: string; email: string } { function projectAccountIdentity(username: string, checkedUsername?: string, checkedEmail?: string): { username: string; email: string } {
const stored = username.trim(); const verifiedUsername = checkedUsername?.trim() || "";
const stored = verifiedUsername || username.trim();
const verifiedEmail = checkedEmail?.trim() || ""; const verifiedEmail = checkedEmail?.trim() || "";
const storedIsEmail = stored.includes("@"); const storedIsEmail = stored.includes("@");
return { return {
@@ -676,7 +678,7 @@ export function projectAccountRows(
const premiumUntilMs = source.status.premiumUntilMs && source.status.premiumUntilMs > nowMs const premiumUntilMs = source.status.premiumUntilMs && source.status.premiumUntilMs > nowMs
? source.status.premiumUntilMs ? source.status.premiumUntilMs
: null; : null;
const identity = projectAccountIdentity(source.username, source.status.email); const identity = projectAccountIdentity(source.username, source.status.username, source.status.email);
return { return {
id, id,
service: source.service, service: source.service,
+1
View File
@@ -63,6 +63,7 @@ export interface DebridAccountStatus {
valid: boolean; valid: boolean;
isPremium: boolean; isPremium: boolean;
premiumUntilMs: number | null; premiumUntilMs: number | null;
username?: string;
email?: string; email?: string;
message: string; message: string;
checkedAt: number; checkedAt: number;
+10 -1
View File
@@ -83,11 +83,20 @@ describe("checkMegaDebridAccount", () => {
describe("checkDebridLinkKey", () => { describe("checkDebridLinkKey", () => {
it("reports valid + premium from premiumLeft seconds", async () => { it("reports valid + premium from premiumLeft seconds", async () => {
const premiumLeft = 60 * 24 * 60 * 60; const premiumLeft = 60 * 24 * 60 * 60;
mockFetchOnce(200, { success: true, value: { username: "u", accountType: 1, premiumLeft } }); mockFetchOnce(200, { success: true, value: { username: "u", email: "u@example.test", accountType: 1, premiumLeft } });
const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW); const st = await checkDebridLinkKey(debridLinkKey(), undefined, NOW);
expect(st.valid).toBe(true); expect(st.valid).toBe(true);
expect(st.isPremium).toBe(true); expect(st.isPremium).toBe(true);
expect(st.premiumUntilMs).toBe(NOW + premiumLeft * 1000); 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 () => { it("reports valid but free (premiumLeft 0, accountType 0)", async () => {
+2
View File
@@ -15,6 +15,7 @@ describe("renderer settings validation", () => {
valid: true, valid: true,
isPremium: true, isPremium: true,
premiumUntilMs: null, premiumUntilMs: null,
username: "web-user",
message: "Premium aktiv", message: "Premium aktiv",
checkedAt: 1_700_000_000_000 checkedAt: 1_700_000_000_000
} }
@@ -22,6 +23,7 @@ describe("renderer settings validation", () => {
const rendererSettings = createRendererSettings(current); const rendererSettings = createRendererSettings(current);
expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("email", undefined); expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("email", undefined);
expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("username", "web-user");
const validated = validateRendererSettingsUpdate(rendererSettings, current); const validated = validateRendererSettingsUpdate(rendererSettings, current);
expect(validated.debridAccountStatuses).toEqual(rendererSettings.debridAccountStatuses); expect(validated.debridAccountStatuses).toEqual(rendererSettings.debridAccountStatuses);
+4 -2
View File
@@ -153,10 +153,10 @@ function accountSources(): AccountRowSource[] {
mode: "API-Key", mode: "API-Key",
icon: "./provider-icons/debrid-link.ico", icon: "./provider-icons/debrid-link.ico",
enabled: true, enabled: true,
status: { state: "free", message: "Free Account", premiumUntilMs: null }, status: { state: "free", message: "Free Account", premiumUntilMs: null, username: "xsucukde5" },
dailyLimitBytes: 0, dailyLimitBytes: 0,
dailyUsageBytes: 0, dailyUsageBytes: 0,
username: "free-user", username: "",
credentialKind: "api-key", credentialKind: "api-key",
canCheck: true canCheck: true
}, },
@@ -352,6 +352,8 @@ describe("settings model", () => {
expect(rows[0].username).toBe("stored-user"); expect(rows[0].username).toBe("stored-user");
expect(rows[0].email).toBe("verified@example.test"); expect(rows[0].email).toBe("verified@example.test");
expect(rows[0].credential).toBe("••••••"); 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[1].credential).toBe("API-Key");
expect(rows.map((row) => row.status.tone)).toEqual(["ok", "free", "invalid", "unknown", "disabled"]); expect(rows.map((row) => row.status.tone)).toEqual(["ok", "free", "invalid", "unknown", "disabled"]);
expect(rows.map((row) => row.status.text)).toEqual([ expect(rows.map((row) => row.status.text)).toEqual([
+29 -1
View File
@@ -673,7 +673,8 @@ describe("settings storage", () => {
valid: true, valid: true,
isPremium: true, isPremium: true,
premiumUntilMs: checkedAt + 1000, premiumUntilMs: checkedAt + 1000,
email: "web-user", username: "web-user",
email: "w***r@example.test",
message: "Premium aktiv", message: "Premium aktiv",
checkedAt checkedAt
} }
@@ -684,10 +685,37 @@ describe("settings storage", () => {
accountId: "svc-realdebrid", accountId: "svc-realdebrid",
provider: "realdebrid", provider: "realdebrid",
valid: true, valid: true,
username: "web-user",
email: "w***r@example.test",
checkedAt 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", () => { it("defaults AllDebrid web login to disabled and normalizes the flag", () => {
expect(defaultSettings().allDebridUseWebLogin).toBe(false); expect(defaultSettings().allDebridUseWebLogin).toBe(false);