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:
@@ -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)")
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-6
@@ -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;
|
||||||
}
|
}
|
||||||
|
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] = {
|
result[key] = {
|
||||||
accountId: entry.accountId,
|
accountId: entry.accountId,
|
||||||
provider: entry.provider === "debridlink"
|
provider,
|
||||||
? "debridlink"
|
|
||||||
: entry.provider === "realdebrid"
|
|
||||||
? "realdebrid"
|
|
||||||
: "megadebrid",
|
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user