diff --git a/messages/de.json b/messages/de.json index 7c4a91435..628c3fd9f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5064,7 +5064,9 @@ "remainingOther": "{count} Wiederherstellungscodes übrig.", "regenerate": "Codes neu erzeugen", "regenFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden.", - "savedIt": "Ich habe sie gespeichert" + "savedIt": "Ich habe sie gespeichert", + "regenerateTitle": "Wiederherstellungscodes neu erzeugen?", + "regenerateConfirm": "Damit werden deine aktuellen Wiederherstellungscodes ersetzt. Deine bisherigen Codes funktionieren sofort nicht mehr, und der neue Satz wird nur einmal angezeigt." }, "keys": { "title": "Sicherheitsschlüssel", diff --git a/messages/en.json b/messages/en.json index b93195156..79eddff16 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5064,7 +5064,9 @@ "remainingOther": "{count} recovery codes remaining.", "regenerate": "Regenerate codes", "regenFailed": "Could not regenerate recovery codes.", - "savedIt": "I've saved them" + "savedIt": "I've saved them", + "regenerateTitle": "Regenerate recovery codes?", + "regenerateConfirm": "This replaces your current recovery codes. Your existing codes will stop working immediately, and a new set is shown only once." }, "keys": { "title": "Security keys", diff --git a/messages/es.json b/messages/es.json index 482776912..50f8d9dd4 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5064,7 +5064,9 @@ "remainingOther": "Quedan {count} códigos de recuperación.", "regenerate": "Regenerar códigos", "regenFailed": "No se pudieron regenerar los códigos de recuperación.", - "savedIt": "Los he guardado" + "savedIt": "Los he guardado", + "regenerateTitle": "¿Regenerar los códigos de recuperación?", + "regenerateConfirm": "Esto reemplaza tus códigos de recuperación actuales. Tus códigos existentes dejarán de funcionar de inmediato y el nuevo conjunto se muestra solo una vez." }, "keys": { "title": "Llaves de seguridad", diff --git a/messages/fr.json b/messages/fr.json index 2cd706a63..122199d74 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5064,7 +5064,9 @@ "remainingOther": "{count} codes de récupération restants.", "regenerate": "Régénérer les codes", "regenFailed": "Impossible de régénérer les codes de récupération.", - "savedIt": "Je les ai enregistrés" + "savedIt": "Je les ai enregistrés", + "regenerateTitle": "Régénérer les codes de récupération ?", + "regenerateConfirm": "Cela remplace vos codes de récupération actuels. Vos codes existants cesseront de fonctionner immédiatement et le nouveau jeu n'est affiché qu'une seule fois." }, "keys": { "title": "Clés de sécurité", diff --git a/messages/it.json b/messages/it.json index 028f384d3..743cb5e34 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5064,7 +5064,9 @@ "remainingOther": "{count} codici di recupero rimanenti.", "regenerate": "Rigenera i codici", "regenFailed": "Impossibile rigenerare i codici di recupero.", - "savedIt": "Li ho salvati" + "savedIt": "Li ho salvati", + "regenerateTitle": "Rigenerare i codici di recupero?", + "regenerateConfirm": "Questa operazione sostituisce i codici di recupero attuali. I codici esistenti smetteranno di funzionare immediatamente e il nuovo set viene mostrato una sola volta." }, "keys": { "title": "Chiavi di sicurezza", diff --git a/messages/pl.json b/messages/pl.json index b0f909a44..7d344e71d 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5064,7 +5064,9 @@ "remainingOther": "Pozostało {count} kodów odzyskiwania.", "regenerate": "Wygeneruj kody ponownie", "regenFailed": "Nie udało się ponownie wygenerować kodów odzyskiwania.", - "savedIt": "Zapisałem je" + "savedIt": "Zapisałem je", + "regenerateTitle": "Wygenerować nowe kody odzyskiwania?", + "regenerateConfirm": "Spowoduje to zastąpienie obecnych kodów odzyskiwania. Dotychczasowe kody natychmiast przestaną działać, a nowy zestaw zostanie pokazany tylko raz." }, "keys": { "title": "Klucze bezpieczeństwa", diff --git a/src/components/settings/security-section/__tests__/totp-card.test.tsx b/src/components/settings/security-section/__tests__/totp-card.test.tsx new file mode 100644 index 000000000..9b778c031 --- /dev/null +++ b/src/components/settings/security-section/__tests__/totp-card.test.tsx @@ -0,0 +1,46 @@ +/** + * v1.23 Phase-Q — `` recovery-code regeneration guard. + * + * Regenerating recovery codes is destructive: the previous set stops working + * immediately. This suite pins that the regenerate action is gated behind a + * confirmation dialog (matching the disable-MFA pattern), not a bare click. + * + * Project convention is SSR-only tests (no `@testing-library/react`); the + * dialog's open-state behaviour is Radix's own, so we assert the source wires + * the trigger + consequence copy rather than driving a portal at runtime. + */ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { TotpCard } from "../totp-card"; + +function render(node: React.ReactNode) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return renderToStaticMarkup( + + {node} + , + ); +} + +describe(" — recovery-code regeneration", () => { + it("SSRs the enabled card with the regenerate action", () => { + const html = render(); + expect(html).toContain("Regenerate codes"); + }); + + it("wraps regenerate in a confirmation dialog with a consequence line", () => { + const src = readFileSync(resolve(__dirname, "../totp-card.tsx"), "utf8"); + // The regenerate action is a dialog trigger, not a bare mutate-on-click. + expect(src).toContain("settings.security.recovery.regenerateTitle"); + expect(src).toContain("settings.security.recovery.regenerateConfirm"); + // The mutate only fires from inside the dialog's confirm action. + expect(src).toContain("regenerate.mutate()"); + }); +}); diff --git a/src/components/settings/security-section/totp-card.tsx b/src/components/settings/security-section/totp-card.tsx index 80e19d06b..2d1d78791 100644 --- a/src/components/settings/security-section/totp-card.tsx +++ b/src/components/settings/security-section/totp-card.tsx @@ -74,6 +74,7 @@ export function TotpCard({ const { t } = useTranslations(); const queryClient = useQueryClient(); const codeFieldId = useId(); + const disableCodeId = useId(); const [setup, setSetup] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(null); @@ -262,7 +263,7 @@ export function TotpCard({ type="button" variant="outline" size="icon" - className="h-8 w-8 shrink-0" + className="min-h-11 min-w-11 shrink-0 sm:h-8 sm:min-h-0 sm:w-8 sm:min-w-0" onClick={copySecret} aria-label={t("common.copy")} > @@ -350,18 +351,47 @@ export function TotpCard({ )}

- + + + + + + + + {t("settings.security.recovery.regenerateTitle")} + + + {t("settings.security.recovery.regenerateConfirm")} + + + + {t("common.cancel")} + { + e.preventDefault(); + regenerate.mutate(); + }} + > + {regenerate.isPending && ( + + )} + {t("settings.security.recovery.regenerate")} + + + + @@ -383,13 +413,19 @@ export function TotpCard({
-