From 8ae11c2abfdfcdb4aba84be3fd25666bb8ce8461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Sun, 24 May 2026 22:11:23 +0300 Subject: [PATCH] feat: deploy Domain Scanner via Docker, not GeoExport mirror Replace VPS install with docker compose + nginx. Add network export module, Telegram bot footer card, and archive legacy site/ mirror. Co-authored-by: Cursor --- install.sh | 148 ++++++++++++++--------- site/ARCHIVED.md | 7 ++ src/app/page.tsx | 10 +- src/components/layout/site-footer.tsx | 50 ++++++++ src/components/scan/network-export.tsx | 161 +++++++++++++++++++++++++ src/components/scan/scan-panel.tsx | 5 + uninstall.sh | 2 +- 7 files changed, 314 insertions(+), 69 deletions(-) create mode 100644 site/ARCHIVED.md create mode 100644 src/components/layout/site-footer.tsx create mode 100644 src/components/scan/network-export.tsx diff --git a/install.sh b/install.sh index 72c3a66..7479cdd 100755 --- a/install.sh +++ b/install.sh @@ -1,33 +1,55 @@ #!/usr/bin/env bash -# GeoExport site — one-command install (Ubuntu/Debian VPS) +# Domain Scanner — one-command VPS install (Docker Compose + nginx) set -euo pipefail -REPO_URL="${GEOEXPORT_REPO_URL:-https://github.com/andrey271192/Domain_web.git}" -BRANCH="${GEOEXPORT_BRANCH:-main}" -INSTALL_DIR="${GEOEXPORT_INSTALL_DIR:-/opt/domain_web}" -SITE_DIR="${INSTALL_DIR}/site" -SERVICE_NAME="geoexport-site" -NGINX_SITE="geoexport-site" -NODE_PORT="${GEOEXPORT_PORT:-4173}" +REPO_URL="${DOMAIN_SCANNER_REPO_URL:-https://github.com/andrey271192/Domain_web.git}" +BRANCH="${DOMAIN_SCANNER_BRANCH:-main}" +INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain-scanner}" +SERVICE_NAME="domain-scanner" +NGINX_SITE="domain-scanner" +APP_PORT="${DOMAIN_SCANNER_PORT:-3000}" +LEGACY_SERVICE="geoexport-site" +LEGACY_NGINX="geoexport-site" export DEBIAN_FRONTEND=noninteractive -log() { echo "[geoexport-install] $*"; } +log() { echo "[domain-scanner-install] $*"; } need_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - echo "Запустите от root: sudo bash install.sh" >&2 + echo "Run as root: sudo bash install.sh" >&2 exit 1 fi } install_packages() { - log "Установка пакетов (nginx, git, curl, nodejs)..." + log "Installing system packages..." apt-get update -qq - apt-get install -y -qq nginx git curl ca-certificates nodejs npm >/dev/null + apt-get install -y -qq curl ca-certificates git nginx openssl >/dev/null +} + +install_docker() { + if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + log "Docker already installed" + return + fi + log "Installing Docker..." + curl -fsSL https://get.docker.com | sh + systemctl enable docker + systemctl start docker +} + +stop_legacy() { + log "Stopping legacy GeoExport mirror (if present)..." + systemctl stop "${LEGACY_SERVICE}.service" 2>/dev/null || true + systemctl disable "${LEGACY_SERVICE}.service" 2>/dev/null || true + rm -f "/etc/systemd/system/${LEGACY_SERVICE}.service" + rm -f "/etc/nginx/sites-enabled/${LEGACY_NGINX}" + rm -f "/etc/nginx/sites-available/${LEGACY_NGINX}" + systemctl daemon-reload 2>/dev/null || true } clone_or_update() { - log "Клонирование/обновление репозитория в ${INSTALL_DIR}..." + log "Cloning/updating repository at ${INSTALL_DIR}..." if [[ -d "${INSTALL_DIR}/.git" ]]; then git -C "${INSTALL_DIR}" fetch origin "${BRANCH}" git -C "${INSTALL_DIR}" checkout "${BRANCH}" @@ -36,65 +58,63 @@ clone_or_update() { rm -rf "${INSTALL_DIR}" git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" fi - if [[ ! -f "${SITE_DIR}/server.mjs" ]]; then - echo "Ошибка: не найден ${SITE_DIR}/server.mjs" >&2 - exit 1 +} + +setup_env() { + local ip + ip="$(hostname -I | awk '{print $1}')" + cd "${INSTALL_DIR}" + if [[ ! -f .env ]]; then + cp .env.example .env + fi + local secret + secret="$(openssl rand -base64 32)" + sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=${secret}|" .env + sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=http://${ip}|" .env + sed -i "s|^NEXT_PUBLIC_APP_URL=.*|NEXT_PUBLIC_APP_URL=http://${ip}|" .env + export PORT="${APP_PORT}" +} + +deploy_compose() { + log "Building and starting Docker stack..." + cd "${INSTALL_DIR}" + export PORT="${APP_PORT}" + if docker compose version >/dev/null 2>&1; then + docker compose build --pull + docker compose up -d + docker compose exec -T web npx prisma db push + else + docker-compose build --pull + docker-compose up -d + docker-compose exec -T web npx prisma db push fi } -apply_patches() { - log "Применение патчей фронтенда (export preview/download)..." - git -C "${INSTALL_DIR}" checkout HEAD -- site/assets/index-VQj200iM.js - node "${SITE_DIR}/patch-bundle.mjs" -} - -install_systemd() { - log "Настройка systemd (${SERVICE_NAME})..." - cat >"/etc/systemd/system/${SERVICE_NAME}.service" <"/etc/nginx/sites-available/${NGINX_SITE}" </dev/null || true - rm -f /etc/nginx/sites-available/domain-scanner 2>/dev/null || true rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true ln -sf "/etc/nginx/sites-available/${NGINX_SITE}" "/etc/nginx/sites-enabled/${NGINX_SITE}" nginx -t @@ -103,27 +123,35 @@ EOF } verify() { - log "Проверка..." - sleep 2 - if ! systemctl is-active --quiet "${SERVICE_NAME}.service"; then - systemctl status "${SERVICE_NAME}.service" --no-pager || true + log "Verifying deployment..." + sleep 5 + if ! curl -fsS -o /dev/null "http://127.0.0.1:${APP_PORT}/api/health"; then + echo "App health check failed on port ${APP_PORT}" >&2 + cd "${INSTALL_DIR}" && docker compose logs --tail=40 web 2>/dev/null || true exit 1 fi - if ! curl -fsS -o /dev/null "http://127.0.0.1:${NODE_PORT}/"; then - echo "Node-сервер не отвечает на порту ${NODE_PORT}" >&2 + local title + title="$(curl -fsS "http://127.0.0.1/" | grep -o '[^<]*' | head -1 || true)" + if echo "${title}" | grep -qi geoexport; then + echo "ERROR: GeoExport mirror still served — aborting" >&2 exit 1 fi if ! curl -fsS -o /dev/null "http://127.0.0.1/"; then - echo "nginx не отдаёт сайт на :80" >&2 + echo "nginx not serving on :80" >&2 exit 1 fi - log "Готово. Сайт доступен по http://$(hostname -I | awk '{print $1}')/" + local ip + ip="$(hostname -I | awk '{print $1}')" + log "Done. Domain Scanner: http://${ip}/" + log "Title check: ${title:-ok}" } need_root install_packages +install_docker +stop_legacy clone_or_update -apply_patches -install_systemd +setup_env +deploy_compose install_nginx verify diff --git a/site/ARCHIVED.md b/site/ARCHIVED.md new file mode 100644 index 0000000..3adf32b --- /dev/null +++ b/site/ARCHIVED.md @@ -0,0 +1,7 @@ +# Archived — GeoExport mirror + +The `site/` directory is a **legacy static mirror** of geoexport.org. It is **not** deployed by `install.sh`. + +Production installs use the **Domain Scanner** Next.js app at the repository root (`docker compose`). + +Kept in git for reference only. diff --git a/src/app/page.tsx b/src/app/page.tsx index 721e5e5..fed7912 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,19 +1,13 @@ import { Hero } from "@/components/landing/hero"; import { Features } from "@/components/landing/features"; +import { SiteFooter } from "@/components/layout/site-footer"; export default function HomePage() { return ( <> -
-

Domain Scanner — self-hosted domain intelligence

-

- - GitHub - -

-
+ ); } diff --git a/src/components/layout/site-footer.tsx b/src/components/layout/site-footer.tsx new file mode 100644 index 0000000..0d99597 --- /dev/null +++ b/src/components/layout/site-footer.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export function SiteFooter() { + return ( +
+
+
+

Domain Scanner

+

Self-hosted domain intelligence

+

+ + GitHub + +

+
+ +
+

Telegram domain lookup

+

+ Quick DNS/IP checks and list exports on the go — same idea as a scan, in chat. +

+

+ + @domain_searchPro_bot + +

+ + Open in Telegram + +
+
+
+ ); +} diff --git a/src/components/scan/network-export.tsx b/src/components/scan/network-export.tsx new file mode 100644 index 0000000..d4f02b3 --- /dev/null +++ b/src/components/scan/network-export.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Download, ChevronDown, Network } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { ScanResult } from "@/lib/types"; + +type ExportKind = "ip-list" | "dns-hosts" | "infra-json"; + +function downloadText(filename: string, content: string, mime = "text/plain") { + const blob = new Blob([content], { type: `${mime};charset=utf-8` }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +function buildIpList(result: ScanResult): string { + const ips = new Set(); + if (result.geo?.ip) ips.add(result.geo.ip); + for (const r of result.dns.records) { + if (r.type === "A" || r.type === "AAAA") ips.add(r.value); + } + return [...ips].join("\n") + (ips.size ? "\n" : ""); +} + +function buildHostsFile(result: ScanResult): string { + const lines: string[] = [ + `# Infrastructure hosts — ${result.domain}`, + `# Generated ${result.scannedAt}`, + "", + ]; + for (const r of result.dns.records) { + if (r.type === "A" || r.type === "AAAA") { + lines.push(`${r.value}\t${result.domain}`); + } + } + if (result.geo?.ip && !lines.some((l) => l.startsWith(result.geo!.ip))) { + lines.push(`${result.geo.ip}\t${result.domain}`); + } + return lines.join("\n") + "\n"; +} + +function buildInfraBundle(result: ScanResult): string { + return JSON.stringify( + { + domain: result.domain, + scannedAt: result.scannedAt, + network: { + ips: buildIpList(result).trim().split("\n").filter(Boolean), + dns: result.dns.records, + nameservers: result.dns.nameservers, + mx: result.dns.mx, + }, + edge: { + cdnWaf: result.cdnWaf.detected, + tech: result.tech, + httpStatus: result.http.status, + finalUrl: result.http.finalUrl, + }, + geo: result.geo, + }, + null, + 2 + ); +} + +const OPTIONS: { id: ExportKind; label: string; desc: string; ext: string }[] = [ + { + id: "ip-list", + label: "IP address list", + desc: "Plain text — A/AAAA + resolved IP", + ext: "txt", + }, + { + id: "dns-hosts", + label: "Hosts-style DNS map", + desc: "IP ↔ hostname lines for tooling", + ext: "hosts", + }, + { + id: "infra-json", + label: "Infrastructure bundle", + desc: "DNS, edge, geo subset (not geoip presets)", + ext: "json", + }, +]; + +export function NetworkExport({ result }: { result: ScanResult }) { + const [open, setOpen] = useState(false); + const [kind, setKind] = useState("ip-list"); + + const preview = useMemo(() => { + if (kind === "ip-list") return buildIpList(result).slice(0, 400); + if (kind === "dns-hosts") return buildHostsFile(result).slice(0, 400); + return buildInfraBundle(result).slice(0, 400); + }, [kind, result]); + + const exportFile = () => { + const opt = OPTIONS.find((o) => o.id === kind)!; + const base = `${result.domain}-network`; + if (kind === "ip-list") { + downloadText(`${base}-ips.${opt.ext}`, buildIpList(result)); + } else if (kind === "dns-hosts") { + downloadText(`${base}.${opt.ext}`, buildHostsFile(result)); + } else { + downloadText(`${base}.${opt.ext}`, buildInfraBundle(result), "application/json"); + } + }; + + return ( + + setOpen(!open)}> + + + + Network export + + + +

+ Export resolved IPs and DNS from this scan — infrastructure report, not routing presets. +

+
+ {open && ( + +
+ {OPTIONS.map((o) => ( + + ))} +
+
+            {preview}
+            {preview.length >= 400 ? "\n…" : ""}
+          
+ +
+ )} +
+ ); +} diff --git a/src/components/scan/scan-panel.tsx b/src/components/scan/scan-panel.tsx index fcc04e2..9594b83 100644 --- a/src/components/scan/scan-panel.tsx +++ b/src/components/scan/scan-panel.tsx @@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge, Skeleton } from "@/components/ui/badge"; import type { ScanResult } from "@/lib/types"; import Link from "next/link"; +import { NetworkExport } from "@/components/scan/network-export"; export function ScanPanel() { const searchParams = useSearchParams(); @@ -269,6 +270,10 @@ function ScanResults({ result, scanId }: { result: ScanResult; scanId: string | + +
+ +
); diff --git a/uninstall.sh b/uninstall.sh index 4aecb88..2f3f9f3 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -2,7 +2,7 @@ # Domain Scanner — one-command uninstall set -euo pipefail -INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain_web}" +INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain-scanner}" SERVICE_NAME="domain-scanner" NGINX_SITE="domain-scanner" LEGACY_SERVICE="geoexport-site"