diff --git a/messages/de.json b/messages/de.json index 413ecb79f..4438719f7 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA an", "mfaEnforcedOff": "MFA aus", "mfaEnforcedOnHint": "Für diesen Nutzer ist ein zweiter Faktor erforderlich. Klicken, um die Anforderung aufzuheben.", - "mfaEnforcedOffHint": "Einen zweiten Faktor für diesen Nutzer verlangen." + "mfaEnforcedOffHint": "Einen zweiten Faktor für diesen Nutzer verlangen.", + "copyIp": "IP-Adresse kopieren", + "ipCopied": "IP-Adresse kopiert", + "copyFailed": "Kopieren fehlgeschlagen." }, "achievements": { "title": "Erfolge", diff --git a/messages/en.json b/messages/en.json index c2bf9e148..11fdc9ee9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA on", "mfaEnforcedOff": "MFA off", "mfaEnforcedOnHint": "A second factor is required for this user. Click to stop requiring it.", - "mfaEnforcedOffHint": "Require a second factor for this user." + "mfaEnforcedOffHint": "Require a second factor for this user.", + "copyIp": "Copy IP address", + "ipCopied": "IP address copied", + "copyFailed": "Could not copy." }, "achievements": { "title": "Achievements", diff --git a/messages/es.json b/messages/es.json index 530ef26a6..c498e22e8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA activado", "mfaEnforcedOff": "MFA desactivado", "mfaEnforcedOnHint": "Este usuario debe usar un segundo factor. Haz clic para dejar de exigirlo.", - "mfaEnforcedOffHint": "Exigir un segundo factor a este usuario." + "mfaEnforcedOffHint": "Exigir un segundo factor a este usuario.", + "copyIp": "Copiar dirección IP", + "ipCopied": "Dirección IP copiada", + "copyFailed": "No se pudo copiar." }, "achievements": { "title": "Logros", diff --git a/messages/fr.json b/messages/fr.json index 503f55f6e..8cba119f6 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA activé", "mfaEnforcedOff": "MFA désactivé", "mfaEnforcedOnHint": "Un second facteur est requis pour cet utilisateur. Cliquez pour ne plus l'exiger.", - "mfaEnforcedOffHint": "Exiger un second facteur pour cet utilisateur." + "mfaEnforcedOffHint": "Exiger un second facteur pour cet utilisateur.", + "copyIp": "Copier l’adresse IP", + "ipCopied": "Adresse IP copiée", + "copyFailed": "Impossible de copier." }, "achievements": { "title": "Réussites", diff --git a/messages/it.json b/messages/it.json index 850acef23..88a6e34fb 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA attivo", "mfaEnforcedOff": "MFA disattivato", "mfaEnforcedOnHint": "Per questo utente è richiesto un secondo fattore. Clicca per non richiederlo più.", - "mfaEnforcedOffHint": "Richiedi un secondo fattore per questo utente." + "mfaEnforcedOffHint": "Richiedi un secondo fattore per questo utente.", + "copyIp": "Copia indirizzo IP", + "ipCopied": "Indirizzo IP copiato", + "copyFailed": "Impossibile copiare." }, "achievements": { "title": "Obiettivi", diff --git a/messages/pl.json b/messages/pl.json index 71e383c3c..3b16a27c3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA wł.", "mfaEnforcedOff": "MFA wył.", "mfaEnforcedOnHint": "Ten użytkownik musi używać drugiego składnika. Kliknij, aby przestać go wymagać.", - "mfaEnforcedOffHint": "Wymagaj drugiego składnika dla tego użytkownika." + "mfaEnforcedOffHint": "Wymagaj drugiego składnika dla tego użytkownika.", + "copyIp": "Kopiuj adres IP", + "ipCopied": "Skopiowano adres IP", + "copyFailed": "Nie udało się skopiować." }, "achievements": { "title": "Osiągnięcia", diff --git a/src/components/admin/login-overview-section.tsx b/src/components/admin/login-overview-section.tsx index 41d6e1ae4..54a6059fc 100644 --- a/src/components/admin/login-overview-section.tsx +++ b/src/components/admin/login-overview-section.tsx @@ -2,8 +2,11 @@ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; +import { toast } from "sonner"; import { + Check, CheckCircle2, + Copy, Download, Loader2, MapPin, @@ -94,6 +97,25 @@ export function LoginOverviewSection() { const AUTH_ACTION_LABELS = useAuthActionLabels(); const AUTH_PROVIDER_LABELS = useAuthProviderLabels(); + // v1.25.11 (#151) — long IPv6 addresses are truncated in the table cell so + // they never force the wrapper into horizontal scroll; the full value is one + // click away. Track the most-recently-copied row so the icon can flip to a + // checkmark for transient feedback. + const [copiedIp, setCopiedIp] = useState(null); + + async function copyIp(id: string, ip: string) { + try { + await navigator.clipboard.writeText(ip); + setCopiedIp(id); + toast.success(t("admin.ipCopied")); + window.setTimeout(() => { + setCopiedIp((current) => (current === id ? null : current)); + }, 2_000); + } catch { + toast.error(t("admin.copyFailed")); + } + } + // Build the query string once so it's reused by the data-query key, the // export download, and the next-/prev- buttons. const queryParams = useMemo(() => { @@ -464,7 +486,35 @@ export function LoginOverviewSection() { - {entry.ipAddress ?? "—"} + {entry.ipAddress ? ( + + ) : ( + "—" + )} {entry.location ?? "—"}