diff --git a/app/(app)/settings/page.tsx b/app/(app)/settings/page.tsx index 632d816..2c4b422 100644 --- a/app/(app)/settings/page.tsx +++ b/app/(app)/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import { Loader2, RefreshCcw, ArrowUpCircle, Globe2, CheckCircle2, Trash2, Mail, Send, Pencil, Server, Bell } from "lucide-react"; +import { Loader2, RefreshCcw, ArrowUpCircle, Globe2, CheckCircle2, Trash2, Mail, Send, Pencil, Server, Bell, ShieldCheck, AlertTriangle } from "lucide-react"; import { PageHeader } from "@/components/PageHeader"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -18,6 +18,8 @@ type Settings = { webhook_url: string | null; }; +type IpAllowlist = { allowlist: string[]; my_ip: string }; + type UpdateStatus = { current: string; latest: string | null; @@ -41,6 +43,7 @@ export default function SettingsPage() { const [status, setStatus] = useState(null); const [geo, setGeo] = useState<{ available: boolean; path: string } | null>(null); const [smtp, setSmtp] = useState(null); + const [ipAllowlist, setIpAllowlist] = useState(null); const [checking, setChecking] = useState(false); const [applying, setApplying] = useState(false); @@ -50,13 +53,15 @@ export default function SettingsPage() { const [smtpOpen, setSmtpOpen] = useState(false); const [geoOpen, setGeoOpen] = useState(false); const [notifyOpen, setNotifyOpen] = useState(false); + const [ipOpen, setIpOpen] = useState(false); async function load() { - const [s, u, g, m] = await Promise.all([ + const [s, u, g, m, ip] = await Promise.all([ fetch("/api/settings").then((r) => r.json()), fetch("/api/update/check").then((r) => r.json()), fetch("/api/settings/geo").then((r) => r.json()), fetch("/api/settings/smtp").then((r) => r.json()), + fetch("/api/settings/ip-allowlist").then((r) => r.json()), ]); setSettings(s); setStatus(u); @@ -69,6 +74,7 @@ export default function SettingsPage() { smtp_from: m.smtp_from || "", smtp_secure: m.smtp_secure || "false", }); + setIpAllowlist(ip as IpAllowlist); } useEffect(() => { load(); }, []); @@ -119,7 +125,7 @@ export default function SettingsPage() { } } - if (!settings || !status || !geo || !smtp) { + if (!settings || !status || !geo || !smtp || !ipAllowlist) { return
; } @@ -238,6 +244,32 @@ export default function SettingsPage() { + {/* IP-Allowlist */} + + + + IP-Zugriffsbeschränkung + {ipAllowlist.allowlist.length > 0 + ? {ipAllowlist.allowlist.length} Einträge + : offen} + + Admin-UI nur für bestimmte IPs / CIDR-Bereiche + + + {ipAllowlist.my_ip} + {ipAllowlist.allowlist.length > 0 ? ( + + {ipAllowlist.allowlist.slice(0, 2).join(", ")}{ipAllowlist.allowlist.length > 2 ? ` +${ipAllowlist.allowlist.length - 2}` : ""} + + ) : ( +

Kein Filter — alle IPs haben Zugriff.

+ )} + +
+
+ {/* Benachrichtigungen / Retention */} @@ -258,6 +290,7 @@ export default function SettingsPage() { setSmtpOpen(false)} initial={smtp} onSaved={load} /> setGeoOpen(false)} status={geo} onChanged={load} /> setNotifyOpen(false)} settings={settings} onSave={saveSettings} /> + setIpOpen(false)} initial={ipAllowlist} onSaved={load} /> ); } @@ -420,6 +453,69 @@ function GeoDialog({ open, onClose, status, onChanged }: { open: boolean; onClos ); } +function IpAllowlistDialog({ open, onClose, initial, onSaved }: { open: boolean; onClose: () => void; initial: IpAllowlist; onSaved: () => void }) { + const [text, setText] = useState(initial.allowlist.join("\n")); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(""); + useEffect(() => { if (open) { setText(initial.allowlist.join("\n")); setMsg(""); } }, [open, initial]); + + const parsed = text.split("\n").map((l) => l.trim()).filter(Boolean); + const myIpIncluded = parsed.length === 0 || parsed.some((e) => e === initial.my_ip || initial.my_ip.startsWith(e.split("/")[0])); + + async function save() { + setBusy(true); setMsg(""); + try { + const r = await fetch("/api/settings/ip-allowlist", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ allowlist: parsed }) }); + const d = await r.json(); + if (!r.ok) { setMsg(`Fehler: ${d.error}`); return; } + onSaved(); + onClose(); + } finally { setBusy(false); } + } + + return ( + !v && onClose()}> + + + IP-Zugriffsbeschränkung + Eine IP oder CIDR pro Zeile. Leer = alle IPs erlaubt. Gilt für die gesamte Admin-UI (außer /api/v1). + +
+
+ Deine IP: + {initial.my_ip} +
+ {!myIpIncluded && ( +
+ + Deine IP ist nicht in der Liste — du sperrst dich selbst aus! +
+ )} +
+ +