import { ArrowLeft, ChevronDown, ChevronRight, Loader2, RefreshCw, Smartphone } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Avatar } from "../components/Avatar"; import { Alert, Badge, EmptyRow, Metric, PageFrame, SectionHead } from "../components/ui"; import { displayName, displayPhone, displayUsername, formatDate } from "../lib/format"; import type { Navigate } from "../routing"; import type { SharedDeviceGroup } from "../types"; // SharedDevicesPage surfaces authorizations that look like they came from the // same physical device but belong to different accounts -- a lead worth // investigating for multi-accounting, not a verdict: device_model and // system_version are self-reported by the client and easy to spoof, and a // matching IP alone is common and innocent behind NAT, shared wifi, or // carrier CGNAT. Treat every group here as "worth a look", not "guilty". export function SharedDevicesPage({ navigate }: { navigate: Navigate }) { const [groups, setGroups] = useState([]); const [hasMore, setHasMore] = useState(false); const [offset, setOffset] = useState(0); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); async function load(next = false) { setBusy(true); setError(""); const at = next ? offset : 0; const params = new URLSearchParams({ limit: "20", offset: String(at) }); try { const result = await api.sharedDeviceGroups(params); const page = result.rows ?? []; setGroups((current) => (next ? [...current, ...page] : page)); setOffset(result.next_offset); setHasMore(Boolean(result.has_more)); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } useEffect(() => { void load(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const totalFlaggedAccounts = groups.reduce((sum, group) => sum + group.AccountCount, 0); return ( } > {error && {error}}

{"Each card below is a device fingerprint (device model + OS + platform + IP) that more than one account has authorized from. "} {"device_model/system_version are self-reported by the client, and IP alone can collide innocently -- use this as a lead, not a verdict."}

{groups.map((group) => (
{`${group.AccountCount} accounts`}} />
{group.Accounts.map((account) => ( ))}
{"User ID"} {"Phone"} {"Username"} {"Name"} {"Active from this device"}
{account.UserID} {displayPhone(account.Phone)} {displayUsername(account.Username) || "-"} {displayName(account) || "-"} {formatDate(account.ActiveAt)}
))} {groups.length === 0 && (
)}
{hasMore && (
)}
); }