From c3434ba45de06261e31de9ab2a0dce295e284e97 Mon Sep 17 00:00:00 2001 From: Hendrik Garske Date: Thu, 4 Jun 2026 22:16:39 +0200 Subject: [PATCH] Import wiki from GitHub --- API.md | 182 +++++++++++++++++++++++++++++++++++++++++ Analytics-&-Reports.md | 73 +++++++++++++++++ Architecture.md | 125 ++++++++++++++++++++++++++++ Bot-Filter.md | 85 +++++++++++++++++++ CLI.md | 69 ++++++++++++++++ DNS-Setup.md | 43 ++++++++++ Domain-Management.md | 52 ++++++++++++ Home.md | 43 ++++++++++ Installation.md | 95 +++++++++++++++++++++ Sunset-Pages.md | 48 +++++++++++ Troubleshooting.md | 136 ++++++++++++++++++++++++++++++ Updates.md | 78 ++++++++++++++++++ _Sidebar.md | 20 +++++ 13 files changed, 1049 insertions(+) create mode 100644 API.md create mode 100644 Analytics-&-Reports.md create mode 100644 Architecture.md create mode 100644 Bot-Filter.md create mode 100644 CLI.md create mode 100644 DNS-Setup.md create mode 100644 Domain-Management.md create mode 100644 Home.md create mode 100644 Installation.md create mode 100644 Sunset-Pages.md create mode 100644 Troubleshooting.md create mode 100644 Updates.md create mode 100644 _Sidebar.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..f1e43c0 --- /dev/null +++ b/API.md @@ -0,0 +1,182 @@ +# API + +REST-API unter `/api/v1/*`. Versioniert und stabil. JSON-only. + +## Auth + +Tokens in der UI unter **Einstellungen → API-Tokens** erstellen. Token wird nur einmalig angezeigt, danach nur als sha256-Hash in der DB. + +Format: `nrx_<64-hex>`. Im Header senden: + +``` +Authorization: Bearer nrx_ +``` + +## Scopes + +| Scope | Zugriff | +|---|---| +| `read:domains` | Domains lesen | +| `write:domains` | Domains anlegen / löschen | +| `read:analytics` | Aggregierte Statistiken | +| `read:hits` | Roh-Hits (nur ip_hash, kein Klartext-IP) | + +Fehler-Format: + +```json +{ "error": "forbidden", "code": "missing_scope", "required": "read:domains" } +``` + +## Endpoints + +### `GET /api/v1/health` + +Liveness-Check. Kein Token nötig. + +```bash +curl https://admin.firma.de/api/v1/health +# {"ok":true,"ts":1714500000000} +``` + +### `GET /api/v1/version` + +Aktuelle Version + Update-Status. Kein Token nötig. + +```json +{ + "current": "0.1.19", + "latest": "0.1.19", + "update_available": false, + "auto_update": false +} +``` + +### `GET /api/v1/domains` + +Scope: `read:domains` + +```json +{ + "domains": [ + { + "id": 1, "domain": "alt-firma.de", "status": "active", + "target_url": "https://www.firma.de", "redirect_code": 302, + "preserve_path": 1, "include_www": 1, + "total_hits": 142, "last_hit": 1714499000000 + } + ] +} +``` + +### `POST /api/v1/domains` + +Scope: `write:domains` + +Body: + +```json +{ + "domain": "alt-firma.de", + "target_url": "https://www.firma.de", + "redirect_code": 302, + "preserve_path": true, + "include_www": true +} +``` + +Response (`201`): + +```json +{ + "domain": { "id": 5, "status": "pending", ... }, + "dns_records": [ + { "type": "A", "name": "alt-firma.de", "value": "203.0.113.42" }, + { "type": "A", "name": "www.alt-firma.de", "value": "203.0.113.42" } + ] +} +``` + +### `GET /api/v1/domains/:id` + +Scope: `read:domains` + +### `DELETE /api/v1/domains/:id` + +Scope: `write:domains` + +### `GET /api/v1/domains/:id/stats?days=30` + +Scope: `read:analytics` + +```json +{ + "domain_id": 1, "days": 30, "total": 412, + "daily": [{"day":"2026-04-01","hits":12}, ...], + "by_country": [{"country":"DE","hits":380}, ...] +} +``` + +### `GET /api/v1/analytics/summary?days=30` + +Scope: `read:analytics` + +```json +{ + "days": 30, "total": 12480, + "daily": [...], + "top": [{"id":1,"domain":"alt-firma.de","hits":412}, ...], + "by_country": [...] +} +``` + +### `GET /api/v1/hits?domain_id=1&limit=100` + +Scope: `read:hits` + +```json +{ + "hits": [ + { + "id": 9001, "domain_id": 1, "ts": 1714499000000, + "ip_hash": "9f4a...", "country": "DE", + "user_agent": "Mozilla/5.0 ...", "referer": null, "path": "/" + } + ] +} +``` + +`ip_hash` = `sha256(ip + täglicher Salt)`. Kein Klartext-IP wird gespeichert. + +## Versionierung + +`/api/v1` ist stabil. Breaking-Changes bekommen `/api/v2`. Deprecation-Hinweise im `Sunset`-Header. + +## Rate-Limits + +Aktuell kein hartes Limit. Empfehlung: max. 60 req/min pro Token. + +## Beispiele + +**Uptime-Check eines Tokens:** + +```bash +curl -fsS -H "Authorization: Bearer $NRX" https://admin.firma.de/api/v1/health || echo "DOWN" +``` + +**Liste tote Domains (0 Hits / 90d):** + +```bash +curl -s -H "Authorization: Bearer $NRX" \ + "https://admin.firma.de/api/v1/analytics/summary?days=90" \ + | jq '.top | map(select(.hits == 0))' +``` + +**Domain anlegen + DNS-Records anzeigen:** + +```bash +curl -X POST -H "Authorization: Bearer $NRX" -H "Content-Type: application/json" \ + -d '{"domain":"shop.alt.de","target_url":"https://shop.firma.de"}' \ + https://admin.firma.de/api/v1/domains | jq '.dns_records' +``` + +→ Weiter mit [[Updates]] diff --git a/Analytics-&-Reports.md b/Analytics-&-Reports.md new file mode 100644 index 0000000..9ca60cf --- /dev/null +++ b/Analytics-&-Reports.md @@ -0,0 +1,73 @@ +# Analytics & Reports + +## Was wird getrackt? + +Pro echtem Hit (siehe [[Bot Filter]]): + +- Zeitstempel (ms) +- Domain-ID +- Land (ISO-2, falls GeoLite2 installiert) +- IP-Hash (sha256(ip + täglicher Salt) — DSGVO, kein Klartext) +- User-Agent (max 500 Zeichen) +- Referer (max 500 Zeichen) +- Pfad (max 500 Zeichen) + +## Dashboard `/dashboard` + +- Total / Aktiv / Wartend Domains +- Hits 24h + Eindeutige Besucher +- Top-10-Domains-Bar +- Daily-Hits-Line letzte 30 Tage + +## Analytics `/analytics` + +- Daily-Line letzte 30 Tage +- Top 10 Domains +- Top Länder (Pie) +- Tote Domains (aktiv aber 0 Hits / 90 Tage) + +## Domain-Detail + +`/domains/` zeigt: + +- Hits 24h / 30 Tage / Gesamt +- Eindeutige Besucher (30d, gesamt) +- Daily-Line der letzten 30 Tage **dieser Domain** +- DNS-Records-Übersicht (live, refreshbar) +- Sunset-Editor + +## PDF-Report-Export + +`/analytics` → "PDF Export" Button: + +- 3 Vorlagen: **Minimal** / **Basic** / **Detailliert** +- Titel + Zeitraum (1–365 Tage) frei wählbar +- Sektionen einzeln an/aus: Zusammenfassung, Daily-Chart, Top-Domains, Geo, Tote, Per-Domain-Detail, Letzte 200 Hits + +Server-side Generation via headless Chromium (puppeteer-core). Direkt-Download als PDF. A4 Hochformat, NexRedirect-Branding, sauberes Page-Break-Verhalten. + +Token-basierter Internal-Access (60s gültig, HMAC-signed) — Report-URL ist nicht öffentlich abrufbar. + +## CSV-Export + +- `/domains` → "CSV" lädt komplette Domain-Liste mit Hit-Counts +- `/analytics` → "Hits CSV" lädt letzte 30 Tage Hits (max 100k Zeilen) + +CSV ist UTF-8 mit Komma-Trenner, RFC 4180-konform (Quotes für Sonderzeichen). + +## Audit-Log `/audit` + +500 letzte administrative Aktionen mit Zeit, Benutzer-Email, Aktion, Ziel, Details: + +- domain.create / .update / .delete / .verify / .bulk_delete +- group.create / .update / .delete +- sunset.bulk +- (kann erweitert werden) + +## Eindeutige Besucher + +`COUNT(DISTINCT ip_hash)` über den Zeitraum. **Wichtig**: IP-Hash rotiert täglich (DSGVO-Anforderung). Gleicher User der heute und morgen kommt zählt als 2 verschiedene Besucher. + +Innerhalb eines Tages ist die Zählung präzise. Über mehrere Tage ist die Besucher-Zahl eine **Obergrenze** (überschätzt). + +→ Weiter mit [[Bot Filter]] diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..a716c19 --- /dev/null +++ b/Architecture.md @@ -0,0 +1,125 @@ +# Architecture + +## Komponenten + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Internet │ +└──────────────────┬───────────────────────────────────────────┘ + │ Port 80 / 443 + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Caddy (auto-HTTPS via Let's Encrypt) │ +│ │ +│ /etc/caddy/Caddyfile (auto-generated) │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ admin.example.de, server-ip → reverse_proxy :3000 │ │ +│ │ alt-firma.de, www.alt-firma.de → reverse_proxy │ │ +│ │ handle_errors { redir https://target.de/ 302 } │ │ +│ │ ... │ │ +│ └────────────────────────────────────────────────────┘ │ +└──────────────────┬───────────────────────────────────────────┘ + │ reverse_proxy localhost:3000 + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ server.ts (Custom Node Server, läuft via tsx) │ +│ │ +│ ① Host-Check │ +│ ├─ Admin-Host (server-IP / base_domain) → Next.js │ +│ ├─ Aktive Redirect-Domain → Hit loggen + 302 │ +│ │ (Sunset → HTML-Notice) │ +│ └─ Unbekannter Host → 404 "Domain not configured" │ +│ │ +│ ② Bei Admin-Host: │ +│ Next.js handleRequest() — App Router │ +│ ├─ middleware.ts (Edge): Auth-Check via JWT-Cookie │ +│ ├─ /(app)/* — eingeloggte Routes │ +│ ├─ /(auth)/login — NextAuth Credentials │ +│ ├─ /(setup)/setup — First-Run-Wizard │ +│ ├─ /api/v1/* — Public API (Bearer Token) │ +│ ├─ /api/* — Admin-API (Session-Cookie) │ +│ └─ /r/[token] — Internal Report-Page für PDF-Export │ +└──────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ better-sqlite3 │ +│ /var/lib/corex-nexredirect/nexredirect.db │ +│ Tables: users, domains, domain_groups, hits, settings, │ +│ api_tokens, update_log, audit_log │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Request-Flow für einen Domain-Hit + +1. `GET https://alt-firma.de/foo` → DNS löst auf Server-IP +2. Caddy nimmt an, ACME hat schon Cert für `alt-firma.de` +3. Caddy `reverse_proxy localhost:3000` +4. server.ts `req.headers.host = "alt-firma.de"` +5. `isAdminHost("alt-firma.de")` → false (kein Match auf server_ip / base_domain / localhost) +6. `resolveHost("alt-firma.de")` → Resolved-Eintrag aus In-Memory-Cache (5s TTL) +7. Bot-Filter (`shouldRecord`) prüft Method, UA, Pfad, Browser-Signals, IP-Scan-Detector +8. Wenn echter Hit: `recordHit()` → Buffer (5s batch) → SQLite INSERT +9. Sunset aktiv? → HTML-Notice + `Cache-Control: no-store` +10. Sonst: 302 zum Target mit `no-store` Headers + +## Request-Flow für Admin + +1. `GET https://admin.example.de/dashboard` → Caddy → server.ts → isAdminHost = true → Next.js +2. `middleware.ts` checkt JWT-Session-Cookie +3. `(app)/layout.tsx` checkt `isSetupComplete()` + `getServerSession()` (force-dynamic) +4. Render Sidebar + Page + +## Datenfluss bei Domain-Add + +``` +UI POST /api/domains + ├─ Zod-Validation + ├─ INSERT INTO domains (status='pending') + └─ Audit-Log + +UI POST /api/domains/[id]/verify + ├─ DNS-Lookup für domain + www-subdomain + ├─ Vergleich mit getSetting('server_ip') + ├─ Wenn Match: status='active', verified_at=now + ├─ invalidateRedirectCache() + ├─ Caddy: writeCaddyfile() + `caddy reload` + └─ Response { ok, caddy_reloaded } +``` + +## Update-Flow + +``` +UI Click "Update installieren" + └─ POST /api/update/apply (admin-only) + └─ exec(`sudo update.sh `) + ├─ git checkout tag + ├─ npm ci + ├─ Prebuilt .next aus GitHub-Release-Asset (oder npm run build) + ├─ ln -sf bin/nexredirect /usr/local/bin/ + └─ ( sleep 2 && systemctl restart ) & disown + (Detached — script exitet, API kann response zurückgeben) + └─ UI polled /api/v1/health → window.location.reload() +``` + +## In-Memory-State + +- **Redirect-Cache** (`lib/redirect-resolver.ts`): Map mit 5s TTL. Invalidiert bei Domain/Group-Mutationen. +- **Hits-Buffer** (`lib/hits.ts`): Array von Pending Hits, alle 5s als Transaction in DB geschrieben. +- **Geo-Reader** (`lib/geo.ts`): MaxMind-mmdb-Reader, lazy load on first lookup. +- **IP-Scan-Tracker** (`lib/hits.ts`): Map mit 30s Window. + +Alle in-memory — bei Restart weg, kein Problem (Cache füllt sich neu). + +## Sicherheit + +- **NextAuth JWT** mit `NEXTAUTH_SECRET` (zufällig generiert beim Install, 30d Session) +- **bcryptjs** für Passwort-Hash (cost 12) +- **API-Tokens** als sha256-Hash in DB; Klartext nur einmalig bei Anlage angezeigt +- **HMAC-signierte Tokens** für PDF-Internal-Access (60s TTL) +- **DSGVO**: IP wird nie im Klartext gespeichert. `sha256(ip + daily_salt)`. Daily-Salt rotiert täglich → keine Re-Identifizierung über Tagesgrenzen. +- **CSRF**: NextAuth handhabt das für Auth-Routes. Admin-Mutations brauchen Session-Cookie. API-Routes brauchen Bearer-Token. +- **Bot-Filter** auch als implizite Schutzschicht gegen Scanner-Probing +- **No exposed admin paths**: alle Mutations brauchen Auth. `/api/v1/*` Bearer. + +→ Weiter mit [[Troubleshooting]] diff --git a/Bot-Filter.md b/Bot-Filter.md new file mode 100644 index 0000000..545b8d9 --- /dev/null +++ b/Bot-Filter.md @@ -0,0 +1,85 @@ +# Bot Filter + +NexRedirect zählt nur "echte" Besucher — Scanner, Crawler und Monitoring-Tools werden serverseitig herausgefiltert. Kombination mehrerer Heuristiken in [`lib/hits.ts`](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/blob/main/lib/hits.ts): + +## 1. HTTP-Method + +`HEAD` und `OPTIONS` werden nie gezählt (Pre-Flight, Health-Checks). + +## 2. Pfad-Pattern + +Bekannte Scanner-Pfade werden ignoriert. Auszug: + +``` +/.env* /.git* /.DS_Store /.vscode /.svn /.hg /.idea +/wp-(admin|login|content|includes|json) /xmlrpc.php /wordpress/ +/admin/ /administrator/ /phpmyadmin /pma/ /myadmin +/server-status /server-info /info.php /test.php +/login.action /console/ /manager/ /jenkins /jolokia +/actuator /telescope /horizon /debug /trace.axd /elmah.axd +/cgi-bin /about +/swagger* /api-docs* /api/swagger /v[23]/api-docs /v2/_catalog +/webjars/ /graphql* /api/gql /api/?$ +/_next/ /@vite /__webpack +/ecp/ /owa/ /___proxy_subdomain +/(js|assets/js|static)/ /(css|assets/css)/ +/bot-connect.* /config.json /composer.* /package.* +\.\./ %2e%2e /s/[0-9a-f]{20,} +rest_route= +/favicon.* /apple-touch-icon /robots.txt /sitemap* /ads.txt +/.well-known/ /browserconfig.xml +``` + +Komplett-Liste in `lib/hits.ts`. + +## 3. User-Agent-Pattern + +Bot-Keywords werden gematched: + +``` +bot, crawl, spider, slurp, curl, wget, httpclient, python-requests, +axios, node-fetch, monitor, uptime, pingdom, datadog, prometheus, +scanner, fetch, preview, whatsapp, telegrambot, facebookexternalhit, +linkedinbot, twitterbot, discordbot, skypeuripreview, mastodon, +matrix-bot, preconnect, dnsperf, sentry, newrelic, gtmetrix, +lighthouse, headlesschrome, phantomjs, puppeteer, playwright, +go-http-client, java/, okhttp, libwww, mechanize, nikto, sqlmap, +nmap, masscan, zgrab, nuclei, acunetix, netcraft, expanse, censys, +shodan, fuzz, burp, arachni, w3af, wpscan, gobuster, ffuf, dirb, +dirbuster +``` + +Plus: +- UA muss `Mozilla/` Prefix haben (echte Browser; Bots ohne Mozilla-Faked werden auch via UA-Keywords gefangen) +- UA muss mindestens 15 Zeichen lang sein + +## 4. Browser-Signal-Heuristik + +Echte Browser senden bei jeder Top-Level-Navigation: + +- **`Sec-Fetch-Mode`** Header (Chrome/FF/Safari/Edge seit ~2020) +- **`Accept-Language`** Header (jeder Browser hat eine Spracheinstellung) +- **`Accept`** mit `text/html` (Browser fragen explizit nach HTML; Scanner senden `*/*`) + +**Mindestens 2 von 3** müssen gesetzt sein. Plus: `Sec-Fetch-Dest` muss `document` / `empty` / `iframe` sein (filtert Image- / Script-Probes). + +Das ist der wichtigste Filter — Scanner mit gefälschtem `Mozilla/5.0`-UA fallen hier durch. + +## 5. Per-IP Scan-Detektor + +In-Memory: gleiche IP-Hash, ≥5 verschiedene Pfade in 30s = Scanner. Alle weiteren Hits dieser IP im Zeitfenster werden verworfen. + +## Ergebnis + +Bei einem typischen Vanity-Redirect-Server: + +- Vorher: ~80% der Hits sind Bots (vor allem nach DNS-Aktivierung kommen Scanner-Wellen) +- Nachher: <5% (vereinzelte Lighthouse / Pingdom etc.) + +## False-Positives anzeigen + +Bot-Hits werden gar nicht gespeichert — wenn du sie sehen willst, müsstest du temporär die Filter ausschalten. Es gibt keinen "ignored hits"-Counter. + +Wenn ein Browser fälschlicherweise ausgefiltert wird (uralter Browser ohne Sec-Fetch?): bitte im GitHub-Issue melden mit User-Agent + Headers. + +→ Weiter mit [[CLI]] diff --git a/CLI.md b/CLI.md new file mode 100644 index 0000000..fc5c0d2 --- /dev/null +++ b/CLI.md @@ -0,0 +1,69 @@ +# CLI + +Auf dem Server: `nexredirect ` (Symlink in `/usr/local/bin/`). + +## Subcommands + +``` +status Service-Status (systemctl status) +start Service starten +stop Service stoppen +restart Service neu starten +logs [-n N] Logs streamen (default: -f live tail) +update [tag] Auf neueste Version (oder bestimmten Tag); skip wenn schon aktuell +update -f [tag] Update erzwingen auch wenn Version gleich +version Aktuelle + neueste Version (von GitHub) +caddy reload Caddyfile reload via Admin-API +caddy regen Caddyfile aus DB neu generieren + reloaden +caddy fix-perms /etc/caddy/Caddyfile dem Service-User übertragen +caddy show Aktuellen Caddyfile anzeigen +db SQLite-Shell auf der Datenbank öffnen +domains Aktive Domains listen +hits [N] Letzte N Hits (default 20) +tokens API-Tokens auflisten +backup [PATH] DB + Caddyfile sichern (default: /tmp/...) +uninstall Service + Files entfernen (DB bleibt) +help Hilfe +``` + +## Beispiele + +```bash +# Status checken +nexredirect status + +# Live-Logs +nexredirect logs + +# Letzte 50 Hits +sudo nexredirect hits 50 + +# Update auf neueste Version +sudo nexredirect update + +# Update auf bestimmten Tag (auch downgrade) +sudo nexredirect update v0.1.10 + +# Force-Update (gleiche Version neu installieren) +sudo nexredirect update -f + +# DB anschauen +sudo nexredirect db +sqlite> SELECT id, domain, status FROM domains; +sqlite> .quit + +# Caddyfile anzeigen +nexredirect caddy show + +# Backup +sudo nexredirect backup +sudo nexredirect backup /backup/nexredirect-$(date +%F).tgz +``` + +## Service-User-Konzept + +Service läuft als unprivileged user `nexredirect` (UID via `useradd --system`). Die meisten Commands brauchen `sudo` weil sie systemctl / DB-Schreibzugriff benötigen. + +`sudo update.sh` ist über `/etc/sudoers.d/corex-nexredirect` als einzige Privileg-Eskalation erlaubt — vom Service-Process selbst aufrufbar wenn das UI "Update installieren" triggert. + +→ Weiter mit [[API]] diff --git a/DNS-Setup.md b/DNS-Setup.md new file mode 100644 index 0000000..07fbf32 --- /dev/null +++ b/DNS-Setup.md @@ -0,0 +1,43 @@ +# DNS Setup + +Damit eine Domain auf NexRedirect zeigt, müssen DNS-Records auf die Server-IP zeigen. + +## Records + +Beim Hinzufügen einer Domain im UI bekommst du genau angezeigt was einzutragen ist. Standard: + +| Type | Name | Value | +|---|---|---| +| A | @ | `` | +| AAAA | @ | `` _(optional)_ | +| A | www | `` | + +`@` steht für die Root-Domain. Manche DNS-Provider erwarten stattdessen den vollen Domain-Namen oder ein leeres Feld — egal, alle drei Schreibweisen meinen dasselbe. + +## Validierung + +Nach dem Eintragen klick im UI auf **Prüfen**. Server fragt selbst per `dns.resolve()` ab und vergleicht mit der eigenen IP. Bei Match → Domain wird auf `active` gestellt → Caddy reload → Auto-HTTPS-Cert via Let's Encrypt. + +DNS-Propagation kann ein paar Minuten dauern. Nochmal prüfen → meistens da. + +## DNS-Records-Übersicht + +In der Domain-Detail-Page gibt's eine Card "DNS-Records" — listet **alle** Records die für diese Domain veröffentlicht sind: A, AAAA, CNAME, MX, NS, TXT, SOA, CAA. A/AAAA-Werte die auf den Server zeigen werden grün markiert. + +Refresh-Button löst neue Abfrage aus. + +## Auto-HTTPS + +Sobald eine Domain `active` ist, holt Caddy automatisch ein Let's-Encrypt-Cert. Erste HTTPS-Anfrage kann ~10s dauern, danach gecacht. + +Voraussetzung: Server muss aus dem Internet auf Port 80 erreichbar sein (für ACME-Challenge). + +## Häufige Probleme + +**DNS validiert nicht**: A-Record zeigt auf falsche IP, oder TTL noch nicht abgelaufen. `dig +short example.de` und `dig +short example.de @8.8.8.8` vergleichen. + +**HTTPS funktioniert nicht trotz active**: Caddy braucht eingehend Port 80 frei. Firewall prüfen. Logs: `nexredirect logs | grep -i caddy` oder `journalctl -u caddy -n 50`. + +**Cert-Renewal-Errors**: Let's Encrypt Rate-Limit (50/Woche pro Domain). Bei vielen Subdomains: Wildcard-Cert nötig — wird aktuell nicht unterstützt, jede Subdomain braucht eigenen Cert. + +→ Weiter mit [[Domain Management]] diff --git a/Domain-Management.md b/Domain-Management.md new file mode 100644 index 0000000..6881fee --- /dev/null +++ b/Domain-Management.md @@ -0,0 +1,52 @@ +# Domain Management + +## Domain hinzufügen + +`/domains/new` öffnen. 3 Schritte: + +1. **Domain & Ziel**: Domain-Name + Ziel-URL (oder Gruppe), Status-Code (302 empfohlen), Pfad-Preserve, www-Subdomain +2. **DNS-Records anzeigen**: A-Records eintragen wie angezeigt +3. **Validieren**: Klick → Server prüft → bei Erfolg ist die Domain sofort aktiv + +## Status-Code 301 vs 302 + +**302 (Default)**: Browser cached den Redirect nicht. Jeder Aufruf hittet den Server, Analytics zählt sauber. Dafür minimaler SEO-Verlust. + +**301**: Browser cached permanent. Folge-Aufrufe gehen direkt zum Ziel ohne deinen Server zu sehen — Hit-Counter bleibt bei 1, egal wie oft. NexRedirect setzt zwar `Cache-Control: no-store`, viele Browser ignorieren das aber bei 301. + +**Empfehlung**: 302 außer bei expliziter SEO-Strategie. + +## Pfad-Preserve + +`alt-domain.de/foo/bar` → wenn aktiv: `https://ziel.de/foo/bar`. Wenn aus: alle Pfade landen auf `https://ziel.de/`. + +Bei Domains die nur als Vanity-Redirect dienen → Aus. Bei echten Migrationen → An. + +## www-Subdomain + +Aktiviert hinzufügen: `www.example.de` zeigt aufs gleiche Ziel. Caddy holt dafür ein zweites Cert. + +## Gruppen + +Mehrere Domains zum gleichen Ziel = Gruppe. + +`/groups` → "Neue Gruppe" → Name + Ziel-URL. Beim Domain-Anlegen unter "Ziel" → "Gruppe" wählen statt Einzel-URL. Vorteil: Ziel-URL einmal ändern → alle Domains in der Gruppe folgen. + +Edit-Funktion über Bleistift-Icon. Löschen blockiert solange Domains die Gruppe nutzen. + +## Bulk-Aktionen + +Auf `/domains` Checkbox-Spalte links. Auswahl löst Selection-Bar oben aus: + +- **Löschen** mehrere Domains (Bestätigung zeigt Hit-Count) +- **Sunset-Hinweis** für N Domains gleichzeitig konfigurieren (siehe [[Sunset Pages]]) + +## Domain bearbeiten + +`/domains/` → "Konfiguration"-Card hat ein inline Edit-Form: Ziel, Code, Gruppe, Pfad-Preserve, www. Speichern triggert automatisch Caddy-Reload. + +## Domain löschen + +Detail-Page → "Aktionen" → "Domain löschen". Confirm-Dialog warnt wie viele Hits mitgelöscht werden. Auch via [[CLI]]: `nexredirect db` und SQL. + +→ Weiter mit [[Sunset Pages]] diff --git a/Home.md b/Home.md new file mode 100644 index 0000000..6d1f3e4 --- /dev/null +++ b/Home.md @@ -0,0 +1,43 @@ +# CoreX NexRedirect + +Self-hosted Domain-Redirect-Server mit Web-Admin-UI, Per-Domain-Analytics und One-Line Install. + +## Was macht NexRedirect? + +Du hast viele Domains, die nur auf eine andere Webseite weiterleiten sollen — und willst wissen, welche davon überhaupt noch genutzt werden? NexRedirect: + +- nimmt alle deine Redirect-Domains entgegen (DNS auf den Server) +- leitet jeden Aufruf konfigurierbar zum Ziel weiter (302/301) +- protokolliert jeden echten Besuch (Bots gefiltert) mit Land, User-Agent, Referer +- zeigt dir im Web-UI welche Domains tatsächlich genutzt werden — und welche tot sind + +## Schnellstart + +```bash +curl -sSL https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/main/scripts/install.sh | sudo bash +``` + +Setup-Wizard danach: `http:///setup` + +→ Detail: [[Installation]] + +## Themen + +- [[Installation]] — Server aufsetzen +- [[DNS Setup]] — Domains korrekt verbinden +- [[Domain Management]] — Domains, Gruppen, Sunset-Pages +- [[Analytics & Reports]] — Hit-Tracking, PDF-Export, CSV-Export +- [[Bot Filter]] — Wie echte Besucher von Scannern unterschieden werden +- [[CLI]] — Server-CLI `nexredirect` +- [[API]] — REST-API mit Token-Auth +- [[Updates]] — Self-Update und Versions-Strategie +- [[Architecture]] — Wie alles zusammenhängt +- [[Troubleshooting]] — Häufige Probleme und Lösungen + +## Stack + +Next.js 15 + TypeScript + TailwindCSS + better-sqlite3 (eine Datei) + Caddy (Auto-HTTPS) + MaxMind GeoLite2 + Recharts. Läuft auf Debian/Ubuntu via systemd. Eine `.db`, eine `Caddyfile`, ein Node-Prozess. + +## Lizenz + +[MIT](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/blob/main/LICENSE) — viel Spaß damit. diff --git a/Installation.md b/Installation.md new file mode 100644 index 0000000..0c50fc1 --- /dev/null +++ b/Installation.md @@ -0,0 +1,95 @@ +# Installation + +## Voraussetzungen + +- Debian 11+ oder Ubuntu 20.04+ Server +- Root-Zugriff (sudo) +- Server-IP öffentlich erreichbar (Port 80 + 443 offen) +- Mindestens 1 GB RAM, 5 GB Disk + +## One-Line Install + +```bash +curl -sSL https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/main/scripts/install.sh | sudo bash +``` + +Das Script: + +1. Detect Debian/Ubuntu +2. `apt install`: curl, ca-certificates, gnupg, git, sqlite3, chromium, sudo +3. Caddy via offiziellem Repo +4. Node.js 20 via NodeSource +5. Holt neuestes Release (Tag), zieht Prebuilt `.next`-Tarball aus GitHub-Release +6. Legt Service-User `nexredirect` an, klont Repo nach `/opt/corex-nexredirect` +7. systemd Unit + sudoers für `update.sh` +8. Caddy Bootstrap-Config + reload +9. CLI-Symlink `/usr/local/bin/nexredirect` +10. Service start + +Nach `Fertig!`-Meldung: Setup-Wizard unter `http:///setup` aufrufen, Admin-Account anlegen. + +## Optional: GeoIP + +Land pro Hit nur wenn MaxMind-DB installiert. Entweder beim Install: + +```bash +sudo MAXMIND_LICENSE_KEY=xxx ... | sudo -E bash +``` + +…oder später im UI unter Einstellungen → GeoIP-Tracking → Account-ID + License-Key eintragen → Installieren. + +[Lizenz-Key kostenlos hier](https://www.maxmind.com/en/geolite2/signup) generieren. + +## Manueller Install + +Wer das Curl-Pipe-Bash nicht mag: + +```bash +sudo apt install -y caddy nodejs git sqlite3 chromium +sudo useradd --system --home /opt/corex-nexredirect --shell /usr/sbin/nologin nexredirect +sudo mkdir -p /opt/corex-nexredirect /var/lib/corex-nexredirect +sudo git clone https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect /opt/corex-nexredirect +sudo chown -R nexredirect:nexredirect /opt/corex-nexredirect /var/lib/corex-nexredirect +sudo -u nexredirect bash -c "cd /opt/corex-nexredirect && npm ci && npm run build" +sudo cp /opt/corex-nexredirect/systemd/corex-nexredirect.service /etc/systemd/system/ +sudo chown nexredirect:nexredirect /etc/caddy/Caddyfile +sudo systemctl daemon-reload +sudo systemctl enable --now caddy corex-nexredirect +``` + +## Verzeichnisstruktur + +| Pfad | Zweck | +|---|---| +| `/opt/corex-nexredirect` | Code (git checkout) | +| `/var/lib/corex-nexredirect/nexredirect.db` | SQLite (alle Daten) | +| `/var/lib/corex-nexredirect/GeoLite2-Country.mmdb` | Geo-DB (optional) | +| `/etc/caddy/Caddyfile` | Caddy-Config (auto-generated) | +| `/etc/systemd/system/corex-nexredirect.service` | systemd-Unit | +| `/etc/sudoers.d/corex-nexredirect` | Sudo-Privileg für `update.sh` | +| `/usr/local/bin/nexredirect` | CLI-Symlink | + +## Backup + +```bash +nexredirect backup [/path/zu.tar.gz] +``` + +Sichert `nexredirect.db` (+ WAL/SHM) und `Caddyfile`. Restore: + +```bash +sudo systemctl stop corex-nexredirect caddy +sudo tar -xzf nexredirect-backup-XXX.tar.gz -C / +sudo chown nexredirect:nexredirect /var/lib/corex-nexredirect/*.db +sudo systemctl start caddy corex-nexredirect +``` + +## Deinstallation + +```bash +sudo nexredirect uninstall +``` + +Entfernt Service, Files, Sudoers, CLI. **DB unter `/var/lib/corex-nexredirect/` bleibt erhalten** — separat löschen wenn gewollt. + +→ Weiter mit [[DNS Setup]] diff --git a/Sunset-Pages.md b/Sunset-Pages.md new file mode 100644 index 0000000..dbfd82b --- /dev/null +++ b/Sunset-Pages.md @@ -0,0 +1,48 @@ +# Sunset Pages + +Statt sofortigem Redirect kann eine **Hinweisseite** kommen — schlichte weiße Seite mit Custom-Text und einem "Weiter"-Button. Klickt der User → echter Redirect. + +## Wann brauchst du das? + +- **Domain wird abgeschaltet**: "Diese Domain wird zum 31.12.2026 abgeschaltet. Bitte verwende ab sofort https://neue-domain.de" +- **Marken-Migration**: Hinweis dass die Firma umfirmiert hat +- **Information** über kommenden Besitzerwechsel + +## Konfiguration per Domain + +Domain-Detail-Page → Card "Abschaltungs-Hinweis" → Toggle aktivieren: + +| Feld | Beispiel | +|---|---| +| Titel | Diese Domain wird abgeschaltet | +| Nachricht | Die Domain alt-firma.de wird zum 31.12.2026 abgeschaltet. Bitte verwenden Sie ab sofort https://neue-firma.de | +| Button-Text | Weiter | +| Abschaltdatum | 31.12.2026 _(optional, nur Anzeige)_ | + +Speichern. Beim nächsten Aufruf der Domain bekommt der User die Notice statt Redirect. Klick auf "Weiter" → `?nr_continue=1` → echter Redirect. + +## Bulk-Konfiguration + +Auf `/domains` mehrere Domains markieren → Selection-Bar → "Sunset-Hinweis" → Form ausfüllen → "Auf N anwenden". Setzt für alle markierten Domains die gleiche Konfiguration. + +Zum Deaktivieren für mehrere: Checkbox "Aktivieren" im Bulk-Dialog ausschalten und anwenden. + +## Implementierung + +Custom Server (`server.ts`) prüft beim Resolve eines Hosts: + +1. Wenn Domain Sunset hat UND Request **kein** `?nr_continue=1` → HTML-Notice mit `Cache-Control: no-store` +2. Wenn `?nr_continue=1` → ganz normaler 302/301 zum Target + +Hit wird beim ERSTEN Request gezählt (Notice-Render). Continue-Click zählt NICHT als zweiter Hit (würde sonst doppeln). + +HTML-Page ist in [`lib/sunset-html.ts`](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/blob/main/lib/sunset-html.ts) — schwarz auf weiß, kein JS, kein externes Asset, kein Tracking. Funktioniert auch ohne JS-Browser. + +## Wichtige Hinweise + +- Sunset-Page wird **nicht** vom Browser gecacht (`Cache-Control: no-store`) +- Sunset-Page ist `noindex,nofollow` für Suchmaschinen +- Robots.txt etc. werden NICHT durch die Notice geleitet, gehen direkt zum Target +- Bei aktivem Sunset wird der Caddy-Fallback (bei App-Down: Direkt-Redirect via `handle_errors`) trotzdem ausgelöst — Notice nur wenn App läuft + +→ Weiter mit [[Analytics & Reports]] diff --git a/Troubleshooting.md b/Troubleshooting.md new file mode 100644 index 0000000..f7e2b23 --- /dev/null +++ b/Troubleshooting.md @@ -0,0 +1,136 @@ +# Troubleshooting + +## Service läuft nicht + +```bash +nexredirect status +nexredirect logs -n 100 +``` + +Häufigste Ursachen: + +- **Port 3000 belegt**: anderer Service auf 3000? `ss -tlnp | grep 3000` +- **better-sqlite3 native module fehlt**: nach Update `sudo nexredirect update -f` zwingt npm ci +- **DB-Dateirechte**: `ls -la /var/lib/corex-nexredirect/`. Sollte `nexredirect:nexredirect` gehören +- **NEXTAUTH_SECRET nicht gesetzt**: `systemctl cat corex-nexredirect.service` prüfen + +## /domains gibt "Application error" + +Spalte fehlt (bei Update von alter Version). Logs prüfen: + +```bash +nexredirect logs -n 50 | grep SqliteError +``` + +Falls `no such column`: Migration nicht durchgelaufen. v0.1.15+ hat Self-Healing (`PRAGMA table_info` Check). Update auf neueste Version + Restart: + +```bash +sudo nexredirect update +``` + +## Domain-Add: DNS-Verify schlägt fehl + +```bash +dig +short example.de @8.8.8.8 +dig +short example.de @1.1.1.1 +``` + +Output muss die Server-IP sein. Falls nicht: + +- DNS noch nicht propagiert (TTL warten) +- A-Record falsch eingetragen (manche Provider erwarten leeres Name-Feld statt `@`) +- DNSSEC-Problem beim Registrar + +Server-IP korrekt? `nexredirect db` und `SELECT * FROM settings WHERE key LIKE 'server_%';` + +## HTTPS funktioniert nicht trotz active + +Caddy braucht eingehend Port 80 für ACME-Challenge: + +```bash +sudo nexredirect logs caddy 2>&1 | grep -i acme +journalctl -u caddy -n 50 | grep -i error +``` + +Häufigste Ursachen: + +- Firewall blockt Port 80 (UFW / iptables) +- Cloudflare-Proxy davor → Caddy bekommt CF-IP, ACME schlägt fehl. Cloudflare auf "DNS-only" stellen +- Let's Encrypt Rate-Limit (50 Certs/Woche pro Domain) + +## Caddyfile kann nicht geschrieben werden + +Bei manueller Migration vor v0.1.9: + +```bash +sudo nexredirect caddy fix-perms +sudo nexredirect caddy regen +``` + +Setzt Owner auf Service-User und regeneriert + reloaded. + +## Update läuft, UI zeigt aber alte Version + +Browser-Cache. Hard-Reload (Ctrl+Shift+R) oder Inkognito. v0.1.5+ hat Auto-Reload nach Update integriert. + +## Hit-Counter steigt nicht + +Wenn Domain `redirect_code = 301`: +Browser cached den Redirect, alle weiteren Aufrufe gehen direkt zum Ziel. NexRedirect setzt `Cache-Control: no-store`, viele Browser ignorieren das aber bei 301. + +**Fix**: Auf 302 ändern. v0.1.7+ migriert beim ersten Boot automatisch. + +## Hits werden gefiltert obwohl echte User + +Modernere Filter (v0.1.19+) prüft Browser-Signal-Header. Sehr alte Browser ohne `Sec-Fetch-Mode` UND ohne `Accept-Language` werden gefiltert. + +Im Hit-Log nachschauen: `nexredirect db` und `SELECT * FROM hits ORDER BY ts DESC LIMIT 50;`. Wenn der Browser geloggt wird, ist alles OK. Sonst Bot-Filter zu aggressiv. + +## Update.sh: "dubious ownership" + +Wenn `update.sh` als root aufgerufen wird aber Repo `nexredirect` gehört. Fix: + +```bash +sudo nexredirect update +``` + +(CLI ruft update.sh über sudo auf und führt git als Service-User aus.) Bei Manuell: + +```bash +sudo -u nexredirect git -C /opt/corex-nexredirect pull --ff-only +sudo /opt/corex-nexredirect/scripts/update.sh +``` + +## PDF-Download: "chrome_not_found" + +Chromium fehlt. Beim Update wird's installiert; ältere Installs: + +```bash +sudo apt install -y chromium +``` + +Pfad in der App ist `/usr/bin/chromium` oder `/usr/bin/chromium-browser`. Override via `NEXREDIRECT_CHROME_PATH` in der systemd-Unit-File. + +## Logs zu Caddy-Reload-Fehlern + +```bash +sudo nexredirect caddy show > /tmp/cur.caddyfile +caddy validate --config /tmp/cur.caddyfile +``` + +Validate-Output zeigt Syntax-Fehler. Bei Bedarf manuell editieren / Settings im UI korrigieren → `sudo nexredirect caddy regen`. + +## Backup wiederherstellen + +```bash +sudo systemctl stop corex-nexredirect caddy +sudo tar -xzf nexredirect-backup-XXX.tar.gz -C / +sudo chown nexredirect:nexredirect /var/lib/corex-nexredirect/*.db* +sudo systemctl start caddy corex-nexredirect +``` + +## Fragen / Bugs + +GitHub Issues: https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/issues + +→ Zurück zu [[Home]] diff --git a/Updates.md b/Updates.md new file mode 100644 index 0000000..87e03e2 --- /dev/null +++ b/Updates.md @@ -0,0 +1,78 @@ +# Updates + +NexRedirect prüft alle 60 Minuten gegen die GitHub-Releases-API auf neue Versionen. **Keine Auto-Updates** außer aktiviert. + +## Update-Verhalten + +| Setting | Verhalten | +|---|---| +| Default | Stündlicher Check, Banner in der UI bei neuem Release. Nichts wird ohne Klick installiert. | +| `update_auto = true` | Bei jedem Check wird verfügbares Update sofort installiert. | +| `update_include_prereleases = true` | Auch Pre-Releases als Update angezeigt. | + +## Manuell aktualisieren + +**UI:** + +1. Banner oben oder Settings → "Update v0.1.X installieren" klicken +2. Bestätigen +3. UI zeigt Spinner, polled `/api/v1/health`, lädt Seite neu sobald Server zurück + +**CLI:** + +```bash +sudo nexredirect update # auf neueste, skip wenn schon aktuell +sudo nexredirect update v0.1.10 # auf bestimmten Tag (auch downgrade) +sudo nexredirect update -f # erzwingen auch wenn gleiche Version +``` + +## Update-Mechanik + +1. `update.sh` prüft latest Release-Tag (oder nimmt übergebenen Tag) +2. Skip wenn current === latest und kein `-f` +3. `git fetch --tags && git checkout ` als Service-User +4. `npm ci` +5. **Prebuilt `.next`-Tarball** aus Release-Asset ziehen — spart ~25s gegenüber lokal bauen +6. Falls kein Asset (oder Download fehlschlägt): `npm run build` als Fallback +7. CLI-Symlink + Permissions aktualisieren +8. **Detached restart** in 2s (Hauptscript exitet zuerst sauber → API kann response zurückgeben → DANN restart) + +## Auto-Update aktivieren + +Settings → Toggle "Auto-Update aktivieren". Ab dann wird bei jedem stündlichen Check ein verfügbares Update direkt installiert. + +**Empfehlung**: Nur in Test-Umgebungen. In Prod manuell prüfen, Release-Notes lesen. + +## Rollback + +```bash +sudo nexredirect update v0.1.10 # auf Vorgänger-Version +``` + +`update.sh` läuft `git checkout ` egal ob vorwärts oder rückwärts. Schema-Migrationen sind additiv (`ALTER TABLE ADD COLUMN`) — Downgrade fragmenten ungenutzte Spalten ignorieren. + +Falls Schema-Inkompatibilität: vorher Backup, ggf. DB händisch downgraden. + +## Schema-Migrationen + +Beim Start: + +1. `ensureSchema(db)` legt fehlende Tabellen idempotent an (`CREATE TABLE IF NOT EXISTS`) +2. `runMigrations(db)` läuft definierte Schritte (Settings-Flag-basiert + Schema-Check) + +Aktuelle Migrationen: + +- `m_301_to_302`: alle existierenden 301-Codes auf 302 ändern (Browser-Cache-Fix) +- `sunset_config`-Spalte: Self-healing — prüft via `PRAGMA table_info` ob Spalte existiert, fügt hinzu wenn nicht + +## Update-Log + +Jeder Update-Versuch wird in der `update_log`-Tabelle protokolliert: + +```sql +SELECT ts, from_version, to_version, status FROM update_log ORDER BY ts DESC LIMIT 10; +``` + +Status: `success` oder `failed` (Log-Auszug in der `log`-Spalte). + +→ Weiter mit [[Architecture]] diff --git a/_Sidebar.md b/_Sidebar.md new file mode 100644 index 0000000..359d011 --- /dev/null +++ b/_Sidebar.md @@ -0,0 +1,20 @@ +**CoreX NexRedirect** + +- [[Home]] +- [[Installation]] +- [[DNS Setup]] +- [[Domain Management]] +- [[Sunset Pages]] +- [[Analytics & Reports]] +- [[Bot Filter]] +- [[CLI]] +- [[API]] +- [[Updates]] +- [[Architecture]] +- [[Troubleshooting]] + +--- + +[Repo](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect) +[Releases](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/releases) +[Issues](https://forgejo.mgmt.corexmanagement.de/admin_hg/cx-nexredirect/issues)