Handle device name decryption errors gracefully

Added error handling for device name decryption in Avatar, LinksDropdown, Settings, and DeviceListPart components. If decryption fails, a fallback 'Unknown device' message is shown using a new translation key. This improves user experience by preventing crashes or blank fields when device name decryption fails.
This commit is contained in:
Pas 2025-11-17 13:30:25 -07:00
parent a76f25fcea
commit c329118e50
5 changed files with 47 additions and 9 deletions

View file

@ -981,6 +981,7 @@
},
"devices": {
"deviceNameLabel": "Device name",
"unknownDevice": "Unknown device, error decrypting name",
"failed": "Failed to load sessions",
"removeDevice": "Remove",
"title": "Devices"

View file

@ -1,5 +1,6 @@
import classNames from "classnames";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
import { Icon, Icons } from "@/components/Icon";
@ -55,11 +56,22 @@ export function UserAvatar(props: {
: null,
[auth],
);
const { t } = useTranslation();
if (!auth.account || auth.account === null) return null;
const deviceName = bufferSeed
? decryptData(auth.account.deviceName, bufferSeed)
? (() => {
try {
return decryptData(auth.account.deviceName, bufferSeed);
} catch (error) {
console.warn(
"Failed to decrypt device name in Avatar, using fallback:",
error,
);
return t("settings.account.devices.unknownDevice");
}
})()
: "...";
return (

View file

@ -257,7 +257,17 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
{deviceName && bufferSeed ? (
<DropdownLink className="text-white" href="/settings">
<UserAvatar />
{decryptData(deviceName, bufferSeed)}
{(() => {
try {
return decryptData(deviceName, bufferSeed);
} catch (error) {
console.warn(
"Failed to decrypt device name in LinksDropdown, using fallback:",
error,
);
return t("settings.account.unknownDevice");
}
})()}
</DropdownLink>
) : (
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight>

View file

@ -490,8 +490,14 @@ export function SettingsPage() {
const updateNickname = useAuthStore((s) => s.setAccountNickname);
const decryptedName = useMemo(() => {
if (!account) return "";
return decryptData(account.deviceName, base64ToBuffer(account.seed));
}, [account]);
try {
return decryptData(account.deviceName, base64ToBuffer(account.seed));
} catch (error) {
console.warn("Failed to decrypt device name, using fallback:", error);
// Return a fallback device name if decryption fails
return t("settings.account.devices.unknownDevice");
}
}, [account, t]);
const backendUrl = useBackendUrl();

View file

@ -75,7 +75,16 @@ export function DeviceListPart(props: {
const deviceListSorted = useMemo(() => {
if (!seed) return [];
let list = sessions.map((session) => {
const decryptedName = decryptData(session.device, base64ToBuffer(seed));
let decryptedName: string;
try {
decryptedName = decryptData(session.device, base64ToBuffer(seed));
} catch (error) {
console.warn(
`Failed to decrypt device name for session ${session.id}:`,
error,
);
decryptedName = t("settings.account.devices.unknownDevice");
}
return {
current: session.id === currentSessionId,
id: session.id,
@ -88,7 +97,7 @@ export function DeviceListPart(props: {
return a.name.localeCompare(b.name);
});
return list;
}, [seed, sessions, currentSessionId]);
}, [seed, sessions, currentSessionId, t]);
if (!seed) return null;
return (
@ -96,10 +105,10 @@ export function DeviceListPart(props: {
<Heading2 border className="mt-0 mb-9">
{t("settings.account.devices.title")}
</Heading2>
{props.error ? (
<p>{t("settings.account.devices.failed")}</p>
) : props.loading ? (
{props.loading ? (
<Loading />
) : props.error && deviceListSorted.length === 0 ? (
<p>{t("settings.account.devices.failed")}</p>
) : (
<div className="space-y-5">
{deviceListSorted.map((session) => (