"use client"; import { useEffect, useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; import { useAuthStore } from "@/stores/auth-store"; import { Bot, Shield, Wrench, Save, AlertTriangle, Download, Upload, Database, Info, Eye, EyeOff, ShieldCheck, Power, Copy, Globe, Link2 } from "lucide-react"; const MASKED_PLACEHOLDER = "\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF\u25CF"; const KEY_META: Record = { BOT_TOKEN: { label: "Bot Token", description: "Telegram Bot API token from @BotFather" }, SUPPORT_LINK: { label: "Support Link", description: "URL shown to users for support" }, ADMIN_IDS: { label: "Admin Telegram IDs", description: "Comma-separated list of admin user IDs" }, SUPER_ADMIN_IDS: { label: "Super Admin IDs", description: "Comma-separated list of super admin IDs" }, WG_ENABLED: { label: "WireGuard VPN", description: "Enable WireGuard VPN for product delivery" }, WG_ENDPOINT: { label: "VPN Endpoint", description: "WireGuard server endpoint address" }, WG_ADDRESS: { label: "VPN Address", description: "WireGuard client address" }, WG_PUBLIC_KEY: { label: "VPN Public Key", description: "WireGuard server public key" }, WG_DNS: { label: "VPN DNS", description: "DNS server for VPN connection" }, ADMIN_PORT: { label: "Admin Port", description: "Port for the admin panel HTTP server" }, ADMIN_URL: { label: "Admin URL", description: "Public URL for the admin panel" }, CATALOG_PATH: { label: "Catalog Path", description: "File system path to the product catalog" }, GITEA_API_URL: { label: "Gitea API URL", description: "Gitea instance API endpoint URL" }, }; const SECTIONS = [ { title: "Bot Configuration", keys: ["BOT_TOKEN", "SUPPORT_LINK", "ADMIN_IDS", "SUPER_ADMIN_IDS"], icon: Bot, description: "Telegram bot and user management settings", }, { title: "WireGuard VPN", keys: ["WG_ENABLED", "WG_ENDPOINT", "WG_ADDRESS", "WG_PUBLIC_KEY", "WG_DNS"], icon: Shield, description: "VPN configuration for secure product delivery", }, { title: "Admin Panel", keys: ["ADMIN_PORT", "ADMIN_URL", "CATALOG_PATH", "GITEA_API_URL"], icon: Wrench, description: "Admin panel server and integration settings", }, ]; function SkeletonForm() { return (
{Array.from({ length: 4 }).map((_, i) => (
))}
); } export function SettingsPage() { const { role } = useAuthStore(); const isSuperAdmin = role === "super_admin"; const [settings, setSettings] = useState | null>(null); const [masked, setMasked] = useState([]); const [saving, setSaving] = useState(null); const [exporting, setExporting] = useState(false); const [importing, setImporting] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false); const fileInputRef = useRef(null); const [revealed, setRevealed] = useState>({}); const [revealing, setRevealing] = useState(null); const [activated, setActivated] = useState(null); const [activationLoading, setActivationLoading] = useState(false); const [activationDialogOpen, setActivationDialogOpen] = useState(false); const [onion, setOnion] = useState(""); const [lanUrl, setLanUrl] = useState(""); useEffect(() => { if (!isSuperAdmin) return; let cancelled = false; (async () => { try { const res = await fetch("/api/activation"); if (!res.ok) throw new Error("Failed to fetch activation"); const data = await res.json(); if (!cancelled) setActivated(!!data.activated); } catch { if (!cancelled) setActivated(null); } })(); return () => { cancelled = true; }; }, [isSuperAdmin]); useEffect(() => { if (!isSuperAdmin) return; let cancelled = false; (async () => { try { const res = await fetch("/api/system/info"); if (!res.ok) throw new Error("Failed to fetch system info"); const data = await res.json(); if (cancelled) return; setOnion(data.adminOnion || ""); setLanUrl(data.lanUrl || ""); } catch { if (cancelled) return; setOnion(""); setLanUrl(""); } })(); return () => { cancelled = true; }; }, [isSuperAdmin]); const copyText = async (text: string, label: string) => { if (!text) return; try { await navigator.clipboard.writeText(text); toast.success(`${label} copied`); } catch { toast.error(`Failed to copy ${label}`); } }; const handleToggleActivation = async () => { if (activated === null) return; const next = !activated; setActivationLoading(true); try { const res = await fetch("/api/activation", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ activated: next }), }); if (!res.ok) throw new Error("Failed to update activation"); const data = await res.json(); setActivated(!!data.activated); toast.success(next ? "Shop activated" : "Shop blocked"); } catch { toast.error("Failed to update shop activation"); } finally { setActivationLoading(false); setActivationDialogOpen(false); } }; useEffect(() => { fetch("/api/settings") .then((res) => res.json()) .then((data) => { const m: string[] = data._masked || []; setMasked(m); delete data._masked; setSettings(data); }) .catch(() => toast.error("Failed to load settings")); }, []); const handleSave = async (key: string) => { if (!settings) return; setSaving(key); try { const res = await fetch("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: settings[key] }), }); if (!res.ok) throw new Error(); toast.success(`${KEY_META[key]?.label || key} saved. Restart required.`); } catch { toast.error(`Failed to save ${KEY_META[key]?.label || key}`); } finally { setSaving(null); } }; const handleChange = (key: string, value: string) => { setSettings((prev) => (prev ? { ...prev, [key]: value } : prev)); }; // Показать/скрыть реальное значение секрета (глазик) const handleReveal = async (key: string) => { if (revealed[key] !== undefined) { // Повторный клик — скрыть setRevealed((prev) => { const next = { ...prev }; delete next[key]; return next; }); return; } setRevealing(key); try { const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key }), }); if (!res.ok) throw new Error(); const data = await res.json(); setRevealed((prev) => ({ ...prev, [key]: data.value ?? "" })); } catch { toast.error(`Failed to reveal ${KEY_META[key]?.label || key}`); } finally { setRevealing(null); } }; const handleSwitchChange = (key: string, checked: boolean) => { setSettings((prev) => (prev ? { ...prev, [key]: checked } : prev)); }; const handleExport = async () => { setExporting(true); try { const res = await fetch("/api/settings/export"); if (!res.ok) throw new Error(); const data = await res.json(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `telegram-shop-backup-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success("Data exported successfully"); } catch { toast.error("Failed to export data"); } finally { setExporting(false); } }; const handleImport = async () => { const file = fileInputRef.current?.files?.[0]; if (!file) return; setImporting(true); try { const text = await file.text(); const data = JSON.parse(text); const res = await fetch("/api/settings/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error(); const result = await res.json(); if (result.ok) { toast.info(result.message || "Import not yet implemented"); } else { toast.error(result.error || "Import failed"); } } catch { toast.error("Failed to import data. Ensure the file is valid JSON."); } finally { setImporting(false); setImportDialogOpen(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }; return (
Restart the application to apply changes.

Configure bot settings, WireGuard VPN, and admin panel options. Changes require a restart.

{!settings ? (
{SECTIONS.map((s) => ( {s.title} {s.description} ))}
) : (
{SECTIONS.map((section) => { const SectionIcon = section.icon; return ( {section.title} {section.description}
{section.keys.map((key) => { const isMasked = masked.includes(key); const isBool = key === "WG_ENABLED"; const value = String(settings[key] ?? ""); const meta = KEY_META[key]; return (
{meta && ( {meta.description} )}
{meta && (

{meta.description}

)} {isBool ? (
handleSwitchChange(key, checked)} /> {settings[key] ? "Enabled" : "Disabled"}
) : isMasked ? (
handleChange(key, e.target.value)} className="flex-1 font-mono" type={revealed[key] !== undefined ? "text" : "password"} />
) : ( handleChange(key, e.target.value)} className="max-w-md" /> )}
{!isMasked && ( )}
); })}
); })}
)} {/* Data Management Section */} Data Management Export all database records as JSON for backup, or import from a previous backup file.
Exports all 10 database tables including users, wallets, purchases, and settings.
{isSuperAdmin && ( \u26A0\uFE0F Confirm Data Import Importing data will overwrite existing records. This is a dangerous operation that cannot be undone. Make sure you have a recent backup before proceeding. Only super admins can perform this action.
Cancel {importing ? "Importing..." : "Proceed with Import"}
)}
{/* Shop Activation (super admin only) */} {isSuperAdmin && ( Shop Activation Control whether the shop is active and accepting orders. Blocking the shop immediately disables purchasing for all users.
{activated === null ? ( Checking... ) : activated ? ( Activated ) : ( Blocked )} {activated === null ? "Loading activation status..." : activated ? "The shop is currently active and accepting orders." : "The shop is currently blocked. Users cannot place orders."}
Onion:
LAN:
{activated ? "Block Shop" : "Activate Shop"} {activated ? "Blocking the shop will immediately prevent all users from placing orders. This action can be reversed at any time." : "Activating the shop will allow users to place orders again. This action can be reversed at any time."} Cancel {activationLoading ? "Updating..." : activated ? "Block Shop" : "Activate Shop"}
)}
); }