v0.1.4 — MaxMind Basic Auth + Account-ID Field, detailed download errors

This commit is contained in:
Hendrik 2026-05-01 18:41:38 +02:00
parent c710d874b1
commit 2e412b61a7
2 changed files with 90 additions and 19 deletions

View file

@ -29,6 +29,7 @@ export default function SettingsPage() {
const [status, setStatus] = useState<UpdateStatus | null>(null);
const [geo, setGeo] = useState<{ available: boolean; path: string } | null>(null);
const [licenseKey, setLicenseKey] = useState("");
const [accountId, setAccountId] = useState("");
const [installingGeo, setInstallingGeo] = useState(false);
const [geoMsg, setGeoMsg] = useState("");
const [saving, setSaving] = useState(false);
@ -56,15 +57,23 @@ export default function SettingsPage() {
const r = await fetch("/api/settings/geo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ license_key: licenseKey.trim() }),
body: JSON.stringify({
license_key: licenseKey.trim(),
...(accountId.trim() ? { account_id: accountId.trim() } : {}),
}),
});
const d = await r.json();
if (r.ok) {
setGeoMsg("GeoLite2-DB installiert.");
setLicenseKey("");
setAccountId("");
load();
} else {
setGeoMsg(`Fehler: ${d.error || "Download fehlgeschlagen"}`);
const parts = [`Fehler: ${d.error || "Download fehlgeschlagen"}`];
if (d.status) parts.push(`HTTP ${d.status}`);
if (d.detail) parts.push(d.detail);
if (d.hint) parts.push(`${d.hint}`);
setGeoMsg(parts.join(" — "));
}
} finally {
setInstallingGeo(false);
@ -240,23 +249,37 @@ export default function SettingsPage() {
</Button>
</div>
) : (
<div className="space-y-2">
<Label htmlFor="licenseKey">MaxMind License-Key</Label>
<div className="flex gap-2">
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="accountId">MaxMind Account-ID</Label>
<Input
id="licenseKey"
type="password"
placeholder="xxxxxxxxxxxxxxxx"
value={licenseKey}
onChange={(e) => setLicenseKey(e.target.value)}
id="accountId"
type="text"
placeholder="123456"
value={accountId}
onChange={(e) => setAccountId(e.target.value)}
disabled={installingGeo}
/>
<Button onClick={installGeo} disabled={installingGeo || !licenseKey.trim()}>
{installingGeo ? <Loader2 className="mr-2 h-3 w-3 animate-spin" /> : null}
Installieren
</Button>
<p className="text-[11px] text-muted-foreground">Empfohlen neue License-Keys brauchen die Account-ID (Basic Auth).</p>
</div>
<div className="space-y-2">
<Label htmlFor="licenseKey">MaxMind License-Key</Label>
<div className="flex gap-2">
<Input
id="licenseKey"
type="password"
placeholder="xxxxxxxxxxxxxxxx"
value={licenseKey}
onChange={(e) => setLicenseKey(e.target.value)}
disabled={installingGeo}
/>
<Button onClick={installGeo} disabled={installingGeo || !licenseKey.trim()}>
{installingGeo ? <Loader2 className="mr-2 h-3 w-3 animate-spin" /> : null}
Installieren
</Button>
</div>
<p className="text-[11px] text-muted-foreground">Lädt GeoLite2-Country.mmdb herunter. EULA muss im MaxMind-Account akzeptiert sein.</p>
</div>
<p className="text-[11px] text-muted-foreground">Lädt GeoLite2-Country.mmdb herunter und aktiviert Geo-Lookup.</p>
</div>
)}
{geoMsg && <p className="text-xs text-muted-foreground">{geoMsg}</p>}

View file

@ -19,8 +19,25 @@ export async function GET() {
const schema = z.object({
license_key: z.string().min(10),
account_id: z.string().optional(),
});
type DownloadResult = { ok: true; body: Buffer } | { ok: false; status: number; text: string };
async function tryDownload(url: string, headers: Record<string, string> = {}): Promise<DownloadResult> {
const res = await fetch(url, { headers: { "User-Agent": "corex-nexredirect", ...headers } });
if (!res.ok) {
const text = await res.text().catch(() => "");
return { ok: false, status: res.status, text: text.slice(0, 500) };
}
const ct = res.headers.get("content-type") || "";
const buf = Buffer.from(await res.arrayBuffer());
if (ct.includes("text/html") || buf.slice(0, 4).toString() === "<htm" || buf.slice(0, 5).toString() === "<!DOC") {
return { ok: false, status: 200, text: "Server returned HTML, not tar.gz (likely auth failure or EULA not accepted)" };
}
return { ok: true, body: buf };
}
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
@ -29,12 +46,43 @@ export async function POST(req: Request) {
const parsed = schema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: "invalid" }, { status: 400 });
const url = `https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${encodeURIComponent(parsed.data.license_key)}&suffix=tar.gz`;
const { license_key, account_id } = parsed.data;
const attempts: Array<{ url: string; headers?: Record<string, string>; label: string }> = [];
if (account_id) {
const auth = Buffer.from(`${account_id}:${license_key}`).toString("base64");
attempts.push({
url: "https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz",
headers: { Authorization: `Basic ${auth}` },
label: "basic-auth",
});
}
attempts.push({
url: `https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${encodeURIComponent(license_key)}&suffix=tar.gz`,
label: "legacy",
});
let lastErr: { status: number; text: string; label: string } | null = null;
let buf: Buffer | null = null;
for (const a of attempts) {
const r = await tryDownload(a.url, a.headers);
if (r.ok) { buf = r.body; break; }
lastErr = { status: r.status, text: r.text, label: a.label };
}
if (!buf) {
return NextResponse.json({
error: "download_failed",
detail: lastErr?.text || "no detail",
status: lastErr?.status,
hint: account_id
? "MaxMind hat das Tarball nicht ausgeliefert. Stimmen Account-ID und License-Key? GeoLite2 muss im Account aktiviert sein und EULA akzeptiert."
: "Falls dein License-Key über das neue MaxMind-Account-System erstellt wurde, ist die Account-ID nötig (siehe Account → My License Keys oder unter Account-Details).",
}, { status: 502 });
}
try {
const res = await fetch(url);
if (!res.ok) return NextResponse.json({ error: "download_failed", status: res.status }, { status: 502 });
const buf = Buffer.from(await res.arrayBuffer());
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "geo-"));
const tarPath = path.join(tmpDir, "geo.tgz");
await fs.writeFile(tarPath, buf);