mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(version): move the finer geo state to the admin status
geoLookup and geoProviderChosen on the public /api/version added one
more way to tell instances apart. They now live on /api/admin/status
(requireAdmin, cookie only) as geo { lookup, providerChosen,
providerHost }, which both admin status views read. offlineGeoEnabled
and geoProviderHost stay public as before.
This commit is contained in:
1 parent
0ea8d929d2
commit
470ddeedc4
11 files changed
+135
-128
No files matched your search
@@ -36065,18 +36065,6 @@ components:
|
||||
type: string
|
||||
description: Host of the online geolocation provider the fallback would use, so an operator surface names the real one
|
||||
instead of assuming the default.
|
||||
geoLookup:
|
||||
type: string
|
||||
enum:
|
||||
- offline
|
||||
- online
|
||||
- off
|
||||
description: "How login IPs are placed (since v1.42): `offline` from the GeoLite2 databases, `online` by the provider
|
||||
above, `off` not at all (`IP_GEO_LOOKUP_DISABLED`)."
|
||||
geoProviderChosen:
|
||||
type: boolean
|
||||
description: The operator named the online provider in `IP_GEO_LOOKUP_URL` (since v1.42). False while the default is in
|
||||
use.
|
||||
required:
|
||||
- version
|
||||
- buildSha
|
||||
@@ -36087,8 +36075,6 @@ components:
|
||||
- docs
|
||||
- offlineGeoEnabled
|
||||
- geoProviderHost
|
||||
- geoLookup
|
||||
- geoProviderChosen
|
||||
additionalProperties: false
|
||||
description: The running build's identity plus the offline-geolocation state. Public — no authentication, no account data.
|
||||
AssistantFlagsEnvelope:
|
||||
|
||||
@@ -44,6 +44,17 @@ vi.mock("@/lib/jobs/worker-status", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
const geoMock = vi.hoisted(() => ({
|
||||
lookup: "online" as "offline" | "online" | "off",
|
||||
chosen: false,
|
||||
host: "ipwho.is",
|
||||
}));
|
||||
vi.mock("@/lib/geo", () => ({
|
||||
geoLookupMode: () => geoMock.lookup,
|
||||
geoProviderChosen: () => geoMock.chosen,
|
||||
resolveGeoProviderHost: () => geoMock.host,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/jobs/job-failures", () => ({
|
||||
JOB_FAILURE_WINDOW_HOURS: 72,
|
||||
readFailingQueues: vi.fn(),
|
||||
@@ -208,4 +219,17 @@ describe("GET /api/admin/status (integration health summary)", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("carries the login-IP geo state the public version route no longer shows", async () => {
|
||||
geoMock.lookup = "online";
|
||||
geoMock.chosen = true;
|
||||
geoMock.host = "ip-api.com";
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as { data: { geo: unknown } };
|
||||
expect(body.data.geo).toEqual({
|
||||
lookup: "online",
|
||||
providerChosen: true,
|
||||
providerHost: "ip-api.com",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
readFailingQueues,
|
||||
} from "@/lib/jobs/job-failures";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
geoLookupMode,
|
||||
geoProviderChosen,
|
||||
resolveGeoProviderHost,
|
||||
} from "@/lib/geo";
|
||||
|
||||
// Build timestamp (set at build time via next.config)
|
||||
const BUILD_TIME = process.env.BUILD_TIMESTAMP || new Date().toISOString();
|
||||
@@ -109,5 +114,12 @@ export const GET = apiHandler(async () => {
|
||||
: null,
|
||||
webPush: webPushConfigured ? { configured: true } : null,
|
||||
},
|
||||
// How login IPs are placed on this host. Admin-only: the public
|
||||
// `/api/version` keeps just `offlineGeoEnabled` and `geoProviderHost`.
|
||||
geo: {
|
||||
lookup: geoLookupMode(),
|
||||
providerChosen: geoProviderChosen(),
|
||||
providerHost: resolveGeoProviderHost(),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,6 @@ const resolveGeoProviderHostMock = vi.fn(() => "ipwho.is");
|
||||
vi.mock("@/lib/geo", () => ({
|
||||
offlineGeoReady: () => offlineGeoReadyMock(),
|
||||
resolveGeoProviderHost: () => resolveGeoProviderHostMock(),
|
||||
geoLookupMode: () => (offlineGeoReadyMock() ? "offline" : "online"),
|
||||
geoProviderChosen: () => false,
|
||||
}));
|
||||
|
||||
import { GET } from "../route";
|
||||
@@ -108,4 +106,23 @@ describe("GET /api/version", () => {
|
||||
const body = (await response.json()) as VersionEnvelope;
|
||||
expect(body.data.geoProviderHost).toBe("ip-api.com");
|
||||
});
|
||||
|
||||
it("keeps the finer geo state off the public answer", async () => {
|
||||
// Lookup mode and whether the operator chose the provider are admin-only
|
||||
// (`/api/admin/status`); the public route would only add a way to tell
|
||||
// instances apart.
|
||||
const response = await (GET as unknown as () => Promise<Response>)();
|
||||
const body = (await response.json()) as { data: Record<string, unknown> };
|
||||
expect(Object.keys(body.data).sort()).toEqual([
|
||||
"buildSha",
|
||||
"builtAt",
|
||||
"changelog",
|
||||
"docs",
|
||||
"geoProviderHost",
|
||||
"license",
|
||||
"offlineGeoEnabled",
|
||||
"repository",
|
||||
"version",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,6 @@
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import {
|
||||
geoLookupMode,
|
||||
geoProviderChosen,
|
||||
offlineGeoReady,
|
||||
resolveGeoProviderHost,
|
||||
} from "@/lib/geo";
|
||||
import { offlineGeoReady, resolveGeoProviderHost } from "@/lib/geo";
|
||||
import packageJson from "../../../../package.json";
|
||||
|
||||
// v1.4.27 R5 — `offlineGeoEnabled` reads from the same source the geo
|
||||
@@ -41,7 +36,10 @@ export const dynamic = "force-dynamic";
|
||||
* `geoProviderHost` (the host of `IP_GEO_LOOKUP_URL`, default
|
||||
* `ipwho.is`) and the resolver emits a one-shot admin notification on
|
||||
* first use. `geoProviderHost` lets the admin status surface name the
|
||||
* real provider instead of assuming the default.
|
||||
* real provider instead of assuming the default. Both were public before
|
||||
* v1.42 and stay so for existing readers; the finer geo state (lookup mode,
|
||||
* whether the operator chose the provider) is admin-only and lives on
|
||||
* `/api/admin/status`, so it adds nothing to tell instances apart by.
|
||||
*/
|
||||
export const GET = apiHandler(async () => {
|
||||
// v1.4.43 B11 — prefer the build-arg-injected env var so the runtime
|
||||
@@ -69,7 +67,5 @@ export const GET = apiHandler(async () => {
|
||||
docs: "https://docs.healthlog.dev",
|
||||
offlineGeoEnabled,
|
||||
geoProviderHost,
|
||||
geoLookup: geoLookupMode(),
|
||||
geoProviderChosen: geoProviderChosen(),
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { geoLookupState, type PublicVersion } from "../_shared";
|
||||
import { geoLookupState, type AdminGeoStatus } from "../_shared";
|
||||
|
||||
const base: PublicVersion = {
|
||||
version: "1.42.0",
|
||||
buildSha: null,
|
||||
builtAt: null,
|
||||
const base: AdminGeoStatus = {
|
||||
lookup: "online",
|
||||
providerChosen: false,
|
||||
providerHost: "ipwho.is",
|
||||
};
|
||||
|
||||
describe("geoLookupState", () => {
|
||||
it("warns only about the unconfigured default provider", () => {
|
||||
expect(
|
||||
geoLookupState({
|
||||
...base,
|
||||
offlineGeoEnabled: false,
|
||||
geoLookup: "online",
|
||||
}),
|
||||
).toBe("default");
|
||||
expect(geoLookupState(base)).toBe("default");
|
||||
});
|
||||
|
||||
it("treats a provider the operator named as a decision", () => {
|
||||
expect(
|
||||
geoLookupState({
|
||||
...base,
|
||||
offlineGeoEnabled: false,
|
||||
geoLookup: "online",
|
||||
geoProviderChosen: true,
|
||||
}),
|
||||
).toBe("chosen");
|
||||
expect(geoLookupState({ ...base, providerChosen: true })).toBe("chosen");
|
||||
});
|
||||
|
||||
it("reads a switched-off lookup and the offline databases as settled", () => {
|
||||
expect(
|
||||
geoLookupState({ ...base, offlineGeoEnabled: false, geoLookup: "off" }),
|
||||
).toBe("off");
|
||||
expect(geoLookupState({ ...base, offlineGeoEnabled: true })).toBe(
|
||||
"offline",
|
||||
);
|
||||
expect(geoLookupState({ ...base, lookup: "off" })).toBe("off");
|
||||
expect(geoLookupState({ ...base, lookup: "offline" })).toBe("offline");
|
||||
});
|
||||
});
|
||||
@@ -50,6 +50,11 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
activeSessions: 1,
|
||||
},
|
||||
integrations: { umami: null, glitchtip: null, webPush: null },
|
||||
geo: {
|
||||
lookup: "offline",
|
||||
providerChosen: false,
|
||||
providerHost: "ipwho.is",
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
||||
@@ -81,6 +81,8 @@ export interface SystemStatus {
|
||||
glitchtip: { configured: boolean; enabled: boolean } | null;
|
||||
webPush: { configured: boolean } | null;
|
||||
};
|
||||
/** Login-IP geolocation state (admin-only since v1.42). */
|
||||
geo: AdminGeoStatus;
|
||||
}
|
||||
|
||||
export interface AdminSettings {
|
||||
@@ -590,10 +592,16 @@ export interface PublicVersion {
|
||||
// offline tier is absent (host of IP_GEO_LOOKUP_URL, default ipwho.is).
|
||||
// Optional so legacy /api/version responses still satisfy the shape.
|
||||
geoProviderHost?: string;
|
||||
/** v1.42 — `offline`, `online` or `off` (IP_GEO_LOOKUP_DISABLED). */
|
||||
geoLookup?: "offline" | "online" | "off";
|
||||
/** v1.42 — the operator named the provider in IP_GEO_LOOKUP_URL. */
|
||||
geoProviderChosen?: boolean;
|
||||
}
|
||||
|
||||
/** How login IPs are placed on this host, as `/api/admin/status` reports it. */
|
||||
export interface AdminGeoStatus {
|
||||
/** `offline`, `online` or `off` (IP_GEO_LOOKUP_DISABLED). */
|
||||
lookup: "offline" | "online" | "off";
|
||||
/** The operator named the provider in IP_GEO_LOOKUP_URL. */
|
||||
providerChosen: boolean;
|
||||
/** Host of the online provider (IP_GEO_LOOKUP_URL, default ipwho.is). */
|
||||
providerHost: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,15 +610,15 @@ export interface PublicVersion {
|
||||
* warns, because it sends every login IP to a third party nobody chose.
|
||||
*/
|
||||
export function geoLookupState(
|
||||
version: PublicVersion,
|
||||
geo: AdminGeoStatus,
|
||||
): "offline" | "off" | "chosen" | "default" {
|
||||
if (version.offlineGeoEnabled) return "offline";
|
||||
if (version.geoLookup === "off") return "off";
|
||||
return version.geoProviderChosen ? "chosen" : "default";
|
||||
if (geo.lookup === "offline") return "offline";
|
||||
if (geo.lookup === "off") return "off";
|
||||
return geo.providerChosen ? "chosen" : "default";
|
||||
}
|
||||
|
||||
/** Shared `/api/version` reader — the system-status section + overview
|
||||
* summary both surface app version / build SHA / offline-geo state. */
|
||||
/** Shared `/api/version` reader for the version tile (version, build SHA,
|
||||
* update check). The geo state comes from `/api/admin/status`. */
|
||||
export function usePublicVersion() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.publicVersion(),
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
geoLookupState,
|
||||
StatusItem,
|
||||
usePublicVersion,
|
||||
useSystemStatus,
|
||||
type FailingQueue,
|
||||
} from "./_shared";
|
||||
@@ -129,7 +128,6 @@ export function SystemStatusSection() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { data: status, isError, refetch } = useSystemStatus();
|
||||
const { data: version } = usePublicVersion();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -238,30 +236,27 @@ export function SystemStatusSection() {
|
||||
tone="success"
|
||||
/>
|
||||
)}
|
||||
{/* v1.4.27 R5 — offline GeoLite2 availability. Renders only
|
||||
when /api/version returns the new field so legacy
|
||||
responses do not produce a placeholder row. */}
|
||||
{version?.offlineGeoEnabled !== undefined && (
|
||||
<StatusItem
|
||||
icon={Map}
|
||||
label={t("admin.offlineGeoLabel")}
|
||||
value={
|
||||
{
|
||||
offline: t("admin.offlineGeoEnabled"),
|
||||
off: t("admin.offlineGeoOff"),
|
||||
chosen: t("admin.offlineGeoChosen", {
|
||||
host: version.geoProviderHost ?? "ipwho.is",
|
||||
}),
|
||||
default: t("admin.offlineGeoFallback", {
|
||||
host: version.geoProviderHost ?? "ipwho.is",
|
||||
}),
|
||||
}[geoLookupState(version)]
|
||||
}
|
||||
tone={
|
||||
geoLookupState(version) === "default" ? "warning" : "success"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* v1.4.27 R5 — offline GeoLite2 availability, read from the
|
||||
admin status rather than the public /api/version. */}
|
||||
<StatusItem
|
||||
icon={Map}
|
||||
label={t("admin.offlineGeoLabel")}
|
||||
value={
|
||||
{
|
||||
offline: t("admin.offlineGeoEnabled"),
|
||||
off: t("admin.offlineGeoOff"),
|
||||
chosen: t("admin.offlineGeoChosen", {
|
||||
host: status.geo.providerHost,
|
||||
}),
|
||||
default: t("admin.offlineGeoFallback", {
|
||||
host: status.geo.providerHost,
|
||||
}),
|
||||
}[geoLookupState(status.geo)]
|
||||
}
|
||||
tone={
|
||||
geoLookupState(status.geo) === "default" ? "warning" : "success"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : isError ? (
|
||||
// P19: surface load failures inline instead of leaving the
|
||||
|
||||
@@ -26,18 +26,12 @@ import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
geoLookupState,
|
||||
StatusItem,
|
||||
usePublicVersion,
|
||||
useSystemStatus,
|
||||
} from "./_shared";
|
||||
import { geoLookupState, StatusItem, useSystemStatus } from "./_shared";
|
||||
import { useKeyBackupStatus } from "./use-key-backup-status";
|
||||
|
||||
export function SystemStatusSummary() {
|
||||
const { t } = useTranslations();
|
||||
const { data: status, isError, refetch } = useSystemStatus();
|
||||
const { data: version } = usePublicVersion();
|
||||
const { data: keyBackup } = useKeyBackupStatus();
|
||||
|
||||
return (
|
||||
@@ -92,30 +86,27 @@ export function SystemStatusSummary() {
|
||||
/>
|
||||
)}
|
||||
{/* v1.4.27 R5 — surface the offline-geo state so the maintainer
|
||||
spots the missing MAXMIND_LICENSE_KEY without crawling logs.
|
||||
The field is undefined on legacy responses; the row only
|
||||
renders when /api/version answers the new shape. */}
|
||||
{version?.offlineGeoEnabled !== undefined && (
|
||||
<StatusItem
|
||||
icon={Globe}
|
||||
label={t("admin.overview.snapshotOfflineGeo")}
|
||||
value={
|
||||
{
|
||||
offline: t("admin.overview.snapshotOfflineGeoOn"),
|
||||
off: t("admin.offlineGeoOff"),
|
||||
chosen: t("admin.offlineGeoChosen", {
|
||||
host: version.geoProviderHost ?? "ipwho.is",
|
||||
}),
|
||||
default: t("admin.overview.snapshotOfflineGeoOff", {
|
||||
host: version.geoProviderHost ?? "ipwho.is",
|
||||
}),
|
||||
}[geoLookupState(version)]
|
||||
}
|
||||
tone={
|
||||
geoLookupState(version) === "default" ? "warning" : "success"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
spots the missing GeoLite2 databases without crawling logs.
|
||||
Read from the admin status, not the public /api/version. */}
|
||||
<StatusItem
|
||||
icon={Globe}
|
||||
label={t("admin.overview.snapshotOfflineGeo")}
|
||||
value={
|
||||
{
|
||||
offline: t("admin.overview.snapshotOfflineGeoOn"),
|
||||
off: t("admin.offlineGeoOff"),
|
||||
chosen: t("admin.offlineGeoChosen", {
|
||||
host: status.geo.providerHost,
|
||||
}),
|
||||
default: t("admin.overview.snapshotOfflineGeoOff", {
|
||||
host: status.geo.providerHost,
|
||||
}),
|
||||
}[geoLookupState(status.geo)]
|
||||
}
|
||||
tone={
|
||||
geoLookupState(status.geo) === "default" ? "warning" : "success"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorRow
|
||||
|
||||
@@ -239,16 +239,6 @@ const versionResponse = z
|
||||
.describe(
|
||||
"Host of the online geolocation provider the fallback would use, so an operator surface names the real one instead of assuming the default.",
|
||||
),
|
||||
geoLookup: z
|
||||
.enum(["offline", "online", "off"])
|
||||
.describe(
|
||||
"How login IPs are placed (since v1.42): `offline` from the GeoLite2 databases, `online` by the provider above, `off` not at all (`IP_GEO_LOOKUP_DISABLED`).",
|
||||
),
|
||||
geoProviderChosen: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"The operator named the online provider in `IP_GEO_LOOKUP_URL` (since v1.42). False while the default is in use.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "VersionResponse",
|
||||
|
||||
Reference in new issue
Block a user