Repo for the whole DH/AS webstack
Find a file
Daniel Heße a69b81e3d7
Some checks failed
CI / ci (push) Has been cancelled
Added CI/CD workflow
2026-08-21 13:12:53 +02:00
.forgejo/workflows Added CI/CD workflow 2026-08-21 13:12:53 +02:00
assets/.aistudio feat: initialize dual identity author portfolio 2026-06-10 22:33:37 +02:00
data Changed folder structure 2026-07-20 21:46:53 +02:00
src feat: add downloadable publication library 2026-08-18 16:07:15 +02:00
tests feat: add downloadable publication library 2026-08-18 16:07:15 +02:00
.dockerignore Switched to modular data-layout. Added fourth portfolio. 2026-07-22 16:46:50 +02:00
.env.example feat: secure admin authentication with sessions 2026-08-15 00:36:01 +02:00
.gitignore Switched to modular data-layout. Added fourth portfolio. 2026-07-22 16:46:50 +02:00
bun.lock Selektiv die AI-Änderungen übernommen 2026-07-20 13:53:24 +02:00
docker-compose.yml feat: secure admin authentication with sessions 2026-08-15 00:36:01 +02:00
Dockerfile Added CI/CD workflow 2026-08-21 13:12:53 +02:00
index.html feat: harden persistence and enrich portfolio projects 2026-08-14 22:43:40 +02:00
metadata.json feat: initialize dual identity author portfolio 2026-06-10 22:33:37 +02:00
package-lock.json Added CI/CD workflow 2026-08-21 13:12:53 +02:00
package.json test: harden uploads and cover critical flows 2026-08-15 08:45:07 +02:00
README.md feat: add downloadable publication library 2026-08-18 16:07:15 +02:00
server.ts feat: add downloadable publication library 2026-08-18 16:07:15 +02:00
tsconfig.json feat: initialize dual identity author portfolio 2026-06-10 22:33:37 +02:00
vite.config.ts feat: initialize dual identity author portfolio 2026-06-10 22:33:37 +02:00

Autoren-Portfolio-Suite

Eine selbst gehostete Portfolio-Anwendung für mehrere voneinander getrennte Autorenprofile. Inhalte, Bücher, laufende Projekte, Designs, SEO-Daten und Rechtstexte werden über eine gemeinsame, passwortgeschützte Administrationsoberfläche gepflegt.

Technisch besteht die Anwendung aus einem React-/Tailwind-Frontend und einem Express-Backend. Sie kann als einzelner Docker-Container betrieben werden und benötigt keine externe Datenbank.

Funktionsumfang

  • Mehrere unabhängig konfigurierte Autorenprofile mit eigenen Domains und Themes
  • Biografie, Schlagworte, individuelle Texte und optionale Zusatzsektion mit bis zu drei CTA-Buttons
  • Bücherregal mit Detailansicht, Cover, Kauflink und optionaler Spotify-Playlist
  • Buchreihen mit Serienname und automatisch sortierter Bandnummer
  • getrennte KDP-Links für E-Book und Taschenbuch
  • optionaler, lokal gehosteter PDF-Leseproben-Download pro Buch
  • optionaler Kurzgeschichten- und Download-Bereich mit PDF und ePUB
  • Aktuelle Projekte mit Fortschritt, Markdown-Detailtext, Bild und optionaler Spotify-Playlist
  • Lokaler Bild-Upload über den Adminbereich
  • Verwaltung von Impressum, Datenschutzerklärung und weiteren Rechtstexten
  • Optionale serverseitige Gemini-Unterstützung für Klappentexte
  • Profilabhängige SEO-Metadaten, Open Graph, strukturierte Daten, Sitemap und robots.txt
  • Health-Endpunkte, persistente JSON-Datenhaltung und automatische Migrationsbackups
  • optionale Kontaktsektion je Profil für E-Mail, Instagram, Threads und Discord
  • adressierbare und teilbare Buch-/Projektansichten
  • neutrale, nicht indexierbare 404-Seite ohne Hinweise auf andere Profile

Trennung der Profile

Die öffentliche Seite erhält ausschließlich die Daten des Profils, das zur aufgerufenen Domain gehört. Andere Profile, deren Domains und interne Pfadkonfigurationen werden von der öffentlichen API nicht ausgeliefert.

Im Produktionsmodus gilt der Hostname als Trennlinie:

  • Konfigurierte Domains reagieren nur auf die exakte Domain und deren www-Variante.
  • Ein URL-Pfad oder Query-Parameter kann nicht zu einem anderen Profil wechseln.
  • Pfadbasierte Profilvorschauen stehen nur in der lokalen Entwicklungsumgebung zur Verfügung.
  • Vollständige Profildaten sind ausschließlich über einen authentifizierten Admin-Endpunkt verfügbar.
  • Gemeinsam gepflegte Rechtstexte werden bei der öffentlichen Ausgabe von strukturiert bekannten Kennungen anderer Profile bereinigt. Die gespeicherten Originaltexte bleiben unverändert.
  • Querverlinkungen entstehen nur durch ausdrücklich gepflegte Buttons oder Inhalte des jeweiligen Profils; es gibt keine automatische Verbindung zu anderen Profilen.

Der Reverse Proxy sollte zusätzlich nur die tatsächlich verwendeten Domains an den Container weiterleiten.

Architektur

Die öffentlichen Portfolioseiten verwenden eine gemeinsame, daten- und themegesteuerte React-Komponente. Dadurch liegen Layout, Buch- und Projektmodale, Rechtstexte sowie die Medienintegration nur einmal im Code vor. Profilbezogene Inhalte und Farben kommen aus der Datenbank; die Auswahl des Profils findet serverseitig statt.

Wichtige Verzeichnisse und Dateien:

src/App.tsx                         Öffentliche/Admin-Routen und Datenabruf
src/components/PortfolioPage.tsx   Gemeinsame öffentliche Portfolioseite
src/components/AdminPanel.tsx      Administrationsarbeitsbereich
src/components/admin/              Ausgelagerte Admin-Komponenten
src/defaultData.ts                 Ausgangsdaten für eine neue Installation
server.ts                           API, Routing, Sessions, Uploads und SEO
data/database.json                 Persistente Inhaltsdaten
data/uploads/                       Lokal hochgeladene Bilder
data/downloads/                     Lokal hochgeladene PDF-Leseproben
data/backups/                       Automatische Migrationsbackups
tests/                              Integrations- und Sicherheitstests

Docker-Inbetriebnahme

1. Konfiguration anlegen

Kopiere .env.example nach .env und ersetze die Beispielwerte:

ADMIN_PASSWORD=EinLangesZufaelligesAdminPasswort
SESSION_SECRET=EineUnabhaengigeZufaelligeZeichenfolgeMitMindestens32Zeichen

# Optional: aktiviert den Klappentext-Assistenten
GEMINI_API_KEY=

# Nur setzen, wenn genau ein vertrauenswürdiger Reverse Proxy vorgeschaltet ist
# TRUST_PROXY=1

Für den Produktionsbetrieb gelten folgende Mindestanforderungen:

  • ADMIN_PASSWORD: gesetzt und mindestens 12 Zeichen lang
  • SESSION_SECRET: gesetzt, unabhängig vom Passwort und mindestens 32 Zeichen lang

Ohne gültige Werte startet der Server im Produktionsmodus bewusst nicht. Geheimnisse gehören ausschließlich in .env beziehungsweise in die Secret-Verwaltung der Betriebsumgebung und nicht in docker-compose.yml oder Git.

2. Container starten

docker compose up -d --build

Die Anwendung ist anschließend am veröffentlichten Container-Port 3000 erreichbar. In einer öffentlichen Installation sollte davor ein Reverse Proxy wie Caddy, Traefik oder Nginx Proxy Manager TLS übernehmen und ausschließlich die vorgesehenen Domains weiterleiten.

Wenn genau ein vertrauenswürdiger Proxy vorgeschaltet und Port 3000 nicht direkt öffentlich erreichbar ist, kann TRUST_PROXY=1 gesetzt werden. Das sorgt dafür, dass das Login-Limit die ursprüngliche Client-IP verwendet. Bei direkter Veröffentlichung des Ports darf diese Option nicht aktiviert werden.

Installation ohne Docker

Voraussetzung ist eine unterstützte Node.js-Version (empfohlen: Node.js 20 oder neuer).

npm install
npm run build
NODE_ENV=production \
ADMIN_PASSWORD="EinLangesZufaelligesAdminPasswort" \
SESSION_SECRET="EineUnabhaengigeZufaelligeZeichenfolgeMitMindestens32Zeichen" \
npm start

Optional können GEMINI_API_KEY, TRUST_PROXY, PORT und DATA_DIR ergänzt werden. Standardmäßig lauscht die Anwendung auf Port 3000 und verwendet ./data als Datenverzeichnis.

Für die lokale Entwicklung genügt:

npm install
npm run dev

Die im Entwicklungsmodus vorhandenen Fallback-Secrets sind ausschließlich für lokale Entwicklung gedacht und werden in Produktion nicht akzeptiert.

Admin-Authentifizierung und Schutzmaßnahmen

Das Admin-Passwort wird nur beim Login übertragen und weder als Bearer-Token zurückgegeben noch im Browser gespeichert. Nach erfolgreicher Anmeldung wird eine zufällige, serverseitig verwaltete Sitzung erzeugt.

  • Sitzungsdauer: 24 Stunden
  • Cookie: HttpOnly, SameSite=Strict, in Produktion zusätzlich Secure
  • Sitzungen liegen nur im Arbeitsspeicher und enden spätestens bei einem Server-/Container-Neustart
  • Abgelaufene Sitzungen führen im Adminbereich kontrolliert zurück zur Anmeldung
  • Schreibende Admin-Anfragen werden auf gleiche Herkunft geprüft
  • Fehlgeschlagene Logins werden pro Client-IP begrenzt
  • Uploads und Gemini-Aufrufe besitzen zusätzliche Sitzungslimits
  • Sicherheitsheader und eine Content Security Policy werden serverseitig gesetzt
  • Profil- und Rechtstextänderungen verwenden eine Revision; parallele Änderungen werden mit einem Konflikthinweis abgelehnt statt überschrieben

Ein vergessenes Admin-Passwort wird über die Betriebsumgebung geändert; es gibt keine öffentliche Passwort-zurücksetzen-Funktion.

Datenhaltung und Backups

Alle Inhaltsdaten liegen in data/database.json. Docker bindet mit ./data:/app/data das vollständige Datenverzeichnis auf dem Host ein. Damit bleiben Daten und Uploads beim Neubauen oder Ersetzen des Containers erhalten.

Für ein vollständiges Backup sollte der gesamte Ordner data/ gesichert werden:

data/database.json
data/uploads/
data/downloads/
data/backups/

Schreibvorgänge auf die JSON-Datenbank erfolgen atomar und nacheinander. Vor schemaändernden Migrationen legt die Anwendung eine unveränderte Sicherung in data/backups/ an. Diese lokalen Sicherungen ersetzen kein externes, regelmäßig getestetes Backup.

Bild-Uploads

Der Adminbereich akzeptiert JPEG, PNG, WebP, GIF und AVIF mit maximal 8 MB pro Datei.

Der Server:

  • prüft Dateiendung, angegebenen MIME-Typ und den tatsächlichen Dateikopf,
  • lehnt SVG, HTML und andere aktive Formate ab,
  • erzeugt zufällige, nicht überschreibbare Dateinamen,
  • begrenzt Uploadversuche pro Admin-Sitzung und Stunde,
  • liefert Uploads mit nosniff und langfristigen Cache-Headern aus.
  • erlaubt das Löschen unbenutzter Bilder in der Server-Mediathek, schützt aber Bilder, die noch in einem Profil referenziert werden.

Die Bilder können für Avatare, Banner, Buchcover und aktuelle Projekte verwendet werden. Leere Bildfelder erzeugen keinen Request zu einem externen Standardbild.

Optionale Buttons im Zusatzabschnitt

Im zusätzlichen Textabschnitt eines Profils können bis zu drei CTA-Buttons gepflegt werden. Jeder Button benötigt eine Beschriftung und eine vollständige http://- oder https://-Adresse. Unvollständige Einträge werden nicht angezeigt, andere URL-Schemata werden serverseitig abgelehnt.

Die Buttons öffnen das Ziel in einem neuen Tab und werden automatisch mit den Akzentfarben des Profils gestaltet. Auf kleinen Bildschirmen stehen sie untereinander, auf größeren Bildschirmen nebeneinander. Damit können ausgewählte Pseudonyme bewusst miteinander verknüpft werden, ohne dass daraus eine automatische Verlinkung zu weiteren Profilen entsteht.

Kontakt und Social Media

Jedes Profil kann unabhängig eine Kontakt-E-Mail-Adresse sowie Links zu Instagram, Threads und Discord erhalten. Der Abschnitt erscheint unterhalb des zusätzlichen Textmoduls und zeigt nur tatsächlich gepflegte Angaben. Es gibt bewusst kein serverseitiges Kontaktformular; dadurch entstehen weder Spam-Endpunkt noch zusätzliche gespeicherte Kontaktdaten.

Bücher können optional einem Seriennamen und einer Bandnummer zugeordnet werden. Serien werden im öffentlichen Bücherregal nach Name und Bandnummer sortiert; Einzelbände behalten ihre vorhandene Reihenfolge. Für den Bezug stehen getrennte Links für E-Book und Taschenbuch zur Verfügung. Der historische einzelne Kauflink bleibt für bestehende Daten als E-Book-Fallback kompatibel.

Pro Buch kann im Adminbereich eine PDF-Leseprobe mit maximal 10 MB hochgeladen werden. Der Server prüft Dateiendung, MIME-Uploadformat und PDF-Dateikopf und liefert die Datei als Download mit nosniff aus. Ohne hinterlegte Datei erscheint kein Leseproben-Button. PDF-Dateien liegen getrennt von Bildern unter data/downloads/ und müssen daher in Backups eingeschlossen werden.

Kurzgeschichten und Downloads

Jedes Profil kann einen eigenen optionalen Download-Bereich pflegen. Einträge bestehen aus Titel, kurzer Markdown-Beschreibung, optionalem Veröffentlichungsdatum, optionalem Bild sowie einer PDF- und/oder ePUB-Datei. Ein Eintrag ohne verfügbare Datei wird öffentlich nicht angezeigt; ohne Einträge verschwinden der gesamte Abschnitt und sein Navigationslink.

Öffentlich erscheinen zunächst höchstens drei Karten. Bei weiteren Einträgen können Besucher mit „Alle Kurzgeschichten anzeigen“ die vollständige Liste einblenden und anschließend wieder einklappen. Die Reihenfolge wird im Adminbereich explizit über Hoch-/Runter-Aktionen gepflegt; neu angelegte Downloads stehen zunächst oben.

PDF und ePUB werden getrennt validiert, lokal unter data/downloads/ gespeichert und direkt als Download ausgeliefert. Beide Formate sind auf 10 MB begrenzt. Der Server prüft bei PDF den Dateikopf und bei ePUB die ZIP-/ePUB-Struktur. Ersetzte oder gelöschte Dateien werden entfernt, sobald kein veröffentlichter Eintrag mehr auf sie verweist.

Teilbare Detailansichten

Buch- und Projektmodale besitzen adressierbare URLs über ?book=<id> beziehungsweise ?project=<id>. Auf geeigneten Mobilgeräten öffnet „Teilen“ den nativen Teilen-Dialog; andernfalls wird die aktuelle URL in die Zwischenablage kopiert. Die URL enthält nur die ID innerhalb des aktuell aufgerufenen Profils und ermöglicht keinen Zugriff auf andere Profile.

Unbekannte Produktionspfade liefern HTTP 404, noindex und eine profilneutrale Fehlerseite. Lokale pfadbasierte Entwicklungsvorschauen bleiben davon unberührt.

Spotify und externe Dienste

Spotify-Playlists werden sowohl in Buch- als auch in Projektdetails nach dem Zwei-Klick-Prinzip eingebunden. Beim Öffnen eines Details erscheint zunächst nur ein lokaler Platzhalter. Erst nach einem bewussten Klick auf „Spotify-Player laden“ wird das Spotify-iframe erzeugt und eine Verbindung zu Spotify hergestellt.

Weitere mögliche externe Verbindungen:

  • GEMINI_API_KEY: Der optionale Admin-Assistent sendet die eingegebenen Buchinformationen serverseitig an die Google-Gemini-API.
  • Kauf- und sonstige Markdown-Links: Erst ein Klick führt zur jeweiligen externen Website.
  • Instagram-, Threads-, Discord- und bewusst gepflegte Profilverlinkungen: Eine Verbindung entsteht erst beim Klick.
  • Externe Bild-URLs: Das System unterstützt sie weiterhin, empfohlen werden jedoch lokal hochgeladene Bilder.

Die Anwendung enthält kein Analytics- oder Tracking-System und lädt keine externen Webfonts. Sie verwendet für Besucher weder localStorage noch sessionStorage. Der Browser speichert lediglich das notwendige Admin-Sitzungscookie nach einer Anmeldung. Diese Punkte sowie das übliche Logging des Reverse Proxys/Hosters sollten passend zur tatsächlichen Installation in der Datenschutzerklärung beschrieben werden.

SEO und Betriebsendpunkte

  • /health/live: Prozess läuft
  • /health/ready: Daten wurden geladen und der Server ist bereit
  • /robots.txt: profilabhängige Crawling-Regeln
  • /sitemap.xml: profilabhängige Sitemap
  • /admin: mit noindex gekennzeichneter Administrationsbereich

Seitentitel, Beschreibung, Canonical URL, Open-Graph-Daten und strukturierte Personendaten werden serverseitig für die aufgerufene Domain erzeugt. Optionale SEO-Felder können je Profil im Adminbereich gepflegt werden; fehlende Angaben werden aus bestehenden Profildaten abgeleitet.

Qualitätschecks

npm run typecheck  # TypeScript-Typprüfung
npm test           # Produktionsbuild und Integrationstests
npm run check      # Typprüfung, Build und Tests

Die Integrationstests verwenden ein temporäres Datenverzeichnis und einen kurzlebigen lokalen HTTP-Server. Geprüft werden unter anderem:

  • verpflichtende Produktions-Secrets,
  • Admin-Sitzung und Logout,
  • Origin-Schutz und Login-Limit,
  • exaktes Domain-Routing und öffentliche Profilisolation,
  • Schutz des vollständigen Admin-Datenendpunkts,
  • Revisionskonflikte und serverseitige Inhaltsvalidierung,
  • gültige und manipulierte Bild-Uploads,
  • Löschung unbenutzter sowie Schutz referenzierter Uploads,
  • validierte PDF-/ePUB-Uploads und Schutz referenzierter Download-Dateien,
  • wesentliche Sicherheitsheader.

Hinweise zur Aktualisierung

Vor einem Update sollte der komplette data/-Ordner gesichert werden. Danach kann das Image neu gebaut werden:

docker compose up -d --build

Vorhandene Inhaltsdaten werden nicht durch die im Quellcode enthaltenen Ausgangsdaten ersetzt. Neue optionale Felder werden migrationsfähig ergänzt; vor notwendigen Schemaänderungen wird automatisch ein Backup angelegt.