diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..379c22d Binary files /dev/null and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore index 145fa92..5b02fd0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,11 +5,3 @@ node_modules npm-debug.log* .git README.md -data/*.json -storage/uploads/images/* -storage/config/* -!storage/config/.gitkeep -!storage/uploads/images/.gitkeep -*.tmp -*.temp -.DS_Store diff --git a/.env.example b/.env.example index 9046cd5..5a68e82 100644 --- a/.env.example +++ b/.env.example @@ -1,37 +1,5 @@ -# Public base URL used for canonical URLs, sitemap, robots.txt and absolute metadata. -# Local example: http://localhost:3010 -# Production example: https://funktechnik-schubert.de -NEXT_PUBLIC_SITE_URL= +NEXT_PUBLIC_SITE_URL=http://localhost:3010 -# Admin login email address. Required for /admin. -ADMIN_EMAIL= - -# Admin login password. Required for /admin. -# Use a long unique password and never commit a real value. -ADMIN_PASSWORD= - -# Secret for signing the HttpOnly admin session cookie. Required. -# Use a long random value, for example generated by: openssl rand -base64 48 -ADMIN_SESSION_SECRET= - -# Set to true in production behind HTTPS so admin cookies are Secure. -# Use false only for local HTTP development. -AUTH_COOKIE_SECURE=false - -# Optional future server-side Olympus intake endpoint. -# Leave empty until the Olympus integration is explicitly implemented. +# Optional spaetere Integration. Nicht im Browser verwenden. OLYMPUS_INTAKE_API_URL= - -# Optional future server-side Olympus intake token. -# Leave empty until the Olympus integration is explicitly implemented. OLYMPUS_INTAKE_API_TOKEN= - -# Server-side Olympus base URL for public repair status links. -# Docker example when Hermes is reachable in the same network: http://hermes:8000 -# Production example: https://olympus.example.internal -# This URL must be reachable by the Next.js server, not by the browser. -OLYMPUS_PUBLIC_STATUS_API_URL= - -# Optional server-side token for future protected status API access. -# Leave empty for Olympus v0.8.1 unless the endpoint is protected later. -OLYMPUS_PUBLIC_STATUS_API_TOKEN= diff --git a/.gitignore b/.gitignore index 7fccc25..ef9c7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,21 +5,3 @@ dist .env .env.local npm-debug.log* -data/contact-inquiries.json -data/repair-inquiries.json -data/site-settings.json -data/smtp-settings.json -storage/uploads/images/* -storage/config/* -storage/content/* -storage/content/backups/* -!storage/.gitkeep -!storage/config/.gitkeep -!storage/content/.gitkeep -!storage/content/backups/ -!storage/content/backups/.gitkeep -!storage/uploads/.gitkeep -!storage/uploads/images/.gitkeep -.DS_Store -*.tmp -*.temp diff --git a/Dockerfile b/Dockerfile index 2dffadb..5c607bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,22 +14,16 @@ FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -ENV NEXT_PUBLIC_APP_VERSION=0.4.1 -ENV DOCKER_ENV=true -ENV PORT=3010 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder --chown=nextjs:nodejs /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder --chown=nextjs:nodejs /app/scripts/runtime-start.js ./runtime-start.js - -RUN mkdir -p /app/data /app/storage/config /app/storage/content/backups /app/storage/uploads/images /app/.next/cache \ - && chown -R nextjs:nodejs /app/data /app/storage /app/.next/cache +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static USER nextjs EXPOSE 3010 +ENV PORT=3010 -CMD ["node", "runtime-start.js"] +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 8ccb7db..e6cb911 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,10 @@ Eigenständige öffentliche Firmenwebsite für Funktechnik Schubert. Dieses Proj - Next.js 16 App Router - TypeScript strict - Serverseitige API-Routen für Kontakt und Reparaturannahme -- Geschützter Admin-Bereich unter `/admin` -- Lokale Storage-Datenablage für Anfragen, Einstellungen, Medien und Website-Inhalte - Docker - Nginx/Reverse-Proxy-fähig - SEO Metadata, Sitemap und robots.txt -Version: `0.4.1` - ## Seiten - `/` Startseite @@ -23,40 +19,20 @@ Version: `0.4.1` - `/reparatur` - `/ueber-uns` - `/kontakt` -- `/status/[token]` - `/impressum` - `/datenschutz` -- `/admin/login` -- `/admin` -- `/admin/kontaktanfragen` -- `/admin/reparaturanfragen` -- `/admin/website` -- `/admin/medien` -- `/admin/einstellungen` -- `/admin/smtp` -- `/admin/seo` -- `/admin/system` ## Lokale Entwicklung ```bash npm install -cp .env.example .env npm run dev ``` Die Website läuft lokal unter: ```text -http://127.0.0.1:3010 -``` - -Für den Admin-Bereich müssen in `.env` mindestens gesetzt sein: - -```text -ADMIN_EMAIL -ADMIN_PASSWORD -ADMIN_SESSION_SECRET +http://localhost:3010 ``` ## Checks @@ -85,14 +61,6 @@ Port: 3010:3010 ``` -Die Runtime-Daten werden über Docker-Volumes gespeichert: - -- `funktechnik-data` → `/app/data` -- `funktechnik-storage` → `/app/storage` -- `funktechnik-next-cache` → `/app/.next/cache` - -Dadurch sind keine manuellen `chmod`- oder `chown`-Befehle notwendig. - ## Umgebung `.env.example` kopieren: @@ -103,277 +71,18 @@ cp .env.example .env Variablen: -- `NEXT_PUBLIC_SITE_URL`: öffentliche Basis-URL für SEO, Sitemap und Metadaten -- `ADMIN_EMAIL`: Admin-Login E-Mail -- `ADMIN_PASSWORD`: Admin-Login Passwort -- `ADMIN_SESSION_SECRET`: langer Zufallswert zum Signieren der Admin-Session -- `AUTH_COOKIE_SECURE`: `true` in Produktion mit HTTPS, lokal `false` -- `OLYMPUS_INTAKE_API_URL`: vorbereitet für spätere serverseitige Olympus-Anbindung -- `OLYMPUS_INTAKE_API_TOKEN`: vorbereitet für spätere serverseitige Olympus-Anbindung -- `OLYMPUS_PUBLIC_STATUS_API_URL`: serverseitig erreichbare Olympus-Basis-URL für öffentliche Reparaturstatuslinks -- `OLYMPUS_PUBLIC_STATUS_API_TOKEN`: optionaler serverseitiger Token für spätere geschützte Status-API-Zugriffe +- `NEXT_PUBLIC_SITE_URL` +- `OLYMPUS_INTAKE_API_URL` +- `OLYMPUS_INTAKE_API_TOKEN` -Die Intake-Variablen sind für eine spätere serverseitige Integration vorbereitet. Die Status-Variablen werden für `/status/[token]` serverseitig verwendet. Sie werden nicht im Browser verwendet. - -Für Reparaturstatuslinks muss `OLYMPUS_PUBLIC_STATUS_API_URL` vom Next.js-Server erreichbar sein. In Docker kann das je nach Netzwerk z. B. `http://hermes:8000` sein. Produktiv sollte eine interne Server-zu-Server-Adresse oder eine abgesicherte Olympus-URL verwendet werden. - -Für lokale Entwicklung kann `AUTH_COOKIE_SECURE=false` bleiben. Produktiv muss HTTPS verwendet und `AUTH_COOKIE_SECURE=true` gesetzt werden. `ADMIN_SESSION_SECRET` muss produktiv ein langer zufälliger Wert sein. +Die Olympus-Variablen sind nur für eine spätere serverseitige Integration vorbereitet. Sie werden nicht im Browser verwendet. ## API-Routen - `POST /api/repair` - `POST /api/contact` -- `POST /api/admin/login` -- `POST /api/admin/logout` -- `PATCH /api/admin/contact/[id]` -- `PATCH /api/admin/repair/[id]` -- `POST /api/admin/settings` -- `POST /api/admin/smtp` -- `GET/POST /api/admin/media` -- `GET/PUT /api/admin/content` -- `GET /api/content/settings` -- `GET /api/media/[filename]` -- `GET /api/health` -## Öffentlicher Reparaturstatus - -Olympus CRM erzeugt sichere Statuslinks für Reparaturen. Die Website stellt dafür die öffentliche Route bereit: - -```text -/status/ -``` - -Die Seite ruft Olympus ausschließlich serverseitig auf: - -```text -${OLYMPUS_PUBLIC_STATUS_API_URL}/public/repairs/status/ -``` - -Der Browser sieht weder die Olympus-URL noch optionale Server-Tokens. Die öffentliche Statusseite zeigt nur: - -- Reparaturnummer -- Gerät -- aktuellen Status -- letzte Aktualisierung -- kundenfreundliche Status-Timeline -- optionalen Kostenvoranschlag aus Olympus CRM v0.8.6 - -Wenn Olympus einen Kostenvoranschlag im Status `sent` liefert, zeigt die Website: - -- Nummer, Titel, Nachricht an den Kunden und Gültigkeit -- Positionen mit Menge, Einheit und Betrag -- Zwischensumme, Steuer und Gesamtbetrag -- Aktionen zum Freigeben, Ablehnen oder für eine Rückfrage - -Diese Aktionen laufen nicht direkt vom Browser zu Olympus. Der Browser ruft ausschließlich relative Website-Endpunkte auf: - -```text -POST /api/status//estimate/approve -POST /api/status//estimate/decline -POST /api/status//estimate/question -``` - -Die Website leitet diese Anfragen serverseitig an Olympus weiter: - -```text -${OLYMPUS_PUBLIC_STATUS_API_URL}/public/repairs/status//estimate/ -``` - -Nicht angezeigt werden Kundendaten, interne Notizen, Diagnosedetails oder technische Fehlermeldungen. - -Fehlerverhalten: - -- ungültiger oder abgelaufener Token: freundliche Meldung mit Links zu Reparatur und Kontakt -- Olympus nicht erreichbar: neutrale Meldung ohne technische Details - -Aktuell validieren die Routen serverseitig, geben klare JSON-Antworten zurück und schreiben nur technische Metadaten in Server-Logs. Es werden keine Nachrichteninhalte oder Tokens geloggt. Kontakt- und Reparaturanfragen werden lokal unter `data/*.json` gespeichert und nicht versioniert. - -## Admin-Bereich - -Der geschützte Admin-Bereich ist unter `/admin/login` erreichbar. Er verwendet ein signiertes HttpOnly-Cookie und ist für Desktop und Tablet ausgelegt. - -Funktionen: - -- Dashboard mit Kennzahlen -- Kontaktanfragen verwalten -- Reparaturanfragen verwalten -- Website-Inhalte pflegen und sofort veröffentlichen -- Firmendaten pflegen -- SMTP-Konfiguration speichern und Testmail senden -- Medien hochladen -- SEO-Übersicht -- Systemübersicht - -Website-Inhalte werden unter `/admin/website` gepflegt und beim Speichern sofort auf der öffentlichen Website wirksam. Die Content-Architektur ist bewusst über einen zentralen `ContentService` gekapselt, damit später Olympus CMS oder PostgreSQL als Backend angebunden werden können. SMTP-Versand fuer Kontakt- und Reparaturanfragen ist serverseitig angebunden, sofern `/admin/smtp` vollstaendig konfiguriert ist. - -## Runtime Data - -Im Repository liegen nur: - -```text -data/.gitkeep -data/contact-inquiries.example.json -data/repair-inquiries.example.json -``` - -Echte Runtime-Dateien werden nicht committed: - -```text -data/contact-inquiries.json -data/repair-inquiries.json -data/site-settings.json -``` - -SMTP-Konfiguration wird unter `storage/config/smtp.json` gespeichert und nicht committed. Das SMTP-Passwort wird nicht im Admin-Formular ausgegeben. - -Uploads liegen unter `storage/uploads/images/`, werden über `/api/media/[filename]` ausgeliefert und nicht committed. - -Alte Uploads aus `public/uploads/images` werden beim Start einmalig nach `storage/uploads/images` migriert, falls sie dort noch nicht vorhanden sind. - -Website-Inhalte liegen unter `storage/content/` und werden nicht committed: - -```text -storage/content/home.json -storage/content/services.json -storage/content/radio-service.json -storage/content/repair.json -storage/content/about.json -storage/content/contact.json -storage/content/settings.json -storage/content/backups/ -``` - -Jede Seite besitzt eine eigene JSON-Datei. React-Komponenten lesen diese Dateien nicht direkt, sondern ausschließlich über `lib/content/service.ts`. Vor jedem Speichern erzeugt der Service eine Backup-Datei unter `storage/content/backups/` und behält pro Dokument die letzten 20 Versionen. Änderungen benötigen keinen Neustart und keinen Docker-Build. - -Bearbeitbar sind aktuell: - -- Startseite mit Hero, Call-to-Actions, Featureboxen, Leistungsboxen und Kundenversprechen -- Leistungen mit beliebig vielen sortierbaren und ein-/ausblendbaren Einträgen -- Funkgeräte-Service mit Einleitung, Marken, Fehlerbildern, Ablauf, Messmöglichkeiten, Abgleich und Reparaturtexten -- Reparaturannahme mit SEO-Daten und Seitentexten -- Über Uns mit Firmenbeschreibung, Werkstattbeschreibung und Philosophie -- Kontakt mit Kontaktinformationen, Öffnungszeiten, Maps-Link und Seitentexten -- Footer, Logo, Copyright, Header-CTA und globale SEO-Einstellungen -- SEO pro Seite inklusive Meta Title, Meta Description, Keywords, OpenGraph und Social Image - -Bilder für Logo und Social Image werden aus dem Medienbestand ausgewählt. Neue Uploads bleiben im privaten Storage und werden über `/api/media/[filename]` ausgeliefert. - -## Roadmap Kundenportal - -Ein vollständiges Kundenportal ist vorbereitet, aber noch nicht öffentlich implementiert. Die öffentliche Statuslink-Seite ist bereits vorhanden. Für spätere Ausbaustufen sind folgende Routen vorgesehen: - -- `/status/[token]`: öffentliche Statuslink-Seite für Olympus -- `/reparatur/status`: Statusabfrage für Reparaturanfragen -- `/portal/login`: geschützter Kundenlogin - -Diese Routen sind bewusst noch nicht angelegt. Die spätere Umsetzung soll serverseitig erfolgen, ohne Tokens im Browser-JavaScript zu speichern und ohne bestehende Admin- oder CMS-Funktionen zu umgehen. - -## Healthcheck - -```bash -scripts/healthcheck.sh -``` - -Oder direkt: - -```bash -curl http://127.0.0.1:3010/api/health -``` - -Antwort: - -```json -{ - "status": "ok", - "version": "0.4.1", - "storage": "ok", - "admin": "configured", - "smtp": "configured", - "timestamp": "..." -} -``` - -## SMTP - -SMTP wird im Adminbereich unter `/admin/smtp` konfiguriert. Die Konfiguration wird persistent unter `storage/config/smtp.json` gespeichert und liegt damit im Docker-Storage-Volume, nicht in `public/`. - -Pflichtfelder: - -- SMTP Host -- SMTP Port -- SMTP Benutzername -- SMTP Passwort -- Verschlüsselung -- Absenderadresse -- Empfängeradresse - -Für Apple Mail/iCloud Mail: - -```text -Host: smtp.mail.me.com -Port: 587 -Verschlüsselung: STARTTLS -Benutzername: vollständige E-Mail-Adresse -Passwort: app-spezifisches Passwort -``` - -Nicht das normale Apple-ID-Passwort verwenden. In der Apple-ID-Verwaltung ein app-spezifisches Passwort erzeugen und dieses als SMTP-Passwort speichern. - -Nach dem Speichern kann im Adminbereich eine Testmail gesendet werden. Kontakt- und Reparaturanfragen werden weiterhin lokal gespeichert. Wenn SMTP konfiguriert ist, wird zusaetzlich eine E-Mail an die konfigurierte Empfaengeradresse gesendet. Schlaegt der Mailversand fehl, bleibt die Anfrage gespeichert; der Besucher sieht keine technische Fehlermeldung. - -Troubleshooting: - -- Host, Port und Verschlüsselung prüfen -- bei Apple Mail STARTTLS und Port 587 verwenden -- vollständige E-Mail-Adresse als Benutzername verwenden -- app-spezifisches Passwort neu erzeugen -- letzte Testmail und letzte Fehlermeldung unter `/admin/smtp` prüfen - -## Deployment - -```bash -scripts/deploy.sh -``` - -Das Skript führt aus: - -- `docker compose config` -- `docker compose build` -- `docker compose up -d` -- Healthcheck über `/api/health` - -Produktiv muss die `.env` auf dem Zielsystem gepflegt werden. Secrets werden nicht ins Repository aufgenommen. - -## Update - -```bash -git pull -docker compose config -docker compose build -docker compose up -d -scripts/healthcheck.sh -``` - -## Backup - -```bash -scripts/backup.sh -``` - -Das Backup enthält `data/` und `storage/`. Die `.env` wird bewusst nicht automatisch gesichert. Sie muss separat sicher abgelegt werden. - -## Restore - -```bash -scripts/restore.sh backups/funktechnik-data-YYYYMMDD-HHMMSS.tar.gz -``` - -Danach Container neu starten: - -```bash -docker compose up -d -scripts/healthcheck.sh -``` +Aktuell validieren die Routen serverseitig, geben klare JSON-Antworten zurück und schreiben nur technische Metadaten in Server-Logs. Es werden keine Nachrichteninhalte oder Tokens geloggt. ## Nginx @@ -394,5 +103,6 @@ certbot --nginx -d funktechnik-schubert.de -d www.funktechnik-schubert.de ## Offene manuelle Punkte - TODO: Rechtliche Angaben ergänzen +- Produktives Kontakt-/Mail-System anbinden - Spätere Olympus-Reparaturannahme serverseitig anbinden - Finale Domain und HTTPS-Konfiguration setzen diff --git a/app/admin/einstellungen/page.tsx b/app/admin/einstellungen/page.tsx deleted file mode 100644 index de7a795..0000000 --- a/app/admin/einstellungen/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { redirect } from "next/navigation"; -import AdminShell from "@/components/admin/AdminShell"; -import { requireAdminSession } from "@/lib/admin/auth"; -import { getSiteSettings } from "@/lib/admin/store"; - -export default async function AdminSettingsPage() { - if (!await requireAdminSession()) redirect("/admin/login"); - const settings = await getSiteSettings(); - - return ( - -
-

Konfiguration

-

Firmeneinstellungen

-
-
- - - - - - -