import React, { useCallback, useEffect, useState } from "react"; import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, List, ListItem, ListItemSecondaryAction, ListItemText, Typography, } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; 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; deviceLabel: string | null; createdAtUtc: string; lastSeenAtUtc: string; expiresAtUtc: string; isCurrentSession: boolean; }; function apiErrorMessage(e: any, t: (k: any) => string) { return getApiErrorMessage(e, t("twoFactorGenericError")); } export default function SessionsSettingsCard() { const { toast } = useToast(); const { t, language } = useI18n(); const locale = language === "nb" ? "nb-NO" : "en"; const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [confirmRevokeOthers, setConfirmRevokeOthers] = useState(false); const [confirmError, setConfirmError] = useState(null); const [revokingOthers, setRevokingOthers] = useState(false); const loadSessions = useCallback(() => { setLoading(true); setError(null); api .get("/auth/sessions") .then((r) => setSessions(r.data)) .catch((e) => setError(apiErrorMessage(e, t))) .finally(() => setLoading(false)); }, [t]); useEffect(() => { loadSessions(); }, [loadSessions]); async function revokeSession(id: string, isCurrentSession: boolean) { try { await api.delete(`/auth/sessions/${id}`); if (isCurrentSession) { // Same pattern as AuthStatusCard's sign-out: clearing local auth state emits // "auth-changed", which App.tsx's listener picks up to refetch /auth/me (now 401, // since the server already deleted the session cookie) and redirect to /login. clearAuthClientState(); return; } toast(t("sessionsRevoked"), "success"); loadSessions(); } catch (e: any) { setError(apiErrorMessage(e, t)); } } 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) { setConfirmError(apiErrorMessage(e, t)); } finally { setRevokingOthers(false); } } function closeConfirmation() { setConfirmRevokeOthers(false); setConfirmError(null); } return ( {loading ? {t("loading")} : null} {error ? {t("retry")}}>{error} : null} {!loading && sessions.length === 0 && !error ? ( {t("sessionsEmpty")} ) : null} {sessions.length > 0 ? ( {sessions.map((s) => ( {s.deviceLabel || t("sessionsUnknownDevice")} {s.isCurrentSession ? ( {t("sessionsCurrentDevice")} ) : null} } secondary={t("sessionsMeta", { lastSeen: new Date(s.lastSeenAtUtc).toLocaleString(locale), expires: new Date(s.expiresAtUtc).toLocaleDateString(locale), })} /> revokeSession(s.id, s.isCurrentSession)}> ))} ) : null} {sessions.length > 1 ? ( ) : null} {t("sessionsRevokeOthersConfirmTitle")} {confirmError ? {confirmError} : null} {t("sessionsRevokeOthersConfirmBody")} ); }