feat(auth): migrate account security presentation

This commit is contained in:
cesnimda
2026-10-11 21:16:05 +02:00
parent 8ad720da5d
commit ad7b4196af
22 changed files with 35532 additions and 143 deletions
@@ -12,7 +12,6 @@ import {
ListItem,
ListItemSecondaryAction,
ListItemText,
Paper,
Typography,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
@@ -21,6 +20,7 @@ import { api, getApiErrorMessage } from "../api";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { clearAuthClientState } from "../auth";
import { PageSection } from "../layout/PageLayout";
type Session = {
id: string;
@@ -37,11 +37,14 @@ function apiErrorMessage(e: any, t: (k: any) => string) {
export default function SessionsSettingsCard() {
const { toast } = useToast();
const { t } = useI18n();
const { t, language } = useI18n();
const locale = language === "nb" ? "nb-NO" : "en";
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [confirmRevokeOthers, setConfirmRevokeOthers] = useState(false);
const [confirmError, setConfirmError] = useState<string | null>(null);
const [revokingOthers, setRevokingOthers] = useState(false);
const loadSessions = useCallback(() => {
setLoading(true);
@@ -73,23 +76,29 @@ export default function SessionsSettingsCard() {
}
async function revokeOthers() {
setRevokingOthers(true);
setConfirmError(null);
try {
await api.post("/auth/sessions/revoke-others");
toast(t("sessionsRevokedOthers"), "success");
setConfirmRevokeOthers(false);
loadSessions();
} catch (e: any) {
setError(apiErrorMessage(e, t));
setConfirmError(apiErrorMessage(e, t));
} finally {
setRevokingOthers(false);
}
}
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("sessionsSectionTitle")}
</Typography>
function closeConfirmation() {
setConfirmRevokeOthers(false);
setConfirmError(null);
}
{error ? <Alert severity="error" role="alert" sx={{ mb: 1 }}>{error}</Alert> : null}
return (
<PageSection title={t("sessionsSectionTitle")} surface>
{loading ? <Typography role="status" color="text.secondary">{t("loading")}</Typography> : null}
{error ? <Alert severity="error" role="alert" sx={{ mb: 1 }} action={<Button color="inherit" onClick={loadSessions}>{t("retry")}</Button>}>{error}</Alert> : null}
{!loading && sessions.length === 0 && !error ? (
<Typography sx={{ color: "text.secondary", mb: 1 }}>{t("sessionsEmpty")}</Typography>
) : null}
@@ -99,6 +108,7 @@ export default function SessionsSettingsCard() {
{sessions.map((s) => (
<ListItem key={s.id} divider>
<ListItemText
sx={{ minWidth: 0, overflowWrap: "anywhere" }}
primary={
<>
{s.deviceLabel || t("sessionsUnknownDevice")}
@@ -110,8 +120,8 @@ export default function SessionsSettingsCard() {
</>
}
secondary={t("sessionsMeta", {
lastSeen: new Date(s.lastSeenAtUtc).toLocaleString(),
expires: new Date(s.expiresAtUtc).toLocaleDateString(),
lastSeen: new Date(s.lastSeenAtUtc).toLocaleString(locale),
expires: new Date(s.expiresAtUtc).toLocaleDateString(locale),
})}
/>
<ListItemSecondaryAction>
@@ -125,23 +135,24 @@ export default function SessionsSettingsCard() {
) : null}
{sessions.length > 1 ? (
<Button variant="text" color="warning" size="small" sx={{ mt: 1 }} onClick={() => setConfirmRevokeOthers(true)}>
<Button variant="text" color="warning" size="small" sx={{ mt: 1 }} onClick={() => { setConfirmError(null); setConfirmRevokeOthers(true); }}>
{t("sessionsRevokeOthers")}
</Button>
) : null}
<Dialog open={confirmRevokeOthers} onClose={() => setConfirmRevokeOthers(false)} maxWidth="sm" fullWidth>
<Dialog open={confirmRevokeOthers} onClose={revokingOthers ? undefined : closeConfirmation} maxWidth="sm" fullWidth>
<DialogTitle>{t("sessionsRevokeOthersConfirmTitle")}</DialogTitle>
<DialogContent>
{confirmError ? <Alert severity="error" sx={{ mb: 2 }}>{confirmError}</Alert> : null}
<Typography>{t("sessionsRevokeOthersConfirmBody")}</Typography>
</DialogContent>
<DialogActions>
<Button type="button" onClick={() => setConfirmRevokeOthers(false)}>{t("cancel")}</Button>
<Button variant="contained" color="warning" onClick={revokeOthers}>
<Button type="button" onClick={closeConfirmation} disabled={revokingOthers}>{t("cancel")}</Button>
<Button variant="contained" color="warning" onClick={revokeOthers} disabled={revokingOthers}>
{t("sessionsRevokeOthers")}
</Button>
</DialogActions>
</Dialog>
</Paper>
</PageSection>
);
}