feat: settings banner when OAuth tokens fall back to file store (#81)
Adds a visible warning in the Settings page when `token_store` has landed in the file fallback instead of the OS keychain. Without this, a user on a keychain-less system would silently lose the security benefit introduced in #78 and never know. - New `get_token_store_mode` service wrapper in authService.ts. - New `TokenStoreFallbackBanner` component: fetches the mode on mount, renders nothing when mode is `keychain` or null, renders an amber warning card when mode is `file`. - Mounted in SettingsPage right after AccountCard so it sits next to the account state the user can fix (log out + log back in once the keychain is available). - i18n keys under `account.tokenStore.fallback.*` in fr/en. Refs #66 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cf31666c35
commit
3b1c41c48e
5 changed files with 68 additions and 6 deletions
39
src/components/settings/TokenStoreFallbackBanner.tsx
Normal file
39
src/components/settings/TokenStoreFallbackBanner.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
import { getTokenStoreMode, TokenStoreMode } from "../../services/authService";
|
||||
|
||||
export default function TokenStoreFallbackBanner() {
|
||||
const { t } = useTranslation();
|
||||
const [mode, setMode] = useState<TokenStoreMode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getTokenStoreMode()
|
||||
.then((m) => {
|
||||
if (!cancelled) setMode(m);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setMode(null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (mode !== "file") return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4">
|
||||
<ShieldAlert size={20} className="mt-0.5 shrink-0 text-amber-500" />
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="font-semibold text-[var(--foreground)]">
|
||||
{t("account.tokenStore.fallback.title")}
|
||||
</p>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
{t("account.tokenStore.fallback.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -882,6 +882,12 @@
|
|||
"description": "Sign in to access Premium features (web version, sync). The account is only required for Premium features.",
|
||||
"signIn": "Sign in",
|
||||
"signOut": "Sign out",
|
||||
"connected": "Connected"
|
||||
"connected": "Connected",
|
||||
"tokenStore": {
|
||||
"fallback": {
|
||||
"title": "Tokens stored in plaintext fallback",
|
||||
"description": "Your authentication tokens are currently stored in a local file protected by filesystem permissions. For stronger protection via the OS keychain, make sure a keyring service is running (GNOME Keyring, KWallet, or equivalent)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@
|
|||
"byCategory": "Dépenses par catégorie",
|
||||
"overTime": "Catégories dans le temps",
|
||||
"trends": "Tendances mensuelles",
|
||||
"budgetVsActual": "Budget vs R\u00e9el",
|
||||
"budgetVsActual": "Budget vs Réel",
|
||||
"subtotalsOnTop": "Sous-totaux en haut",
|
||||
"subtotalsOnBottom": "Sous-totaux en bas",
|
||||
"detail": {
|
||||
|
|
@ -376,9 +376,9 @@
|
|||
"bva": {
|
||||
"monthly": "Mensuel",
|
||||
"ytd": "Cumul annuel",
|
||||
"dollarVar": "$ \u00c9cart",
|
||||
"pctVar": "% \u00c9cart",
|
||||
"noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode.",
|
||||
"dollarVar": "$ Écart",
|
||||
"pctVar": "% Écart",
|
||||
"noData": "Aucune donnée de budget ou de transaction pour cette période.",
|
||||
"titlePrefix": "Budget vs Réel pour le mois de"
|
||||
},
|
||||
"dynamic": "Rapport dynamique",
|
||||
|
|
@ -882,6 +882,12 @@
|
|||
"description": "Connectez-vous pour accéder aux fonctionnalités Premium (version web, synchronisation). Le compte est requis uniquement pour les fonctionnalités Premium.",
|
||||
"signIn": "Se connecter",
|
||||
"signOut": "Se déconnecter",
|
||||
"connected": "Connecté"
|
||||
"connected": "Connecté",
|
||||
"tokenStore": {
|
||||
"fallback": {
|
||||
"title": "Stockage des tokens en clair",
|
||||
"description": "Vos jetons d'authentification sont stockés dans un fichier local protégé par les permissions du système. Pour une protection renforcée via le trousseau du système d'exploitation, vérifiez que le service de trousseau est disponible (GNOME Keyring, KWallet, ou équivalent)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import DataManagementCard from "../components/settings/DataManagementCard";
|
|||
import LicenseCard from "../components/settings/LicenseCard";
|
||||
import AccountCard from "../components/settings/AccountCard";
|
||||
import LogViewerCard from "../components/settings/LogViewerCard";
|
||||
import TokenStoreFallbackBanner from "../components/settings/TokenStoreFallbackBanner";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
|
@ -80,6 +81,10 @@ export default function SettingsPage() {
|
|||
{/* Account card */}
|
||||
<AccountCard />
|
||||
|
||||
{/* Security banner — renders only when OAuth tokens are in the
|
||||
file fallback instead of the OS keychain */}
|
||||
<TokenStoreFallbackBanner />
|
||||
|
||||
{/* About card */}
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -26,3 +26,9 @@ export async function checkSubscriptionStatus(): Promise<AccountInfo | null> {
|
|||
export async function logoutAccount(): Promise<void> {
|
||||
return invoke<void>("logout");
|
||||
}
|
||||
|
||||
export type TokenStoreMode = "keychain" | "file";
|
||||
|
||||
export async function getTokenStoreMode(): Promise<TokenStoreMode | null> {
|
||||
return invoke<TokenStoreMode | null>("get_token_store_mode");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue