fix(dashboard): show enriched network details
This commit is contained in:
@@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { UserWorldMap, type UserMapLocation } from "@/components/user-world-map";
|
import { UserWorldMap, type UserMapLocation } from "@/components/user-world-map";
|
||||||
import { db } from "@/lib/database";
|
import { db } from "@/lib/database";
|
||||||
import { fillDailySeries, mergeRiskActivity, type DailyCount } from "@/lib/admin-metrics";
|
import { fillDailySeries, mergeRiskActivity, type DailyCount } from "@/lib/admin-metrics";
|
||||||
import { parseUserLocation } from "@/lib/user-location-map";
|
import { parseUserLocation, parseUserNetwork } from "@/lib/user-location-map";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export default async function AdminDashboardPage() {
|
|||||||
name: users.firstName,
|
name: users.firstName,
|
||||||
discordUsername: users.discordUsername,
|
discordUsername: users.discordUsername,
|
||||||
primaryUsername: minecraftAccounts.username,
|
primaryUsername: minecraftAccounts.username,
|
||||||
classification: ipObservations.classification,
|
classification: ipIntelligence.classification,
|
||||||
source: ipObservations.source,
|
source: ipObservations.source,
|
||||||
observedAt: ipObservations.observedAt,
|
observedAt: ipObservations.observedAt,
|
||||||
intelligence: ipIntelligence.rawResponse,
|
intelligence: ipIntelligence.rawResponse,
|
||||||
@@ -108,6 +108,7 @@ export default async function AdminDashboardPage() {
|
|||||||
const locations = locationRows.flatMap((row): UserMapLocation[] => {
|
const locations = locationRows.flatMap((row): UserMapLocation[] => {
|
||||||
const parsed = parseUserLocation(row.intelligence);
|
const parsed = parseUserLocation(row.intelligence);
|
||||||
if (!parsed || !row.userId) return [];
|
if (!parsed || !row.userId) return [];
|
||||||
|
const network = parseUserNetwork(row.intelligence);
|
||||||
return [{
|
return [{
|
||||||
userId: row.userId,
|
userId: row.userId,
|
||||||
name: row.name ?? row.discordUsername,
|
name: row.name ?? row.discordUsername,
|
||||||
@@ -117,6 +118,10 @@ export default async function AdminDashboardPage() {
|
|||||||
longitude: parsed.longitude,
|
longitude: parsed.longitude,
|
||||||
location: parsed.label,
|
location: parsed.label,
|
||||||
classification: row.classification,
|
classification: row.classification,
|
||||||
|
networkProvider: network.provider,
|
||||||
|
networkAsn: network.asn,
|
||||||
|
connectionType: network.connectionType,
|
||||||
|
proxy: network.proxy,
|
||||||
source: row.source,
|
source: row.source,
|
||||||
observedAt: row.observedAt,
|
observedAt: row.observedAt,
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ describe("UserWorldMap", () => {
|
|||||||
longitude: -122.0775,
|
longitude: -122.0775,
|
||||||
location: "Mountain View, California, US",
|
location: "Mountain View, California, US",
|
||||||
classification: "clear",
|
classification: "clear",
|
||||||
|
networkProvider: "Comcast Cable Communications, LLC",
|
||||||
|
networkAsn: "AS7922",
|
||||||
|
connectionType: "Residential",
|
||||||
|
proxy: false,
|
||||||
source: "game",
|
source: "game",
|
||||||
observedAt: new Date("2026-08-01T12:00:00Z"),
|
observedAt: new Date("2026-08-01T12:00:00Z"),
|
||||||
}, {
|
}, {
|
||||||
@@ -23,7 +27,11 @@ describe("UserWorldMap", () => {
|
|||||||
latitude: 37.4057,
|
latitude: 37.4057,
|
||||||
longitude: -122.0774,
|
longitude: -122.0774,
|
||||||
location: "Mountain View, California, US",
|
location: "Mountain View, California, US",
|
||||||
classification: "clear",
|
classification: "vpn",
|
||||||
|
networkProvider: "Proton AG",
|
||||||
|
networkAsn: "AS62371",
|
||||||
|
connectionType: "VPN",
|
||||||
|
proxy: true,
|
||||||
source: "web",
|
source: "web",
|
||||||
observedAt: new Date("2026-08-01T13:00:00Z"),
|
observedAt: new Date("2026-08-01T13:00:00Z"),
|
||||||
}]} unavailableCount={2} />);
|
}]} unavailableCount={2} />);
|
||||||
@@ -38,6 +46,13 @@ describe("UserWorldMap", () => {
|
|||||||
expect(markup).toMatch(/<text[^>]*>2<\/text>/);
|
expect(markup).toMatch(/<text[^>]*>2<\/text>/);
|
||||||
expect(markup).toContain('<details class="mt-5 border-t border-line pt-4" id="map-location-list" open="">');
|
expect(markup).toContain('<details class="mt-5 border-t border-line pt-4" id="map-location-list" open="">');
|
||||||
expect(markup).toContain("Mountain View, California, US");
|
expect(markup).toContain("Mountain View, California, US");
|
||||||
|
expect(markup).toContain("Comcast Cable Communications, LLC");
|
||||||
|
expect(markup).toContain("AS7922");
|
||||||
|
expect(markup).toContain("Residential");
|
||||||
|
expect(markup).toContain("Proton AG");
|
||||||
|
expect(markup).toContain(">Proxy/VPN<");
|
||||||
|
expect(markup).toContain(">Yes<");
|
||||||
|
expect(markup).toContain(">No<");
|
||||||
expect(markup).toContain("World overview");
|
expect(markup).toContain("World overview");
|
||||||
expect(markup).toContain("Interactive OpenStreetMap");
|
expect(markup).toContain("Interactive OpenStreetMap");
|
||||||
expect(markup).toContain("OpenStreetMap, which receives your IP address");
|
expect(markup).toContain("OpenStreetMap, which receives your IP address");
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export interface UserMapLocation {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
location: string;
|
location: string;
|
||||||
classification: string;
|
classification: string;
|
||||||
|
networkProvider: string | null;
|
||||||
|
networkAsn: string | null;
|
||||||
|
connectionType: string | null;
|
||||||
|
proxy: boolean | null;
|
||||||
source: string;
|
source: string;
|
||||||
observedAt: Date;
|
observedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -98,12 +102,12 @@ export function UserWorldMap({ locations, unavailableCount }: { locations: UserM
|
|||||||
<details className="mt-5 border-t border-line pt-4" id="map-location-list" open={locationGroups.some((group) => group.count > 1)}>
|
<details className="mt-5 border-t border-line pt-4" id="map-location-list" open={locationGroups.some((group) => group.count > 1)}>
|
||||||
<summary className="w-fit cursor-pointer font-mono text-[10px] font-bold uppercase underline underline-offset-4">View accessible location list</summary>
|
<summary className="w-fit cursor-pointer font-mono text-[10px] font-bold uppercase underline underline-offset-4">View accessible location list</summary>
|
||||||
<div className="mt-4 overflow-x-auto">
|
<div className="mt-4 overflow-x-auto">
|
||||||
<table className="w-full min-w-[680px] border-collapse text-left text-xs">
|
<table className="w-full min-w-[980px] border-collapse text-left text-xs">
|
||||||
<caption className="sr-only">Latest approximate registered-user locations</caption>
|
<caption className="sr-only">Latest approximate registered-user locations and enriched network details</caption>
|
||||||
<thead className="border-b border-line font-mono text-[9px] uppercase tracking-wider text-muted"><tr><th className="py-3 pr-4" scope="col">User</th><th className="p-3" scope="col">Location</th><th className="p-3" scope="col">Network</th><th className="p-3" scope="col">Source</th><th className="py-3 pl-4" scope="col">Last observed</th></tr></thead>
|
<thead className="border-b border-line font-mono text-[9px] uppercase tracking-wider text-muted"><tr><th className="py-3 pr-4" scope="col">User</th><th className="p-3" scope="col">Location</th><th className="p-3" scope="col">Network</th><th className="p-3" scope="col">Connection</th><th className="p-3" scope="col">Proxy/VPN</th><th className="p-3" scope="col">Risk</th><th className="p-3" scope="col">Source</th><th className="py-3 pl-4" scope="col">Last observed</th></tr></thead>
|
||||||
<tbody className="divide-y divide-line">
|
<tbody className="divide-y divide-line">
|
||||||
{locations.map((user) => <tr key={user.userId}><th className="py-3 pr-4 text-left" scope="row"><Link className="font-mono font-bold underline underline-offset-4" href={`/admin/users/${user.userId}`}>{user.nickname}</Link><span className="mt-1 block font-mono text-[9px] font-normal text-muted">@{user.discordUsername}</span></th><td className="p-3">{user.location}</td><td className="p-3 font-mono uppercase">{user.classification}</td><td className="p-3">{user.source}</td><td className="py-3 pl-4 font-mono text-[9px]"><time dateTime={user.observedAt.toISOString()}>{user.observedAt.toISOString()}</time></td></tr>)}
|
{locations.map((user) => <tr key={user.userId}><th className="py-3 pr-4 text-left" scope="row"><Link className="font-mono font-bold underline underline-offset-4" href={`/admin/users/${user.userId}`}>{user.nickname}</Link><span className="mt-1 block font-mono text-[9px] font-normal text-muted">@{user.discordUsername}</span></th><td className="p-3">{user.location}</td><td className="p-3"><span className="block">{user.networkProvider ?? "Unknown"}</span>{user.networkAsn && <span className="mt-1 block font-mono text-[9px] text-muted">{user.networkAsn}</span>}</td><td className="p-3">{user.connectionType ?? "Unknown"}</td><td className="p-3 font-mono font-bold uppercase">{user.proxy === null ? "Unknown" : user.proxy ? "Yes" : "No"}</td><td className="p-3 font-mono uppercase">{user.classification}</td><td className="p-3">{user.source}</td><td className="py-3 pl-4 font-mono text-[9px]"><time dateTime={user.observedAt.toISOString()}>{user.observedAt.toISOString()}</time></td></tr>)}
|
||||||
{!locations.length && <tr><td className="py-6 text-muted" colSpan={5}>No user observations currently include valid coordinates.</td></tr>}
|
{!locations.length && <tr><td className="py-6 text-muted" colSpan={8}>No user observations currently include valid coordinates.</td></tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { groupMapLocations, parseUserLocation, projectWorldPoint } from "./user-location-map";
|
import { groupMapLocations, parseUserLocation, parseUserNetwork, projectWorldPoint } from "./user-location-map";
|
||||||
|
|
||||||
describe("user location map", () => {
|
describe("user location map", () => {
|
||||||
it("extracts a valid approximate location from cached IP intelligence", () => {
|
it("extracts a valid approximate location from cached IP intelligence", () => {
|
||||||
@@ -19,6 +19,29 @@ describe("user location map", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts enriched network fields from existing ProxyCheck cache entries", () => {
|
||||||
|
expect(parseUserNetwork({
|
||||||
|
network: { asn: "AS7922", provider: "Comcast Cable Communications, LLC" },
|
||||||
|
rawResponse: {
|
||||||
|
status: "ok",
|
||||||
|
"203.0.113.10": { type: "Residential", proxy: "no" },
|
||||||
|
},
|
||||||
|
})).toEqual({
|
||||||
|
asn: "AS7922",
|
||||||
|
provider: "Comcast Cable Communications, LLC",
|
||||||
|
connectionType: "Residential",
|
||||||
|
proxy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers normalized network fields and preserves unavailable values", () => {
|
||||||
|
expect(parseUserNetwork({
|
||||||
|
network: { asn: "AS62371", provider: "Proton AG", connectionType: "VPN", proxy: true },
|
||||||
|
rawResponse: { "198.51.100.5": { type: "Residential", proxy: "no" } },
|
||||||
|
})).toEqual({ asn: "AS62371", provider: "Proton AG", connectionType: "VPN", proxy: true });
|
||||||
|
expect(parseUserNetwork({ network: {} })).toEqual({ asn: null, provider: null, connectionType: null, proxy: null });
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects missing and out-of-range coordinates", () => {
|
it("rejects missing and out-of-range coordinates", () => {
|
||||||
expect(parseUserLocation({ location: { latitude: 91, longitude: 0 } })).toBeNull();
|
expect(parseUserLocation({ location: { latitude: 91, longitude: 0 } })).toBeNull();
|
||||||
expect(parseUserLocation({ location: { city: "Unknown" } })).toBeNull();
|
expect(parseUserLocation({ location: { city: "Unknown" } })).toBeNull();
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ function objectValue(value: unknown): UnknownMap | null {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringValue(value: unknown) {
|
||||||
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyValue(value: unknown) {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "string" && value.toLowerCase() === "yes") return true;
|
||||||
|
if (typeof value === "string" && value.toLowerCase() === "no") return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function coordinate(value: unknown) {
|
function coordinate(value: unknown) {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||||
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
||||||
@@ -18,6 +29,28 @@ export interface ParsedUserLocation {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParsedUserNetwork {
|
||||||
|
asn: string | null;
|
||||||
|
provider: string | null;
|
||||||
|
connectionType: string | null;
|
||||||
|
proxy: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseUserNetwork(value: unknown): ParsedUserNetwork {
|
||||||
|
const intelligence = objectValue(value);
|
||||||
|
const network = objectValue(intelligence?.network);
|
||||||
|
const providerResponse = objectValue(intelligence?.rawResponse);
|
||||||
|
const legacyDetails = Object.values(providerResponse ?? {})
|
||||||
|
.map(objectValue)
|
||||||
|
.find((details) => details && ("type" in details || "proxy" in details));
|
||||||
|
return {
|
||||||
|
asn: stringValue(network?.asn),
|
||||||
|
provider: stringValue(network?.provider),
|
||||||
|
connectionType: stringValue(network?.connectionType) ?? stringValue(legacyDetails?.type),
|
||||||
|
proxy: proxyValue(network?.proxy) ?? proxyValue(legacyDetails?.proxy),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parseUserLocation(value: unknown): ParsedUserLocation | null {
|
export function parseUserLocation(value: unknown): ParsedUserLocation | null {
|
||||||
const intelligence = objectValue(value);
|
const intelligence = objectValue(value);
|
||||||
const location = objectValue(intelligence?.location);
|
const location = objectValue(intelligence?.location);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 2026-08-02
|
## 2026-08-02
|
||||||
|
|
||||||
|
* **Fix**: Replace the dashboard's pre-enrichment network label with enriched company, ASN, connection type, Proxy/VPN status, and risk fields.
|
||||||
* **Fix**: Group collocated map users into count-badged markers with complete nickname tooltips and per-user interactive-map links.
|
* **Fix**: Group collocated map users into count-badged markers with complete nickname tooltips and per-user interactive-map links.
|
||||||
* **Refine**: Replace registration counts with daily active users, collapse enriched VPN activity per user, add opt-in OpenStreetMap zoom, show managed nickname tooltips, and measure active Minecraft accounts from confirmed Velocity connections.
|
* **Refine**: Replace registration counts with daily active users, collapse enriched VPN activity per user, add opt-in OpenStreetMap zoom, show managed nickname tooltips, and measure active Minecraft accounts from confirmed Velocity connections.
|
||||||
* **Governance**: Require user review and explicit confirmation of relevant OKF story changes before future implementation work.
|
* **Governance**: Require user review and explicit confirmation of relevant OKF story changes before future implementation work.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ type: User Story
|
|||||||
title: Monitor community account activity
|
title: Monitor community account activity
|
||||||
description: Administrators use a server-rendered dashboard to review daily activity, confirmed connections, locations, denials, and risky networks.
|
description: Administrators use a server-rendered dashboard to review daily activity, confirmed connections, locations, denials, and risky networks.
|
||||||
tags: [admin, dashboard, metrics, security, maps, ssr]
|
tags: [admin, dashboard, metrics, security, maps, ssr]
|
||||||
timestamp: 2026-08-02T01:23:21Z
|
timestamp: 2026-08-02T12:05:27Z
|
||||||
story_id: US-018
|
story_id: US-018
|
||||||
status: verified
|
status: verified
|
||||||
---
|
---
|
||||||
@@ -23,6 +23,10 @@ As an administrator, I want an operational dashboard of account and game activit
|
|||||||
- [x] Grouped-marker hover and keyboard focus list every managed Discord nickname at that location.
|
- [x] Grouped-marker hover and keyboard focus list every managed Discord nickname at that location.
|
||||||
- [x] Interactive grouped markers open a popup with links to every corresponding user record.
|
- [x] Interactive grouped markers open a popup with links to every corresponding user record.
|
||||||
- [x] Single-user markers retain their direct nickname tooltip and user-record link.
|
- [x] Single-user markers retain their direct nickname tooltip and user-record link.
|
||||||
|
- [x] The location list identifies the enriched network company and ASN when available.
|
||||||
|
- [x] The location list shows ProxyCheck's connection type separately from its risk classification.
|
||||||
|
- [x] The location list shows the provider's proxy/VPN signal as an explicit Yes or No value.
|
||||||
|
- [x] Unknown is shown only for individual enriched fields that are unavailable, including existing cached responses.
|
||||||
- [x] The dashboard graphs distinct daily active users by UTC day for the previous 14 days with understandable date labels.
|
- [x] The dashboard graphs distinct daily active users by UTC day for the previous 14 days with understandable date labels.
|
||||||
- [x] Monthly active users count distinct users observed through portal or game activity in the previous 30 days.
|
- [x] Monthly active users count distinct users observed through portal or game activity in the previous 30 days.
|
||||||
- [x] Monthly active Minecraft accounts count distinct accounts with a confirmed Velocity post-login connection in the previous 30 days.
|
- [x] Monthly active Minecraft accounts count distinct accounts with a confirmed Velocity post-login connection in the previous 30 days.
|
||||||
@@ -44,7 +48,7 @@ As an administrator, I want an operational dashboard of account and game activit
|
|||||||
# Validation
|
# Validation
|
||||||
|
|
||||||
- Missing-day chart behavior and per-user VPN collapsing are covered by [`apps/web/src/lib/admin-metrics.test.ts`](../apps/web/src/lib/admin-metrics.test.ts).
|
- Missing-day chart behavior and per-user VPN collapsing are covered by [`apps/web/src/lib/admin-metrics.test.ts`](../apps/web/src/lib/admin-metrics.test.ts).
|
||||||
- Coordinate parsing, normalized location grouping, projection, count badges, complete grouped tooltips, linked markers, text fallback, and attribution are covered by the user-world-map tests.
|
- Coordinate parsing, backward-compatible ProxyCheck network parsing, normalized location grouping, projection, count badges, complete grouped tooltips, linked markers, semantic network columns, text fallback, and attribution are covered by the user-world-map tests.
|
||||||
- The Next.js production build reports the dashboard and database-backed console pages as dynamic server-rendered routes.
|
- The Next.js production build reports the dashboard and database-backed console pages as dynamic server-rendered routes.
|
||||||
|
|
||||||
# Related Stories
|
# Related Stories
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Player account management, administrator navigation, dashboard metrics and chart
|
|||||||
- Added an accessible title, description, date labels, per-point labels, and textual values to the daily-active-user chart.
|
- Added an accessible title, description, date labels, per-point labels, and textual values to the daily-active-user chart.
|
||||||
- Added labelled, keyboard-linked world-map markers plus a complete semantic table equivalent for approximate user locations.
|
- Added labelled, keyboard-linked world-map markers plus a complete semantic table equivalent for approximate user locations.
|
||||||
- Collocated users share a visible count badge; hover and focus tooltips announce every nickname, while interactive grouped markers expose per-user popup links.
|
- Collocated users share a visible count badge; hover and focus tooltips announce every nickname, while interactive grouped markers expose per-user popup links.
|
||||||
|
- The semantic location table separates network company, connection type, Proxy/VPN status, and risk classification under explicit column headers.
|
||||||
- Added keyboard-operable tabs for the server-rendered overview and opt-in interactive OpenStreetMap view.
|
- Added keyboard-operable tabs for the server-rendered overview and opt-in interactive OpenStreetMap view.
|
||||||
- Added explicit new-tab context to the external Discord invite link.
|
- Added explicit new-tab context to the external Discord invite link.
|
||||||
- Kept destructive account and group actions behind native keyboard-operable `details` confirmation disclosures.
|
- Kept destructive account and group actions behind native keyboard-operable `details` confirmation disclosures.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ The admission decision is fail closed. Unknown players, invalid responses, expir
|
|||||||
|
|
||||||
## IP intelligence
|
## IP intelligence
|
||||||
|
|
||||||
ProxyCheck.io supplies approximate city/region/country, coordinates, timezone, ASN, provider, risk, and VPN/proxy/Tor classification. Results are cached in PostgreSQL for 48 hours by default. Portal and game login events are enriched when data is available; lookup failures do not deny login. User Minecraft-account additions fail closed for unknown, VPN, proxy, or Tor classifications and record denied attempts. Hosting-provider blocking is optional through `BLOCK_HOSTING_IPS=true`. Private and reserved addresses are never sent to ProxyCheck.
|
ProxyCheck.io supplies approximate city/region/country, coordinates, timezone, ASN, network company, connection type, proxy signal, risk, and VPN/proxy/Tor classification. Results are cached in PostgreSQL for 48 hours by default. Portal and game login events are enriched when data is available; lookup failures do not deny login. User Minecraft-account additions fail closed for unknown, VPN, proxy, or Tor classifications and record denied attempts. Hosting-provider blocking is optional through `BLOCK_HOSTING_IPS=true`. Private and reserved addresses are never sent to ProxyCheck.
|
||||||
|
|
||||||
## Event naming
|
## Event naming
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Next.js portal and APIs, Discord bot, PostgreSQL persistence, Keycloak admin aut
|
|||||||
- CSP, clickjacking, MIME-sniffing, referrer, and browser-permission headers are configured.
|
- CSP, clickjacking, MIME-sniffing, referrer, and browser-permission headers are configured.
|
||||||
- Forwarded IP headers are ignored unless `TRUST_PROXY=true` is explicitly configured.
|
- Forwarded IP headers are ignored unless `TRUST_PROXY=true` is explicitly configured.
|
||||||
- Private and reserved addresses are not sent to ProxyCheck.io; lookup results are cached to reduce disclosure and API usage.
|
- Private and reserved addresses are not sent to ProxyCheck.io; lookup results are cached to reduce disclosure and API usage.
|
||||||
- Portal and game login events include approximate network location and VPN/proxy classification when available.
|
- Portal and game login events include approximate network location and VPN/proxy classification when available. The administrator-only location list also exposes enriched network company, ASN, connection type, and the provider's proxy signal.
|
||||||
- Structured Pino logging redacts credential fields, and secrets are excluded from logs and repository configuration.
|
- Structured Pino logging redacts credential fields, and secrets are excluded from logs and repository configuration.
|
||||||
|
|
||||||
## Outstanding production requirements
|
## Outstanding production requirements
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface IpLocation {
|
|||||||
export interface IpNetwork {
|
export interface IpNetwork {
|
||||||
asn: string | null;
|
asn: string | null;
|
||||||
provider: string | null;
|
provider: string | null;
|
||||||
|
connectionType?: string | null;
|
||||||
|
proxy?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IpIntelligenceResult {
|
export interface IpIntelligenceResult {
|
||||||
@@ -112,6 +114,10 @@ export class ProxyCheckProvider implements IpIntelligenceProvider {
|
|||||||
network: {
|
network: {
|
||||||
asn: stringValue(data.asn),
|
asn: stringValue(data.asn),
|
||||||
provider: stringValue(data.provider) ?? stringValue(data.organisation),
|
provider: stringValue(data.provider) ?? stringValue(data.organisation),
|
||||||
|
connectionType: stringValue(data.type),
|
||||||
|
proxy: String(data.proxy).toLowerCase() === "yes"
|
||||||
|
? true
|
||||||
|
: String(data.proxy).toLowerCase() === "no" ? false : null,
|
||||||
},
|
},
|
||||||
rawResponse: root,
|
rawResponse: root,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("ProxyCheck.io intelligence", () => {
|
|||||||
longitude: -122.0775,
|
longitude: -122.0775,
|
||||||
timezone: "America/Los_Angeles",
|
timezone: "America/Los_Angeles",
|
||||||
},
|
},
|
||||||
network: { asn: "AS15169", provider: "Google LLC" },
|
network: { asn: "AS15169", provider: "Google LLC", connectionType: "Business", proxy: false },
|
||||||
});
|
});
|
||||||
expect(request).toHaveBeenCalledWith(
|
expect(request).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("https://proxycheck.io/v2/8.8.8.8"),
|
expect.stringContaining("https://proxycheck.io/v2/8.8.8.8"),
|
||||||
|
|||||||
Reference in New Issue
Block a user