feat(auth): migrate account security presentation
This commit is contained in:
@@ -16,7 +16,6 @@ import {
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
@@ -25,6 +24,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
type Status = { enabled: boolean; enabledAtUtc: string | null };
|
||||
type SetupResponse = { manualEntryKey: string; qrCodeDataUrl: string };
|
||||
@@ -55,8 +55,11 @@ function apiErrorMessage(e: any, t: (k: any) => string) {
|
||||
|
||||
export default function TwoFactorSettingsCard() {
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { t, language } = useI18n();
|
||||
const locale = language === "nb" ? "nb-NO" : "en";
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [statusLoading, setStatusLoading] = useState(true);
|
||||
const [statusError, setStatusError] = useState<string | null>(null);
|
||||
const [flow, setFlow] = useState<Flow>("closed");
|
||||
const [password, setPassword] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
@@ -70,8 +73,13 @@ export default function TwoFactorSettingsCard() {
|
||||
const [devicesError, setDevicesError] = useState<string | null>(null);
|
||||
|
||||
const loadStatus = useCallback(() => {
|
||||
api.get<Status>("/auth/2fa/status").then((r) => setStatus(r.data)).catch(() => setStatus(null));
|
||||
}, []);
|
||||
setStatusLoading(true);
|
||||
setStatusError(null);
|
||||
api.get<Status>("/auth/2fa/status")
|
||||
.then((r) => setStatus(r.data))
|
||||
.catch((e) => { setStatus(null); setStatusError(apiErrorMessage(e, t)); })
|
||||
.finally(() => setStatusLoading(false));
|
||||
}, [t]);
|
||||
|
||||
const loadDevices = useCallback(() => {
|
||||
setDevicesLoading(true);
|
||||
@@ -184,21 +192,19 @@ export default function TwoFactorSettingsCard() {
|
||||
const isRecoveryStep = flow === "enable-recovery" || flow === "regenerate-recovery";
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2.5, borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t("twoFactorSectionTitle")}
|
||||
</Typography>
|
||||
|
||||
{status ? (
|
||||
<PageSection title={t("twoFactorSectionTitle")} surface>
|
||||
{statusLoading ? <Typography role="status" color="text.secondary">{t("loading")}</Typography> : null}
|
||||
{statusError ? <Alert severity="error" sx={{ mb: 1.5 }} action={<Button color="inherit" onClick={loadStatus}>{t("retry")}</Button>}>{statusError}</Alert> : null}
|
||||
{status && !statusLoading ? (
|
||||
<Typography sx={{ color: "text.secondary", mb: 1.5 }}>
|
||||
{status.enabled
|
||||
? t("twoFactorStatusEnabled", { date: status.enabledAtUtc ? new Date(status.enabledAtUtc).toLocaleDateString() : "" })
|
||||
? t("twoFactorStatusEnabled", { date: status.enabledAtUtc ? new Date(status.enabledAtUtc).toLocaleDateString(locale) : "" })
|
||||
: t("twoFactorStatusDisabled")}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{!status?.enabled ? (
|
||||
{status && !statusLoading ? <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{!status.enabled ? (
|
||||
<Button variant="contained" onClick={() => setFlow("enable-password")}>
|
||||
{t("twoFactorEnableButton")}
|
||||
</Button>
|
||||
@@ -212,15 +218,16 @@ export default function TwoFactorSettingsCard() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box> : null}
|
||||
|
||||
{status?.enabled ? (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Divider sx={{ mb: 1.5 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
<Typography component="h3" variant="sectionTitle" sx={{ mb: 0.5 }}>
|
||||
{t("twoFactorTrustedDevicesTitle")}
|
||||
</Typography>
|
||||
{devicesError ? <Alert severity="error" role="alert" sx={{ mb: 1 }}>{devicesError}</Alert> : null}
|
||||
{devicesLoading ? <Typography role="status" color="text.secondary">{t("loading")}</Typography> : null}
|
||||
{devicesError ? <Alert severity="error" role="alert" sx={{ mb: 1 }} action={<Button color="inherit" onClick={loadDevices}>{t("retry")}</Button>}>{devicesError}</Alert> : null}
|
||||
{!devicesLoading && devices.length === 0 && !devicesError ? (
|
||||
<Typography sx={{ color: "text.secondary", mb: 1 }}>{t("twoFactorTrustedDevicesEmpty")}</Typography>
|
||||
) : null}
|
||||
@@ -229,6 +236,7 @@ export default function TwoFactorSettingsCard() {
|
||||
{devices.map((d) => (
|
||||
<ListItem key={d.id} divider>
|
||||
<ListItemText
|
||||
sx={{ minWidth: 0, overflowWrap: "anywhere" }}
|
||||
primary={
|
||||
<>
|
||||
{d.deviceLabel || t("twoFactorTrustedDeviceUnknown")}
|
||||
@@ -240,8 +248,8 @@ export default function TwoFactorSettingsCard() {
|
||||
</>
|
||||
}
|
||||
secondary={t("twoFactorTrustedDeviceMeta", {
|
||||
lastSeen: new Date(d.lastSeenAtUtc).toLocaleDateString(),
|
||||
expires: new Date(d.expiresAtUtc).toLocaleDateString(),
|
||||
lastSeen: new Date(d.lastSeenAtUtc).toLocaleDateString(locale),
|
||||
expires: new Date(d.expiresAtUtc).toLocaleDateString(locale),
|
||||
})}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
@@ -385,6 +393,6 @@ export default function TwoFactorSettingsCard() {
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user