fix(admin): prefer non-anonymized map locations
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, parseUserNetwork } from "@/lib/user-location-map";
|
import { MAP_LOCATION_CLASSIFICATIONS, parseUserLocation, parseUserNetwork } from "@/lib/user-location-map";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ export default async function AdminDashboardPage() {
|
|||||||
))
|
))
|
||||||
.where(and(
|
.where(and(
|
||||||
isNotNull(ipObservations.userId),
|
isNotNull(ipObservations.userId),
|
||||||
|
inArray(ipIntelligence.classification, MAP_LOCATION_CLASSIFICATIONS),
|
||||||
sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'latitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'latitude')::double precision between -90 and 90 else false end`,
|
sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'latitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'latitude')::double precision between -90 and 90 else false end`,
|
||||||
sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'longitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'longitude')::double precision between -180 and 180 else false end`,
|
sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'longitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'longitude')::double precision between -180 and 180 else false end`,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { groupMapLocations, parseUserLocation, parseUserNetwork, projectWorldPoint } from "./user-location-map";
|
import {
|
||||||
|
MAP_LOCATION_CLASSIFICATIONS,
|
||||||
|
groupMapLocations,
|
||||||
|
parseUserLocation,
|
||||||
|
parseUserNetwork,
|
||||||
|
projectWorldPoint,
|
||||||
|
} from "./user-location-map";
|
||||||
|
|
||||||
describe("user location map", () => {
|
describe("user location map", () => {
|
||||||
|
it("allows only clear and hosting observations as map locations", () => {
|
||||||
|
expect(MAP_LOCATION_CLASSIFICATIONS).toEqual(["clear", "hosting"]);
|
||||||
|
expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("vpn");
|
||||||
|
expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("proxy");
|
||||||
|
expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("tor");
|
||||||
|
});
|
||||||
|
|
||||||
it("extracts a valid approximate location from cached IP intelligence", () => {
|
it("extracts a valid approximate location from cached IP intelligence", () => {
|
||||||
expect(parseUserLocation({
|
expect(parseUserLocation({
|
||||||
classification: "clear",
|
classification: "clear",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
type UnknownMap = Record<string, unknown>;
|
type UnknownMap = Record<string, unknown>;
|
||||||
|
|
||||||
|
export const MAP_LOCATION_CLASSIFICATIONS = ["clear", "hosting"] as const;
|
||||||
|
|
||||||
function objectValue(value: unknown): UnknownMap | null {
|
function objectValue(value: unknown): UnknownMap | null {
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
return value && typeof value === "object" && !Array.isArray(value)
|
||||||
? value as UnknownMap
|
? value as UnknownMap
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Design Update Log
|
# Design Update Log
|
||||||
|
|
||||||
|
## 2026-08-07
|
||||||
|
|
||||||
|
* **Refine**: Select each admin map marker from the user's latest coordinate-bearing clear or hosting observation while keeping VPN, proxy, and Tor activity in the network-risk view.
|
||||||
|
|
||||||
## 2026-08-02
|
## 2026-08-02
|
||||||
|
|
||||||
* **Extend**: Add recurring UTC group-access windows, browser-local schedule editing, and validated static denial-message variables.
|
* **Extend**: Add recurring UTC group-access windows, browser-local schedule editing, and validated static denial-message variables.
|
||||||
|
|||||||
@@ -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-02T12:05:27Z
|
timestamp: 2026-08-07T22:31:05Z
|
||||||
story_id: US-018
|
story_id: US-018
|
||||||
status: verified
|
status: verified
|
||||||
---
|
---
|
||||||
@@ -15,7 +15,9 @@ As an administrator, I want an operational dashboard of account and game activit
|
|||||||
# Acceptance Criteria
|
# Acceptance Criteria
|
||||||
|
|
||||||
- [x] The administrator landing page is a dashboard rather than a settings form.
|
- [x] The administrator landing page is a dashboard rather than a settings form.
|
||||||
- [x] A server-rendered Natural Earth overview plots each user's latest observation with valid approximate coordinates.
|
- [x] A server-rendered Natural Earth overview plots each user's latest non-anonymized observation with valid approximate coordinates, allowing clear and hosting classifications while excluding VPN, proxy, and Tor observations.
|
||||||
|
- [x] When a user's newest coordinate-bearing observation is VPN, proxy, or Tor, the map uses that user's older clear or hosting observation when one exists.
|
||||||
|
- [x] A user without a coordinate-bearing clear or hosting observation is counted as unavailable on the map.
|
||||||
- [x] Administrators can opt into a zoomable OpenStreetMap view without removing the default overview.
|
- [x] Administrators can opt into a zoomable OpenStreetMap view without removing the default overview.
|
||||||
- [x] OpenStreetMap tiles load only after the administrator selects the interactive view and retain required attribution.
|
- [x] OpenStreetMap tiles load only after the administrator selects the interactive view and retain required attribution.
|
||||||
- [x] Map markers show the managed Discord nickname on hover or keyboard focus, link to user records, and have an accessible text-table equivalent.
|
- [x] Map markers show the managed Discord nickname on hover or keyboard focus, link to user records, and have an accessible text-table equivalent.
|
||||||
@@ -31,7 +33,7 @@ As an administrator, I want an operational dashboard of account and game activit
|
|||||||
- [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.
|
||||||
- [x] The dashboard shows login denials from the previous 24 hours.
|
- [x] The dashboard shows login denials from the previous 24 hours.
|
||||||
- [x] Recent VPN, proxy, and Tor observations use enriched ProxyCheck classifications, collapse repeated rows per user, and show counts, sources, and latest activity.
|
- [x] Recent VPN, proxy, and Tor observations remain available in the separate network-risk section when excluded from map-location selection.
|
||||||
- [x] The graph includes an accessible title, description, point labels, and textual values.
|
- [x] The graph includes an accessible title, description, point labels, and textual values.
|
||||||
- [x] Dashboard queries and initial rendering execute server-side; only the opt-in pan-and-zoom map hydrates client-side.
|
- [x] Dashboard queries and initial rendering execute server-side; only the opt-in pan-and-zoom map hydrates client-side.
|
||||||
- [x] Deployment-managed guild settings and denial messaging remain available on a dedicated settings page.
|
- [x] Deployment-managed guild settings and denial messaging remain available on a dedicated settings page.
|
||||||
@@ -48,8 +50,9 @@ 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, 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.
|
- Coordinate parsing, the clear/hosting map policy, 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-location and user-world-map tests.
|
||||||
- The Next.js production build reports the dashboard and database-backed console pages as dynamic server-rendered routes.
|
- The full test suite passes with 106 tests across 36 files; web type checking and lint pass.
|
||||||
|
- The Next.js production build succeeds and reports the dashboard and database-backed console pages as dynamic server-rendered routes.
|
||||||
|
|
||||||
# Related Stories
|
# Related Stories
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user