feat(auth): add trusted-device 30-day 2FA skip (frontend)

Adds a "Trust this device for 30 days" checkbox to the 2FA challenge step,
and a "Trusted devices" section to the 2FA settings card: list devices with
a "this device" badge, per-row revoke, and a confirm-gated "sign out all
other trusted devices" action. Both flows are opt-in and additive -- default
unchecked, so nothing changes for a user who never uses them.
This commit is contained in:
cesnimda
2026-07-13 01:02:43 +02:00
parent b914630657
commit 0ca2f2b261
4 changed files with 146 additions and 6 deletions
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Alert, Box, Button, TextField, Typography } from "@mui/material";
import { Alert, Box, Button, Checkbox, FormControlLabel, TextField, Typography } from "@mui/material";
import { api, getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
@@ -18,6 +18,7 @@ export default function TwoFactorChallenge({
}) {
const { t } = useI18n();
const [code, setCode] = useState("");
const [trustDevice, setTrustDevice] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -25,7 +26,7 @@ export default function TwoFactorChallenge({
setLoading(true);
setError(null);
try {
const res = await api.post<ChallengeResponse>("/auth/2fa/challenge", { pendingToken, code });
const res = await api.post<ChallengeResponse>("/auth/2fa/challenge", { pendingToken, code, trustDevice });
onSuccess(res.data);
} catch (e: any) {
if (e?.response?.status === 429) {
@@ -64,6 +65,11 @@ export default function TwoFactorChallenge({
fullWidth
/>
<FormControlLabel
control={<Checkbox checked={trustDevice} onChange={(e) => setTrustDevice(e.target.checked)} />}
label={t("twoFactorTrustDevice")}
/>
<Box sx={{ display: "flex", gap: 1, justifyContent: "flex-end" }}>
<Button type="button" variant="text" disabled={loading} onClick={onCancel}>
{t("twoFactorBack")}