Compare commits
3 commits
cf31666c35
...
745f71782f
| Author | SHA1 | Date | |
|---|---|---|---|
| 745f71782f | |||
|
|
9a9d3c89b9 | ||
|
|
3b1c41c48e |
5 changed files with 102 additions and 8 deletions
69
src/components/settings/TokenStoreFallbackBanner.tsx
Normal file
69
src/components/settings/TokenStoreFallbackBanner.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ShieldAlert, X } from "lucide-react";
|
||||||
|
import { getTokenStoreMode, TokenStoreMode } from "../../services/authService";
|
||||||
|
|
||||||
|
// Per-session dismissal flag. Kept in sessionStorage so the banner
|
||||||
|
// returns on the next app launch if the fallback condition still
|
||||||
|
// holds — this matches the acceptance criteria from issue #81.
|
||||||
|
const DISMISS_KEY = "tokenStoreFallbackBannerDismissed";
|
||||||
|
|
||||||
|
export default function TokenStoreFallbackBanner() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [mode, setMode] = useState<TokenStoreMode | null>(null);
|
||||||
|
const [dismissed, setDismissed] = useState<boolean>(() => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(DISMISS_KEY) === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
getTokenStoreMode()
|
||||||
|
.then((m) => {
|
||||||
|
if (!cancelled) setMode(m);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setMode(null);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (mode !== "file" || dismissed) return null;
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(DISMISS_KEY, "1");
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors — the banner will simply hide for the
|
||||||
|
// remainder of this render cycle via state.
|
||||||
|
}
|
||||||
|
setDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
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="flex-1 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>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={dismiss}
|
||||||
|
aria-label={t("common.close")}
|
||||||
|
className="shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -847,7 +847,8 @@
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
"lightMode": "Light mode"
|
"lightMode": "Light mode",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "License",
|
"title": "License",
|
||||||
|
|
@ -882,6 +883,12 @@
|
||||||
"description": "Sign in to access Premium features (web version, sync). The account is only required for Premium features.",
|
"description": "Sign in to access Premium features (web version, sync). The account is only required for Premium features.",
|
||||||
"signIn": "Sign in",
|
"signIn": "Sign in",
|
||||||
"signOut": "Sign out",
|
"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",
|
"byCategory": "Dépenses par catégorie",
|
||||||
"overTime": "Catégories dans le temps",
|
"overTime": "Catégories dans le temps",
|
||||||
"trends": "Tendances mensuelles",
|
"trends": "Tendances mensuelles",
|
||||||
"budgetVsActual": "Budget vs R\u00e9el",
|
"budgetVsActual": "Budget vs Réel",
|
||||||
"subtotalsOnTop": "Sous-totaux en haut",
|
"subtotalsOnTop": "Sous-totaux en haut",
|
||||||
"subtotalsOnBottom": "Sous-totaux en bas",
|
"subtotalsOnBottom": "Sous-totaux en bas",
|
||||||
"detail": {
|
"detail": {
|
||||||
|
|
@ -376,9 +376,9 @@
|
||||||
"bva": {
|
"bva": {
|
||||||
"monthly": "Mensuel",
|
"monthly": "Mensuel",
|
||||||
"ytd": "Cumul annuel",
|
"ytd": "Cumul annuel",
|
||||||
"dollarVar": "$ \u00c9cart",
|
"dollarVar": "$ Écart",
|
||||||
"pctVar": "% \u00c9cart",
|
"pctVar": "% Écart",
|
||||||
"noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode.",
|
"noData": "Aucune donnée de budget ou de transaction pour cette période.",
|
||||||
"titlePrefix": "Budget vs Réel pour le mois de"
|
"titlePrefix": "Budget vs Réel pour le mois de"
|
||||||
},
|
},
|
||||||
"dynamic": "Rapport dynamique",
|
"dynamic": "Rapport dynamique",
|
||||||
|
|
@ -847,7 +847,8 @@
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"darkMode": "Mode sombre",
|
"darkMode": "Mode sombre",
|
||||||
"lightMode": "Mode clair"
|
"lightMode": "Mode clair",
|
||||||
|
"close": "Fermer"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "Licence",
|
"title": "Licence",
|
||||||
|
|
@ -882,6 +883,12 @@
|
||||||
"description": "Connectez-vous pour accéder aux fonctionnalités Premium (version web, synchronisation). Le compte est requis uniquement pour les fonctionnalités Premium.",
|
"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",
|
"signIn": "Se connecter",
|
||||||
"signOut": "Se dé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 LicenseCard from "../components/settings/LicenseCard";
|
||||||
import AccountCard from "../components/settings/AccountCard";
|
import AccountCard from "../components/settings/AccountCard";
|
||||||
import LogViewerCard from "../components/settings/LogViewerCard";
|
import LogViewerCard from "../components/settings/LogViewerCard";
|
||||||
|
import TokenStoreFallbackBanner from "../components/settings/TokenStoreFallbackBanner";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
@ -80,6 +81,10 @@ export default function SettingsPage() {
|
||||||
{/* Account card */}
|
{/* Account card */}
|
||||||
<AccountCard />
|
<AccountCard />
|
||||||
|
|
||||||
|
{/* Security banner — renders only when OAuth tokens are in the
|
||||||
|
file fallback instead of the OS keychain */}
|
||||||
|
<TokenStoreFallbackBanner />
|
||||||
|
|
||||||
{/* About card */}
|
{/* About card */}
|
||||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6">
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,9 @@ export async function checkSubscriptionStatus(): Promise<AccountInfo | null> {
|
||||||
export async function logoutAccount(): Promise<void> {
|
export async function logoutAccount(): Promise<void> {
|
||||||
return invoke<void>("logout");
|
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